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