001/**
002 *
003 * Copyright 2003-2007 Jive Software.
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;
019
020import java.text.ParseException;
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.Date;
024import java.util.List;
025
026import org.jivesoftware.smack.packet.NamedElement;
027import org.jivesoftware.smack.util.StringUtils;
028import org.jivesoftware.smack.util.XmlStringBuilder;
029
030import org.jivesoftware.smackx.xdatavalidation.packet.ValidateElement;
031
032import org.jxmpp.util.XmppDateTime;
033
034/**
035 * Represents a field of a form. The field could be used to represent a question to complete,
036 * a completed question or a data returned from a search. The exact interpretation of the field
037 * depends on the context where the field is used.
038 *
039 * @author Gaston Dombiak
040 */
041public class FormField implements NamedElement {
042
043    public static final String ELEMENT = "field";
044
045    /**
046     * The constant String "FORM_TYPE".
047     */
048    public static final String FORM_TYPE = "FORM_TYPE";
049
050    /**
051     * Form Field Types as defined in XEP-4 § 3.3.
052     *
053     * @see <a href="http://xmpp.org/extensions/xep-0004.html#protocol-fieldtypes">XEP-4 § 3.3 Field Types</a>
054     */
055    public enum Type {
056
057        /**
058         * Boolean type. Can be 0 or 1, true or false, yes or no. Default value is 0.
059         * <p>
060         * Note that in XEP-4 this type is called 'boolean', but since that String is a restricted keyword in Java, it
061         * is named 'bool' in Smack.
062         * </p>
063         */
064        bool,
065
066        /**
067         * Fixed for putting in text to show sections, or just advertise your web site in the middle of the form.
068         */
069        fixed,
070
071        /**
072         * Is not given to the user at all, but returned with the questionnaire.
073         */
074        hidden,
075
076        /**
077         * multiple entries for JIDs.
078         */
079        jid_multi,
080
081        /**
082         * Jabber ID - choosing a JID from your roster, and entering one based on the rules for a JID.
083         */
084        jid_single,
085
086        /**
087         * Given a list of choices, pick one or more.
088         */
089        list_multi,
090
091        /**
092         * Given a list of choices, pick one.
093         */
094        list_single,
095
096        /**
097         * Multiple lines of text entry.
098         */
099        text_multi,
100
101        /**
102         * Instead of showing the user what they typed, you show ***** to protect it.
103         */
104        text_private,
105
106        /**
107         * Single line or word of text.
108         */
109        text_single,
110        ;
111
112        @Override
113        public String toString() {
114            switch (this) {
115            case bool:
116                return "boolean";
117            default:
118                return this.name().replace('_', '-');
119            }
120        }
121
122        /**
123         * Get a form field type from the given string. If <code>string</code> is null, then null will be returned.
124         *
125         * @param string the string to transform or null.
126         * @return the type or null.
127         */
128        public static Type fromString(String string) {
129            if (string == null) {
130                return null;
131            }
132            switch (string) {
133            case "boolean":
134                return bool;
135            default:
136                string = string.replace('-', '_');
137                return Type.valueOf(string);
138            }
139        }
140    }
141
142    private final String variable;
143
144    private String description;
145    private boolean required = false;
146    private String label;
147    private Type type;
148    private final List<Option> options = new ArrayList<>();
149    private final List<CharSequence> values = new ArrayList<>();
150    private ValidateElement validateElement;
151
152    /**
153     * Creates a new FormField with the variable name that uniquely identifies the field
154     * in the context of the form.
155     *
156     * @param variable the variable name of the question.
157     */
158    public FormField(String variable) {
159        this.variable = StringUtils.requireNotNullOrEmpty(variable, "Variable must not be null or empty");
160    }
161
162    /**
163     * Creates a new FormField of type FIXED. The fields of type FIXED do not define a variable
164     * name.
165     */
166    public FormField() {
167        this.variable = null;
168        this.type = Type.fixed;
169    }
170
171    /**
172     * Returns a description that provides extra clarification about the question. This information
173     * could be presented to the user either in tool-tip, help button, or as a section of text
174     * before the question.
175     * <p>
176     * If the question is of type FIXED then the description should remain empty.
177     * </p>
178     *
179     * @return description that provides extra clarification about the question.
180     */
181    public String getDescription() {
182        return description;
183    }
184
185    /**
186     * Returns the label of the question which should give enough information to the user to
187     * fill out the form.
188     *
189     * @return label of the question.
190     */
191    public String getLabel() {
192        return label;
193    }
194
195    /**
196     * Returns a List of the available options that the user has in order to answer
197     * the question.
198     *
199     * @return List of the available options.
200     */
201    public List<Option> getOptions() {
202        synchronized (options) {
203            return Collections.unmodifiableList(new ArrayList<>(options));
204        }
205    }
206
207    /**
208     * Returns true if the question must be answered in order to complete the questionnaire.
209     *
210     * @return true if the question must be answered in order to complete the questionnaire.
211     */
212    public boolean isRequired() {
213        return required;
214    }
215
216    /**
217     * Returns an indicative of the format for the data to answer.
218     *
219     * @return format for the data to answer.
220     * @see Type
221     */
222    public Type getType() {
223        return type;
224    }
225
226    /**
227     * Returns a List of the default values of the question if the question is part
228     * of a form to fill out. Otherwise, returns a List of the answered values of
229     * the question.
230     *
231     * @return a List of the default values or answered values of the question.
232     */
233    public List<CharSequence> getValues() {
234        synchronized (values) {
235            return Collections.unmodifiableList(new ArrayList<>(values));
236        }
237    }
238
239    /**
240     * Returns the values a String. Note that you should use {@link #getValues()} whenever possible instead of this
241     * method.
242     *
243     * @return a list of Strings representing the values
244     * @see #getValues()
245     * @since 4.3
246     */
247    public List<String> getValuesAsString() {
248        List<CharSequence> valuesAsCharSequence = getValues();
249        List<String> res = new ArrayList<>(valuesAsCharSequence.size());
250        for (CharSequence value : valuesAsCharSequence) {
251            res.add(value.toString());
252        }
253        return res;
254    }
255
256    /**
257     * Returns the first value of this form fold or {@code null}.
258     *
259     * @return the first value or {@code null}
260     * @since 4.3
261     */
262    public String getFirstValue() {
263        CharSequence firstValue;
264        synchronized (values) {
265            firstValue = values.get(0);
266        }
267        if (firstValue == null) {
268            return null;
269        }
270        return firstValue.toString();
271    }
272
273    /**
274     * Parses the first value of this form field as XEP-0082 date/time format and returns a date instance or {@code null}.
275     *
276     * @return a Date instance representing the date/time information of the first value of this field.
277     * @throws ParseException if parsing fails.
278     * @since 4.3.0
279     */
280    public Date getFirstValueAsDate() throws ParseException {
281        String valueString = getFirstValue();
282        if (valueString == null) {
283            return null;
284        }
285        return XmppDateTime.parseXEP0082Date(valueString);
286    }
287
288    /**
289     * Returns the variable name that the question is filling out.
290     * <p>
291     * According to XEP-4 § 3.2 the variable name (the 'var' attribute)
292     * "uniquely identifies the field in the context of the form" (if the field is not of type 'fixed', in which case
293     * the field "MAY possess a 'var' attribute")
294     * </p>
295     *
296     * @return the variable name of the question.
297     */
298    public String getVariable() {
299        return variable;
300    }
301
302    /**
303     * Get validate element.
304     *
305     * @return the validateElement
306     */
307    public ValidateElement getValidateElement() {
308        return validateElement;
309    }
310
311    /**
312     * Sets a description that provides extra clarification about the question. This information
313     * could be presented to the user either in tool-tip, help button, or as a section of text
314     * before the question.
315     * <p>
316     * If the question is of type FIXED then the description should remain empty.
317     * </p>
318     *
319     * @param description provides extra clarification about the question.
320     */
321    public void setDescription(String description) {
322        this.description = description;
323    }
324
325    /**
326     * Sets the label of the question which should give enough information to the user to
327     * fill out the form.
328     *
329     * @param label the label of the question.
330     */
331    public void setLabel(String label) {
332        this.label = label;
333    }
334
335    /**
336     * Sets if the question must be answered in order to complete the questionnaire.
337     *
338     * @param required if the question must be answered in order to complete the questionnaire.
339     */
340    public void setRequired(boolean required) {
341        this.required = required;
342    }
343
344    /**
345     * Set validate element.
346     * @param validateElement the validateElement to set
347     */
348    public void setValidateElement(ValidateElement validateElement) {
349        validateElement.checkConsistency(this);
350        this.validateElement = validateElement;
351    }
352
353    /**
354     * Sets an indicative of the format for the data to answer.
355     * <p>
356     * This method will throw an IllegalArgumentException if type is 'fixed'. To create FormFields of type 'fixed' use
357     * {@link #FormField()} instead.
358     * </p>
359     *
360     * @param type an indicative of the format for the data to answer.
361     * @see Type
362     * @throws IllegalArgumentException if type is 'fixed'.
363     */
364    public void setType(Type type) {
365        if (type == Type.fixed) {
366            throw new IllegalArgumentException("Can not set type to fixed, use FormField constructor without arguments instead.");
367        }
368        this.type = type;
369    }
370
371    /**
372     * Adds a default value to the question if the question is part of a form to fill out.
373     * Otherwise, adds an answered value to the question.
374     *
375     * @param value a default value or an answered value of the question.
376     */
377    public void addValue(CharSequence value) {
378        synchronized (values) {
379            values.add(value);
380        }
381    }
382
383    /**
384     * Adds the given Date as XEP-0082 formated string by invoking {@link #addValue(CharSequence)} after the date
385     * instance was formated.
386     *
387     * @param date the date instance to add as XEP-0082 formated string.
388     * @since 4.3.0
389     */
390    public void addValue(Date date) {
391        String dateString = XmppDateTime.formatXEP0082Date(date);
392        addValue(dateString);
393    }
394
395    /**
396     * Adds a default values to the question if the question is part of a form to fill out.
397     * Otherwise, adds an answered values to the question.
398     *
399     * @param newValues default values or an answered values of the question.
400     */
401    public void addValues(List<? extends CharSequence> newValues) {
402        synchronized (values) {
403            values.addAll(newValues);
404        }
405    }
406
407    /**
408     * Removes all the values of the field.
409     */
410    protected void resetValues() {
411        synchronized (values) {
412            values.clear();
413        }
414    }
415
416    /**
417     * Adss an available options to the question that the user has in order to answer
418     * the question.
419     *
420     * @param option a new available option for the question.
421     */
422    public void addOption(Option option) {
423        synchronized (options) {
424            options.add(option);
425        }
426    }
427
428    @Override
429    public String getElementName() {
430        return ELEMENT;
431    }
432
433    @Override
434    public XmlStringBuilder toXML(String enclosingNamespace) {
435        XmlStringBuilder buf = new XmlStringBuilder(this);
436        // Add attributes
437        buf.optAttribute("label", getLabel());
438        buf.optAttribute("var", getVariable());
439        buf.optAttribute("type", getType());
440        buf.rightAngleBracket();
441        // Add elements
442        buf.optElement("desc", getDescription());
443        buf.condEmptyElement(isRequired(), "required");
444        // Loop through all the values and append them to the string buffer
445        for (CharSequence value : getValues()) {
446            buf.element("value", value);
447        }
448        // Loop through all the values and append them to the string buffer
449        for (Option option : getOptions()) {
450            buf.append(option.toXML(null));
451        }
452        buf.optElement(validateElement);
453        buf.closeElement(this);
454        return buf;
455    }
456
457    @Override
458    public boolean equals(Object obj) {
459        if (obj == null)
460            return false;
461        if (obj == this)
462            return true;
463        if (!(obj instanceof FormField))
464            return false;
465
466        FormField other = (FormField) obj;
467
468        return toXML(null).equals(other.toXML(null));
469    }
470
471    @Override
472    public int hashCode() {
473        return toXML(null).hashCode();
474    }
475
476    /**
477     * Represents the available option of a given FormField.
478     *
479     * @author Gaston Dombiak
480     */
481    public static class Option implements NamedElement {
482
483        public static final String ELEMENT = "option";
484
485        private final String value;
486        private String label;
487
488        public Option(String value) {
489            this.value = value;
490        }
491
492        public Option(String label, String value) {
493            this.label = label;
494            this.value = value;
495        }
496
497        /**
498         * Returns the label that represents the option.
499         *
500         * @return the label that represents the option.
501         */
502        public String getLabel() {
503            return label;
504        }
505
506        /**
507         * Returns the value of the option.
508         *
509         * @return the value of the option.
510         */
511        public String getValue() {
512            return value;
513        }
514
515        @Override
516        public String toString() {
517            return getLabel();
518        }
519
520        @Override
521        public String getElementName() {
522            return ELEMENT;
523        }
524
525        @Override
526        public XmlStringBuilder toXML(String enclosingNamespace) {
527            XmlStringBuilder xml = new XmlStringBuilder(this);
528            // Add attribute
529            xml.optAttribute("label", getLabel());
530            xml.rightAngleBracket();
531
532            // Add element
533            xml.element("value", getValue());
534
535            xml.closeElement(this);
536            return xml;
537        }
538
539        @Override
540        public boolean equals(Object obj) {
541            if (obj == null)
542                return false;
543            if (obj == this)
544                return true;
545            if (obj.getClass() != getClass())
546                return false;
547
548            Option other = (Option) obj;
549
550            if (!value.equals(other.value))
551                return false;
552
553            String thisLabel = label == null ? "" : label;
554            String otherLabel = other.label == null ? "" : other.label;
555
556            if (!thisLabel.equals(otherLabel))
557                return false;
558
559            return true;
560        }
561
562        @Override
563        public int hashCode() {
564            int result = 1;
565            result = 37 * result + value.hashCode();
566            result = 37 * result + (label == null ? 0 : label.hashCode());
567            return result;
568        }
569    }
570}