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