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