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 &lt;title/&gt; 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 &lt;title/&gt; 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            if (items == null) {
498                items = new ArrayList<>();
499            }
500            items.add(item);
501            return this;
502        }
503
504        /**
505         * Sets the fields that will be returned from a search.
506         *
507         * @param reportedData the fields that will be returned from a search.
508         * @return a reference to this builder.
509         */
510        public Builder setReportedData(ReportedData reportedData) {
511            this.reportedData = reportedData;
512            return this;
513        }
514
515        public Builder addExtensionElement(Element element) {
516            if (extensionElements == null) {
517                extensionElements = new ArrayList<>();
518            }
519            extensionElements.add(element);
520            return this;
521        }
522
523        public DataForm build() {
524            return new DataForm(this);
525        }
526    }
527
528    /**
529     *
530     * Represents the fields that will be returned from a search. This information is useful when
531     * you try to use the jabber:iq:search namespace to return dynamic form information.
532     *
533     * @author Gaston Dombiak
534     */
535    public static class ReportedData implements ExtensionElement {
536        public static final String ELEMENT = "reported";
537        public static final QName QNAME = new QName(NAMESPACE, ELEMENT);
538
539        private final List<? extends FormField> fields;
540
541        public ReportedData(List<? extends FormField> fields) {
542            this.fields = Collections.unmodifiableList(fields);
543        }
544
545        @Override
546        public String getElementName() {
547            return ELEMENT;
548        }
549
550        @Override
551        public String getNamespace() {
552            return NAMESPACE;
553        }
554
555        /**
556         * Returns the fields returned from a search.
557         *
558         * @return the fields returned from a search.
559         */
560        public List<? extends FormField> getFields() {
561            return fields;
562        }
563
564        @Override
565        public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) {
566            XmlStringBuilder xml = new XmlStringBuilder(this, xmlEnvironment);
567            xml.rightAngleBracket();
568            xml.append(getFields());
569            xml.closeElement(this);
570            return xml;
571        }
572
573    }
574
575    /**
576     *
577     * Represents items of reported data.
578     *
579     * @author Gaston Dombiak
580     */
581    public static class Item implements ExtensionElement {
582        public static final String ELEMENT = "item";
583        public static final QName QNAME = new QName(NAMESPACE, ELEMENT);
584
585        private final List<? extends FormField> fields;
586
587        public Item(List<? extends FormField> fields) {
588            this.fields = Collections.unmodifiableList(fields);
589        }
590
591        @Override
592        public String getElementName() {
593            return ELEMENT;
594        }
595
596        @Override
597        public String getNamespace() {
598            return NAMESPACE;
599        }
600
601        /**
602         * Returns the fields that define the data that goes with the item.
603         *
604         * @return the fields that define the data that goes with the item.
605         */
606        public List<? extends FormField> getFields() {
607            return fields;
608        }
609
610        @Override
611        public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) {
612            XmlStringBuilder xml = new XmlStringBuilder(this, xmlEnvironment);
613            xml.rightAngleBracket();
614            xml.append(getFields());
615            xml.closeElement(this);
616            return xml;
617        }
618    }
619}