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
265        synchronized (values) {
266            if (values.isEmpty()) {
267                return null;
268            }
269            firstValue = values.get(0);
270        }
271
272        return firstValue.toString();
273    }
274
275    /**
276     * Parses the first value of this form field as XEP-0082 date/time format and returns a date instance or {@code null}.
277     *
278     * @return a Date instance representing the date/time information of the first value of this field.
279     * @throws ParseException if parsing fails.
280     * @since 4.3.0
281     */
282    public Date getFirstValueAsDate() throws ParseException {
283        String valueString = getFirstValue();
284        if (valueString == null) {
285            return null;
286        }
287        return XmppDateTime.parseXEP0082Date(valueString);
288    }
289
290    /**
291     * Returns the variable name that the question is filling out.
292     * <p>
293     * According to XEP-4 § 3.2 the variable name (the 'var' attribute)
294     * "uniquely identifies the field in the context of the form" (if the field is not of type 'fixed', in which case
295     * the field "MAY possess a 'var' attribute")
296     * </p>
297     *
298     * @return the variable name of the question.
299     */
300    public String getVariable() {
301        return variable;
302    }
303
304    /**
305     * Get validate element.
306     *
307     * @return the validateElement
308     */
309    public ValidateElement getValidateElement() {
310        return validateElement;
311    }
312
313    /**
314     * Sets a description that provides extra clarification about the question. This information
315     * could be presented to the user either in tool-tip, help button, or as a section of text
316     * before the question.
317     * <p>
318     * If the question is of type FIXED then the description should remain empty.
319     * </p>
320     *
321     * @param description provides extra clarification about the question.
322     */
323    public void setDescription(String description) {
324        this.description = description;
325    }
326
327    /**
328     * Sets the label of the question which should give enough information to the user to
329     * fill out the form.
330     *
331     * @param label the label of the question.
332     */
333    public void setLabel(String label) {
334        this.label = label;
335    }
336
337    /**
338     * Sets if the question must be answered in order to complete the questionnaire.
339     *
340     * @param required if the question must be answered in order to complete the questionnaire.
341     */
342    public void setRequired(boolean required) {
343        this.required = required;
344    }
345
346    /**
347     * Set validate element.
348     * @param validateElement the validateElement to set
349     */
350    public void setValidateElement(ValidateElement validateElement) {
351        validateElement.checkConsistency(this);
352        this.validateElement = validateElement;
353    }
354
355    /**
356     * Sets an indicative of the format for the data to answer.
357     * <p>
358     * This method will throw an IllegalArgumentException if type is 'fixed'. To create FormFields of type 'fixed' use
359     * {@link #FormField()} instead.
360     * </p>
361     *
362     * @param type an indicative of the format for the data to answer.
363     * @see Type
364     * @throws IllegalArgumentException if type is 'fixed'.
365     */
366    public void setType(Type type) {
367        if (type == Type.fixed) {
368            throw new IllegalArgumentException("Can not set type to fixed, use FormField constructor without arguments instead.");
369        }
370        this.type = type;
371    }
372
373    /**
374     * Adds a default value to the question if the question is part of a form to fill out.
375     * Otherwise, adds an answered value to the question.
376     *
377     * @param value a default value or an answered value of the question.
378     */
379    public void addValue(CharSequence value) {
380        synchronized (values) {
381            values.add(value);
382        }
383    }
384
385    /**
386     * Adds the given Date as XEP-0082 formated string by invoking {@link #addValue(CharSequence)} after the date
387     * instance was formated.
388     *
389     * @param date the date instance to add as XEP-0082 formated string.
390     * @since 4.3.0
391     */
392    public void addValue(Date date) {
393        String dateString = XmppDateTime.formatXEP0082Date(date);
394        addValue(dateString);
395    }
396
397    /**
398     * Adds a default values to the question if the question is part of a form to fill out.
399     * Otherwise, adds an answered values to the question.
400     *
401     * @param newValues default values or an answered values of the question.
402     */
403    public void addValues(List<? extends CharSequence> newValues) {
404        synchronized (values) {
405            values.addAll(newValues);
406        }
407    }
408
409    /**
410     * Removes all the values of the field.
411     */
412    protected void resetValues() {
413        synchronized (values) {
414            values.clear();
415        }
416    }
417
418    /**
419     * Adss an available options to the question that the user has in order to answer
420     * the question.
421     *
422     * @param option a new available option for the question.
423     */
424    public void addOption(Option option) {
425        synchronized (options) {
426            options.add(option);
427        }
428    }
429
430    @Override
431    public String getElementName() {
432        return ELEMENT;
433    }
434
435    @Override
436    public XmlStringBuilder toXML(String enclosingNamespace) {
437        XmlStringBuilder buf = new XmlStringBuilder(this);
438        // Add attributes
439        buf.optAttribute("label", getLabel());
440        buf.optAttribute("var", getVariable());
441        buf.optAttribute("type", getType());
442        buf.rightAngleBracket();
443        // Add elements
444        buf.optElement("desc", getDescription());
445        buf.condEmptyElement(isRequired(), "required");
446        // Loop through all the values and append them to the string buffer
447        for (CharSequence value : getValues()) {
448            buf.element("value", value);
449        }
450        // Loop through all the values and append them to the string buffer
451        for (Option option : getOptions()) {
452            buf.append(option.toXML(null));
453        }
454        buf.optElement(validateElement);
455        buf.closeElement(this);
456        return buf;
457    }
458
459    @Override
460    public boolean equals(Object obj) {
461        if (obj == null)
462            return false;
463        if (obj == this)
464            return true;
465        if (!(obj instanceof FormField))
466            return false;
467
468        FormField other = (FormField) obj;
469
470        return toXML(null).equals(other.toXML(null));
471    }
472
473    @Override
474    public int hashCode() {
475        return toXML(null).hashCode();
476    }
477
478    /**
479     * Represents the available option of a given FormField.
480     *
481     * @author Gaston Dombiak
482     */
483    public static class Option implements NamedElement {
484
485        public static final String ELEMENT = "option";
486
487        private final String value;
488        private String label;
489
490        public Option(String value) {
491            this.value = value;
492        }
493
494        public Option(String label, String value) {
495            this.label = label;
496            this.value = value;
497        }
498
499        /**
500         * Returns the label that represents the option.
501         *
502         * @return the label that represents the option.
503         */
504        public String getLabel() {
505            return label;
506        }
507
508        /**
509         * Returns the value of the option.
510         *
511         * @return the value of the option.
512         */
513        public String getValue() {
514            return value;
515        }
516
517        @Override
518        public String toString() {
519            return getLabel();
520        }
521
522        @Override
523        public String getElementName() {
524            return ELEMENT;
525        }
526
527        @Override
528        public XmlStringBuilder toXML(String enclosingNamespace) {
529            XmlStringBuilder xml = new XmlStringBuilder(this);
530            // Add attribute
531            xml.optAttribute("label", getLabel());
532            xml.rightAngleBracket();
533
534            // Add element
535            xml.element("value", getValue());
536
537            xml.closeElement(this);
538            return xml;
539        }
540
541        @Override
542        public boolean equals(Object obj) {
543            if (obj == null)
544                return false;
545            if (obj == this)
546                return true;
547            if (obj.getClass() != getClass())
548                return false;
549
550            Option other = (Option) obj;
551
552            if (!value.equals(other.value))
553                return false;
554
555            String thisLabel = label == null ? "" : label;
556            String otherLabel = other.label == null ? "" : other.label;
557
558            if (!thisLabel.equals(otherLabel))
559                return false;
560
561            return true;
562        }
563
564        @Override
565        public int hashCode() {
566            int result = 1;
567            result = 37 * result + value.hashCode();
568            result = 37 * result + (label == null ? 0 : label.hashCode());
569            return result;
570        }
571    }
572}