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 &lt;title/&gt; 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 &lt;title/&gt; 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}