001/** 002 * 003 * Copyright 2003-2007 Jive Software, 2020-2024 Florian Schmaus. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.jivesoftware.smackx.xdata.packet; 019 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.HashMap; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Locale; 027import java.util.Map; 028 029import javax.xml.namespace.QName; 030 031import org.jivesoftware.smack.packet.Element; 032import org.jivesoftware.smack.packet.ExtensionElement; 033import org.jivesoftware.smack.packet.StanzaView; 034import org.jivesoftware.smack.packet.XmlEnvironment; 035import org.jivesoftware.smack.util.CollectionUtil; 036import org.jivesoftware.smack.util.StringUtils; 037import org.jivesoftware.smack.util.XmlStringBuilder; 038 039import org.jivesoftware.smackx.formtypes.FormFieldRegistry; 040import org.jivesoftware.smackx.xdata.FormField; 041import org.jivesoftware.smackx.xdata.TextSingleFormField; 042 043/** 044 * Represents a form that could be use for gathering data as well as for reporting data 045 * returned from a search. 046 * <p> 047 * Note that unlike many other things in XMPP, the order of the form fields is actually 048 * Important in data forms. 049 * </p> 050 * 051 * @author Gaston Dombiak 052 */ 053public final class DataForm implements ExtensionElement { 054 055 public static final String NAMESPACE = "jabber:x:data"; 056 public static final String ELEMENT = "x"; 057 058 public static final QName QNAME = new QName(NAMESPACE, ELEMENT); 059 060 public enum Type { 061 /** 062 * This stanza contains a form to fill out. Display it to the user (if your program can). 063 */ 064 form, 065 066 /** 067 * The form is filled out, and this is the data that is being returned from the form. 068 */ 069 submit, 070 071 /** 072 * The form was cancelled. Tell the asker that piece of information. 073 */ 074 cancel, 075 076 /** 077 * Data results being returned from a search, or some other query. 078 */ 079 result, 080 ; 081 082 public static Type fromString(String string) { 083 return Type.valueOf(string.toLowerCase(Locale.US)); 084 } 085 } 086 087 private final Type type; 088 private final String title; 089 private final List<String> instructions; 090 private final ReportedData reportedData; 091 private final List<Item> items; 092 private final List<FormField> fields; 093 private final Map<String, FormField> fieldsMap; 094 private final List<Element> extensionElements; 095 096 private DataForm(Builder builder) { 097 type = builder.type; 098 title = builder.title; 099 instructions = CollectionUtil.cloneAndSeal(builder.instructions); 100 reportedData = builder.reportedData; 101 items = CollectionUtil.cloneAndSeal(builder.items); 102 103 builder.orderFields(); 104 105 fields = CollectionUtil.cloneAndSeal(builder.fields); 106 fieldsMap = CollectionUtil.cloneAndSeal(builder.fieldsMap); 107 extensionElements = CollectionUtil.cloneAndSeal(builder.extensionElements); 108 109 // Ensure that the types of the form fields of every data form is known by registering such fields. 110 if (type == Type.form) { 111 FormFieldRegistry.register(this); 112 } 113 } 114 115 /** 116 * Returns the meaning of the data within the context. The data could be part of a form 117 * to fill out, a form submission or data results. 118 * 119 * @return the form's type. 120 */ 121 public Type getType() { 122 return type; 123 } 124 125 /** 126 * Returns the description of the data. It is similar to the title on a web page or an X 127 * window. You can put a <title/> on either a form to fill out, or a set of data results. 128 * 129 * @return description of the data. 130 */ 131 public String getTitle() { 132 return title; 133 } 134 135 /** 136 * Returns a List of the list of instructions that explain how to fill out the form and 137 * what the form is about. The dataform could include multiple instructions since each 138 * instruction could not contain newlines characters. Join the instructions together in order 139 * to show them to the user. 140 * 141 * @return a List of the list of instructions that explain how to fill out the form. 142 */ 143 public List<String> getInstructions() { 144 return instructions; 145 } 146 147 /** 148 * Returns the fields that will be returned from a search. 149 * 150 * @return fields that will be returned from a search. 151 */ 152 public ReportedData getReportedData() { 153 return reportedData; 154 } 155 156 /** 157 * Returns a List of the items returned from a search. 158 * 159 * @return a List of the items returned from a search. 160 */ 161 public List<Item> getItems() { 162 return items; 163 } 164 165 /** 166 * Returns a List of the fields that are part of the form. 167 * 168 * @return a List of the fields that are part of the form. 169 */ 170 public List<FormField> getFields() { 171 return fields; 172 } 173 174 /** 175 * Return the form field with the given variable name or null. 176 * 177 * @param fieldName the name of the field (the value of the 'var' (variable) attribute) 178 * @return the form field or null. 179 * @since 4.1 180 */ 181 public FormField getField(String fieldName) { 182 return fieldsMap.get(fieldName); 183 } 184 185 /** 186 * Check if a form field with the given variable name exists. 187 * 188 * @param fieldName the name of the field. 189 * @return true if a form field with the variable name exists, false otherwise. 190 * @since 4.2 191 */ 192 public boolean hasField(String fieldName) { 193 return fieldsMap.containsKey(fieldName); 194 } 195 196 @Override 197 public String getElementName() { 198 return ELEMENT; 199 } 200 201 @Override 202 public String getNamespace() { 203 return NAMESPACE; 204 } 205 206 public List<Element> getExtensionElements() { 207 return extensionElements; 208 } 209 210 /** 211 * Return the form type from the hidden form type field. 212 * 213 * @return the form type or <code>null</code> if this form has none set. 214 * @since 4.4.0 215 */ 216 public String getFormType() { 217 FormField formTypeField = getHiddenFormTypeField(); 218 if (formTypeField == null) { 219 return null; 220 } 221 return formTypeField.getFirstValue(); 222 } 223 224 /** 225 * Returns the hidden FORM_TYPE field or null if this data form has none. 226 * 227 * @return the hidden FORM_TYPE field or null. 228 * @since 4.1 229 */ 230 public TextSingleFormField getHiddenFormTypeField() { 231 FormField field = getField(FormField.FORM_TYPE); 232 if (field == null) { 233 return null; 234 } 235 return field.asHiddenFormTypeFieldIfPossible(); 236 } 237 238 /** 239 * Returns true if this DataForm has at least one FORM_TYPE field which is 240 * hidden. This method is used for sanity checks. 241 * 242 * @return true if there is at least one field which is hidden. 243 */ 244 public boolean hasHiddenFormTypeField() { 245 return getHiddenFormTypeField() != null; 246 } 247 248 @Override 249 public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { 250 XmlStringBuilder buf = new XmlStringBuilder(this, xmlEnvironment); 251 buf.attribute("type", getType()); 252 buf.rightAngleBracket(); 253 254 xmlEnvironment = buf.getXmlEnvironment(); 255 256 buf.optElement("title", getTitle()); 257 for (String instruction : getInstructions()) { 258 buf.element("instructions", instruction); 259 } 260 // Append the list of fields returned from a search 261 buf.optElement(getReportedData()); 262 // Loop through all the items returned from a search and append them to the string buffer 263 buf.append(getItems()); 264 265 // Add all form fields. 266 // We do not need to include the type for data forms of the type submit. 267 boolean includeType = getType() != Type.submit; 268 for (FormField formField : getFields()) { 269 buf.append(formField.toXML(xmlEnvironment, includeType)); 270 } 271 272 buf.append(getExtensionElements()); 273 buf.closeElement(this); 274 return buf; 275 } 276 277 public Builder asBuilder() { 278 return new Builder(this); 279 } 280 281 /** 282 * Get data form from a stanza. 283 * 284 * @param stanzaView the stanza to get data form from. 285 * @return the DataForm or null 286 */ 287 public static DataForm from(StanzaView stanzaView) { 288 return from(stanzaView, null); 289 } 290 291 /** 292 * Get the data form with the given form type from a stanza view. 293 * 294 * @param stanzaView the stanza view to retrieve the data form from 295 * @param formType the form type 296 * @return the retrieved data form or <code>null</code> if there is no matching one 297 * @since 4.4.0 298 */ 299 public static DataForm from(StanzaView stanzaView, String formType) { 300 if (formType == null) { 301 return stanzaView.getExtension(DataForm.class); 302 } 303 List<DataForm> dataForms = stanzaView.getExtensions(DataForm.class); 304 return from(dataForms, formType); 305 } 306 307 /** 308 * Return the first matching data form with the given form type from the given collection of data forms. 309 * 310 * @param dataForms the collection of data forms 311 * @param formType the form type to match for 312 * @return the first matching data form or <code>null</code> if there is none 313 * @since 4.4.0 314 */ 315 public static DataForm from(Collection<DataForm> dataForms, String formType) { 316 for (DataForm dataForm : dataForms) { 317 if (formType.equals(dataForm.getFormType())) { 318 return dataForm; 319 } 320 } 321 return null; 322 } 323 324 /** 325 * Remove the first matching data form with the given form type from the given collection. 326 * 327 * @param dataForms the collection of data forms 328 * @param formType the form type to match for 329 * @return the removed data form or <code>null</code> if there was none removed 330 * @since 4.4.0 331 */ 332 public static DataForm remove(Collection<DataForm> dataForms, String formType) { 333 Iterator<DataForm> it = dataForms.iterator(); 334 while (it.hasNext()) { 335 DataForm dataForm = it.next(); 336 if (formType.equals(dataForm.getFormType())) { 337 it.remove(); 338 return dataForm; 339 } 340 } 341 return null; 342 } 343 344 /** 345 * Get a new data form builder with the form type set to {@link Type#submit}. 346 * 347 * @return a new data form builder. 348 */ 349 public static Builder builder() { 350 return new Builder(); 351 } 352 353 public static Builder builder(Type type) { 354 return new Builder(type); 355 } 356 357 public static final class Builder { 358 private final Type type; 359 private String title; 360 private List<String> instructions; 361 private ReportedData reportedData; 362 private List<Item> items; 363 private List<FormField> fields = new ArrayList<>(); 364 private Map<String, FormField> fieldsMap = new HashMap<>(); 365 private List<Element> extensionElements; 366 367 private Builder() { 368 this(Type.submit); 369 } 370 371 private Builder(Type type) { 372 this.type = type; 373 } 374 375 private Builder(DataForm dataForm) { 376 type = dataForm.getType(); 377 title = dataForm.getTitle(); 378 instructions = dataForm.getInstructions(); 379 reportedData = dataForm.getReportedData(); 380 items = CollectionUtil.newListWith(dataForm.getItems()); 381 fields = CollectionUtil.newListWith(dataForm.getFields()); 382 fieldsMap = new HashMap<>(dataForm.fieldsMap); 383 extensionElements = CollectionUtil.newListWith(dataForm.getExtensionElements()); 384 } 385 386 private void orderFields() { 387 Iterator<FormField> it = fields.iterator(); 388 if (!it.hasNext()) { 389 return; 390 } 391 392 FormField hiddenFormTypeField = it.next().asHiddenFormTypeFieldIfPossible(); 393 if (hiddenFormTypeField != null) { 394 // The hidden FROM_TYPE field is already in first position, nothing to do. 395 return; 396 } 397 398 while (it.hasNext()) { 399 hiddenFormTypeField = it.next().asHiddenFormTypeFieldIfPossible(); 400 if (hiddenFormTypeField != null) { 401 // Remove the hidden FORM_TYPE field that is not on first position. 402 it.remove(); 403 // And insert it again at first position. 404 fields.add(0, hiddenFormTypeField); 405 break; 406 } 407 } 408 } 409 410 /** 411 * Sets the description of the data. It is similar to the title on a web page or an X window. 412 * You can put a <title/> on either a form to fill out, or a set of data results. 413 * 414 * @param title description of the data. 415 * @return a reference to this builder. 416 */ 417 public Builder setTitle(String title) { 418 this.title = title; 419 return this; 420 } 421 422 /** 423 * Adds a new field as part of the form. 424 * 425 * @param field the field to add to the form. 426 * @return a reference to this builder. 427 */ 428 public Builder addField(FormField field) { 429 String fieldName = field.getFieldName(); 430 if (fieldName != null) { 431 if (fieldsMap.containsKey(fieldName)) { 432 throw new IllegalArgumentException("A field with the name " + fieldName + " already exists"); 433 } 434 435 fieldsMap.put(fieldName, field); 436 } 437 fields.add(field); 438 439 return this; 440 } 441 442 /** 443 * Add the given fields to this form. 444 * 445 * @param fieldsToAdd TODO javadoc me please 446 * @return a reference to this builder. 447 */ 448 public Builder addFields(Collection<? extends FormField> fieldsToAdd) { 449 for (FormField field : fieldsToAdd) { 450 String fieldName = field.getFieldName(); 451 if (fieldsMap.containsKey(fieldName)) { 452 throw new IllegalArgumentException("A field with the name " + fieldName + " already exists"); 453 } 454 } 455 for (FormField field : fieldsToAdd) { 456 String fieldName = field.getFieldName(); 457 if (fieldName != null) { 458 fieldsMap.put(fieldName, field); 459 } 460 fields.add(field); 461 } 462 return this; 463 } 464 465 public Builder removeField(String fieldName) { 466 FormField field = fieldsMap.remove(fieldName); 467 if (field != null) { 468 fields.remove(field); 469 } 470 return this; 471 } 472 473 public Builder setFormType(String formType) { 474 FormField formField = FormField.buildHiddenFormType(formType); 475 return addField(formField); 476 } 477 478 public Builder setInstructions(String instructions) { 479 return setInstructions(StringUtils.splitLinesPortable(instructions)); 480 } 481 482 /** 483 * Sets the list of instructions that explain how to fill out the form and what the form is 484 * about. The dataform could include multiple instructions since each instruction could not 485 * contain newlines characters. 486 * 487 * @param instructions list of instructions that explain how to fill out the form. 488 * @return a reference to this builder. 489 */ 490 public Builder setInstructions(List<String> instructions) { 491 this.instructions = instructions; 492 return this; 493 } 494 495 /** 496 * Adds a new instruction to the list of instructions that explain how to fill out the form 497 * and what the form is about. The dataform could include multiple instructions since each 498 * instruction could not contain newlines characters. 499 * 500 * @param instruction the new instruction that explain how to fill out the form. 501 * @return a reference to this builder. 502 */ 503 public Builder addInstruction(String instruction) { 504 if (instructions == null) { 505 instructions = new ArrayList<>(); 506 } 507 instructions.add(instruction); 508 return this; 509 } 510 511 /** 512 * Adds a new item returned from a search. 513 * 514 * @param item the item returned from a search. 515 * @return a reference to this builder. 516 */ 517 public Builder addItem(Item item) { 518 if (items == null) { 519 items = new ArrayList<>(); 520 } 521 items.add(item); 522 return this; 523 } 524 525 /** 526 * Sets the fields that will be returned from a search. 527 * 528 * @param reportedData the fields that will be returned from a search. 529 * @return a reference to this builder. 530 */ 531 public Builder setReportedData(ReportedData reportedData) { 532 this.reportedData = reportedData; 533 return this; 534 } 535 536 public Builder addExtensionElement(Element element) { 537 if (extensionElements == null) { 538 extensionElements = new ArrayList<>(); 539 } 540 extensionElements.add(element); 541 return this; 542 } 543 544 public DataForm build() { 545 return new DataForm(this); 546 } 547 } 548 549 /** 550 * 551 * Represents the fields that will be returned from a search. This information is useful when 552 * you try to use the jabber:iq:search namespace to return dynamic form information. 553 * 554 * @author Gaston Dombiak 555 */ 556 public static class ReportedData implements ExtensionElement { 557 public static final String ELEMENT = "reported"; 558 public static final QName QNAME = new QName(NAMESPACE, ELEMENT); 559 560 private final List<? extends FormField> fields; 561 562 private Map<String, FormField> fieldMap; 563 564 public ReportedData(List<? extends FormField> fields) { 565 this.fields = Collections.unmodifiableList(fields); 566 } 567 568 @Override 569 public String getElementName() { 570 return ELEMENT; 571 } 572 573 @Override 574 public String getNamespace() { 575 return NAMESPACE; 576 } 577 578 /** 579 * Returns the fields returned from a search. 580 * 581 * @return the fields returned from a search. 582 */ 583 public List<? extends FormField> getFields() { 584 return fields; 585 } 586 587 public FormField getField(String name) { 588 if (fieldMap == null) { 589 fieldMap = new HashMap<>(fields.size()); 590 for (FormField field : fields) { 591 String fieldName = field.getFieldName(); 592 fieldMap.put(fieldName, field); 593 } 594 } 595 596 return fieldMap.get(name); 597 } 598 599 @Override 600 public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { 601 XmlStringBuilder xml = new XmlStringBuilder(this, xmlEnvironment); 602 xml.rightAngleBracket(); 603 xml.append(getFields()); 604 xml.closeElement(this); 605 return xml; 606 } 607 608 } 609 610 /** 611 * 612 * Represents items of reported data. 613 * 614 * @author Gaston Dombiak 615 */ 616 public static class Item implements ExtensionElement { 617 public static final String ELEMENT = "item"; 618 public static final QName QNAME = new QName(NAMESPACE, ELEMENT); 619 620 private final List<? extends FormField> fields; 621 622 public Item(List<? extends FormField> fields) { 623 this.fields = Collections.unmodifiableList(fields); 624 } 625 626 @Override 627 public String getElementName() { 628 return ELEMENT; 629 } 630 631 @Override 632 public String getNamespace() { 633 return NAMESPACE; 634 } 635 636 /** 637 * Returns the fields that define the data that goes with the item. 638 * 639 * @return the fields that define the data that goes with the item. 640 */ 641 public List<? extends FormField> getFields() { 642 return fields; 643 } 644 645 @Override 646 public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { 647 XmlStringBuilder xml = new XmlStringBuilder(this, xmlEnvironment); 648 xml.rightAngleBracket(); 649 xml.append(getFields()); 650 xml.closeElement(this); 651 return xml; 652 } 653 } 654}