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 */
017package org.jivesoftware.smackx.xdata;
018
019import java.util.ArrayList;
020import java.util.Iterator;
021import java.util.List;
022import java.util.StringTokenizer;
023
024import org.jivesoftware.smack.packet.Stanza;
025
026import org.jivesoftware.smackx.xdata.packet.DataForm;
027
028/**
029 * Represents a Form for gathering data. The form could be of the following types:
030 * <ul>
031 *  <li>form &rarr; Indicates a form to fill out.</li>
032 *  <li>submit &rarr; The form is filled out, and this is the data that is being returned from 
033 * the form.</li>
034 *  <li>cancel &rarr; The form was cancelled. Tell the asker that piece of information.</li>
035 *  <li>result &rarr; Data results being returned from a search, or some other query.</li>
036 * </ul>
037 * 
038 * Depending of the form's type different operations are available. For example, it's only possible
039 * to set answers if the form is of type "submit".
040 * 
041 * @see <a href="http://xmpp.org/extensions/xep-0004.html">XEP-0004 Data Forms</a>
042 * 
043 * @author Gaston Dombiak
044 */
045public class Form {
046
047    private DataForm dataForm;
048
049    /**
050     * Returns a new ReportedData if the stanza(/packet) is used for gathering data and includes an 
051     * extension that matches the elementName and namespace "x","jabber:x:data".  
052     * 
053     * @param packet the stanza(/packet) used for gathering data.
054     * @return the data form parsed from the stanza(/packet) or <tt>null</tt> if there was not
055     *      a form in the packet.
056     */
057    public static Form getFormFrom(Stanza packet) {
058        // Check if the packet includes the DataForm extension
059        DataForm dataForm = DataForm.from(packet);
060        if (dataForm != null) {
061            if (dataForm.getReportedData() == null)
062                return new Form(dataForm);
063        }
064        // Otherwise return null
065        return null;
066    }
067
068    /**
069     * Creates a new Form that will wrap an existing DataForm. The wrapped DataForm must be
070     * used for gathering data. 
071     * 
072     * @param dataForm the data form used for gathering data. 
073     */
074    public Form(DataForm dataForm) {
075        this.dataForm = dataForm;
076    }
077
078    /**
079     * Creates a new Form of a given type from scratch.
080     *
081     * @param type the form's type (e.g. form, submit,cancel,result).
082     */
083    public Form(DataForm.Type type) {
084        this.dataForm = new DataForm(type);
085    }
086
087    /**
088     * Adds a new field to complete as part of the form.
089     * 
090     * @param field the field to complete.
091     */
092    public void addField(FormField field) {
093        dataForm.addField(field);
094    }
095
096    /**
097     * Sets a new String value to a given form's field. The field whose variable matches the 
098     * requested variable will be completed with the specified value. If no field could be found 
099     * for the specified variable then an exception will be raised.<p>
100     * 
101     * If the value to set to the field is not a basic type (e.g. String, boolean, int, etc.) you
102     * can use this message where the String value is the String representation of the object. 
103     * 
104     * @param variable the variable name that was completed.
105     * @param value the String value that was answered.
106     * @throws IllegalStateException if the form is not of type "submit".
107     * @throws IllegalArgumentException if the form does not include the specified variable or
108     *      if the answer type does not correspond with the field type..
109     */
110    public void setAnswer(String variable, String value) {
111        FormField field = getField(variable);
112        if (field == null) {
113            throw new IllegalArgumentException("Field not found for the specified variable name.");
114        }
115        switch (field.getType()) {
116        case text_multi:
117        case text_private:
118        case text_single:
119        case jid_single:
120        case hidden:
121            break;
122        default:
123            throw new IllegalArgumentException("This field is not of type String.");
124        }
125        setAnswer(field, value);
126    }
127
128    /**
129     * Sets a new int value to a given form's field. The field whose variable matches the 
130     * requested variable will be completed with the specified value. If no field could be found 
131     * for the specified variable then an exception will be raised.
132     * 
133     * @param variable the variable name that was completed.
134     * @param value the int value that was answered.
135     * @throws IllegalStateException if the form is not of type "submit".
136     * @throws IllegalArgumentException if the form does not include the specified variable or
137     *      if the answer type does not correspond with the field type.
138     */
139    public void setAnswer(String variable, int value) {
140        FormField field = getField(variable);
141        if (field == null) {
142            throw new IllegalArgumentException("Field not found for the specified variable name.");
143        }
144        validateThatFieldIsText(field);
145        setAnswer(field, value);
146    }
147
148    /**
149     * Sets a new long value to a given form's field. The field whose variable matches the 
150     * requested variable will be completed with the specified value. If no field could be found 
151     * for the specified variable then an exception will be raised.
152     * 
153     * @param variable the variable name that was completed.
154     * @param value the long value that was answered.
155     * @throws IllegalStateException if the form is not of type "submit".
156     * @throws IllegalArgumentException if the form does not include the specified variable or
157     *      if the answer type does not correspond with the field type.
158     */
159    public void setAnswer(String variable, long value) {
160        FormField field = getField(variable);
161        if (field == null) {
162            throw new IllegalArgumentException("Field not found for the specified variable name.");
163        }
164        validateThatFieldIsText(field);
165        setAnswer(field, value);
166    }
167
168    /**
169     * Sets a new float value to a given form's field. The field whose variable matches the 
170     * requested variable will be completed with the specified value. If no field could be found 
171     * for the specified variable then an exception will be raised.
172     * 
173     * @param variable the variable name that was completed.
174     * @param value the float value that was answered.
175     * @throws IllegalStateException if the form is not of type "submit".
176     * @throws IllegalArgumentException if the form does not include the specified variable or
177     *      if the answer type does not correspond with the field type.
178     */
179    public void setAnswer(String variable, float value) {
180        FormField field = getField(variable);
181        if (field == null) {
182            throw new IllegalArgumentException("Field not found for the specified variable name.");
183        }
184        validateThatFieldIsText(field);
185        setAnswer(field, value);
186    }
187
188    /**
189     * Sets a new double value to a given form's field. The field whose variable matches the 
190     * requested variable will be completed with the specified value. If no field could be found 
191     * for the specified variable then an exception will be raised.
192     * 
193     * @param variable the variable name that was completed.
194     * @param value the double value that was answered.
195     * @throws IllegalStateException if the form is not of type "submit".
196     * @throws IllegalArgumentException if the form does not include the specified variable or
197     *      if the answer type does not correspond with the field type.
198     */
199    public void setAnswer(String variable, double value) {
200        FormField field = getField(variable);
201        if (field == null) {
202            throw new IllegalArgumentException("Field not found for the specified variable name.");
203        }
204        validateThatFieldIsText(field);
205        setAnswer(field, value);
206    }
207
208    private static void validateThatFieldIsText(FormField field) {
209        switch (field.getType()) {
210        case text_multi:
211        case text_private:
212        case text_single:
213            break;
214        default:
215            throw new IllegalArgumentException("This field is not of type text (multi, private or single).");
216        }
217    }
218
219    /**
220     * Sets a new boolean value to a given form's field. The field whose variable matches the 
221     * requested variable will be completed with the specified value. If no field could be found 
222     * for the specified variable then an exception will be raised.
223     * 
224     * @param variable the variable name that was completed.
225     * @param value the boolean value that was answered.
226     * @throws IllegalStateException if the form is not of type "submit".
227     * @throws IllegalArgumentException if the form does not include the specified variable or
228     *      if the answer type does not correspond with the field type.
229     */
230    public void setAnswer(String variable, boolean value) {
231        FormField field = getField(variable);
232        if (field == null) {
233            throw new IllegalArgumentException("Field not found for the specified variable name.");
234        }
235        if (field.getType() != FormField.Type.bool) {
236            throw new IllegalArgumentException("This field is not of type boolean.");
237        }
238        setAnswer(field, (value ? "1" : "0"));
239    }
240
241    /**
242     * Sets a new Object value to a given form's field. In fact, the object representation 
243     * (i.e. #toString) will be the actual value of the field.<p>
244     * 
245     * If the value to set to the field is not a basic type (e.g. String, boolean, int, etc.) you
246     * will need to use {@link #setAnswer(String, String)} where the String value is the 
247     * String representation of the object.<p> 
248     * 
249     * Before setting the new value to the field we will check if the form is of type submit. If 
250     * the form isn't of type submit means that it's not possible to complete the form and an   
251     * exception will be thrown.
252     * 
253     * @param field the form field that was completed.
254     * @param value the Object value that was answered. The object representation will be the 
255     * actual value.
256     * @throws IllegalStateException if the form is not of type "submit".
257     */
258    private void setAnswer(FormField field, Object value) {
259        if (!isSubmitType()) {
260            throw new IllegalStateException("Cannot set an answer if the form is not of type " +
261            "\"submit\"");
262        }
263        field.resetValues();
264        field.addValue(value.toString());
265    }
266
267    /**
268     * Sets a new values to a given form's field. The field whose variable matches the requested 
269     * variable will be completed with the specified values. If no field could be found for 
270     * the specified variable then an exception will be raised.<p>
271     * 
272     * The Objects contained in the List could be of any type. The String representation of them
273     * (i.e. #toString) will be actually used when sending the answer to the server.
274     * 
275     * @param variable the variable that was completed.
276     * @param values the values that were answered.
277     * @throws IllegalStateException if the form is not of type "submit".
278     * @throws IllegalArgumentException if the form does not include the specified variable.
279     */
280    public void setAnswer(String variable, List<String> values) {
281        if (!isSubmitType()) {
282            throw new IllegalStateException("Cannot set an answer if the form is not of type " +
283            "\"submit\"");
284        }
285        FormField field = getField(variable);
286        if (field != null) {
287            // Check that the field can accept a collection of values
288            switch (field.getType()) {
289            case jid_multi:
290            case list_multi:
291            case list_single:
292            case text_multi:
293            case hidden:
294                break;
295            default:
296                throw new IllegalArgumentException("This field only accept list of values.");
297            }
298            // Clear the old values 
299            field.resetValues();
300            // Set the new values. The string representation of each value will be actually used.
301            field.addValues(values);
302        }
303        else {
304            throw new IllegalArgumentException("Couldn't find a field for the specified variable.");
305        }
306    }
307
308    /**
309     * Sets the default value as the value of a given form's field. The field whose variable matches
310     * the requested variable will be completed with its default value. If no field could be found
311     * for the specified variable then an exception will be raised.
312     *
313     * @param variable the variable to complete with its default value.
314     * @throws IllegalStateException if the form is not of type "submit".
315     * @throws IllegalArgumentException if the form does not include the specified variable.
316     */
317    public void setDefaultAnswer(String variable) {
318        if (!isSubmitType()) {
319            throw new IllegalStateException("Cannot set an answer if the form is not of type " +
320            "\"submit\"");
321        }
322        FormField field = getField(variable);
323        if (field != null) {
324            // Clear the old values
325            field.resetValues();
326            // Set the default value
327            for (String value : field.getValues()) {
328                field.addValue(value);
329            }
330        }
331        else {
332            throw new IllegalArgumentException("Couldn't find a field for the specified variable.");
333        }
334    }
335
336    /**
337     * Returns a List of the fields that are part of the form.
338     *
339     * @return a List of the fields that are part of the form.
340     */
341    public List<FormField> getFields() {
342        return dataForm.getFields();
343    }
344
345    /**
346     * Returns the field of the form whose variable matches the specified variable.
347     * The fields of type FIXED will never be returned since they do not specify a 
348     * variable. 
349     * 
350     * @param variable the variable to look for in the form fields. 
351     * @return the field of the form whose variable matches the specified variable.
352     */
353    public FormField getField(String variable) {
354        return dataForm.getField(variable);
355    }
356
357    /**
358     * Check if a field with the given variable exists.
359     *
360     * @param variable the variable to check for.
361     * @return true if a field with the variable exists, false otherwise.
362     * @since 4.2
363     */
364    public boolean hasField(String variable) {
365        return dataForm.hasField(variable);
366    }
367
368    /**
369     * Returns the instructions that explain how to fill out the form and what the form is about.
370     * 
371     * @return instructions that explain how to fill out the form.
372     */
373    public String getInstructions() {
374        StringBuilder sb = new StringBuilder();
375        // Join the list of instructions together separated by newlines
376        for (Iterator<String> it = dataForm.getInstructions().iterator(); it.hasNext();) {
377            sb.append(it.next());
378            // If this is not the last instruction then append a newline
379            if (it.hasNext()) {
380                sb.append('\n');
381            }
382        }
383        return sb.toString();
384    }
385
386
387    /**
388     * Returns the description of the data. It is similar to the title on a web page or an X 
389     * window.  You can put a title on either a form to fill out, or a set of data results.
390     * 
391     * @return description of the data.
392     */
393    public String getTitle() {
394        return dataForm.getTitle();
395    }
396
397
398    /**
399     * Returns the meaning of the data within the context. The data could be part of a form
400     * to fill out, a form submission or data results.
401     * 
402     * @return the form's type.
403     */
404    public DataForm.Type getType() {
405        return dataForm.getType(); 
406    }
407
408
409    /**
410     * Sets instructions that explain how to fill out the form and what the form is about.
411     * 
412     * @param instructions instructions that explain how to fill out the form.
413     */
414    public void setInstructions(String instructions) {
415        // Split the instructions into multiple instructions for each existent newline
416        ArrayList<String> instructionsList = new ArrayList<>();
417        StringTokenizer st = new StringTokenizer(instructions, "\n");
418        while (st.hasMoreTokens()) {
419            instructionsList.add(st.nextToken());
420        }
421        // Set the new list of instructions
422        dataForm.setInstructions(instructionsList);
423
424    }
425
426
427    /**
428     * Sets the description of the data. It is similar to the title on a web page or an X window.
429     * You can put a title on either a form to fill out, or a set of data results.
430     * 
431     * @param title description of the data.
432     */
433    public void setTitle(String title) {
434        dataForm.setTitle(title);
435    }
436
437    /**
438     * Returns a DataForm that serves to send this Form to the server. If the form is of type 
439     * submit, it may contain fields with no value. These fields will be removed since they only 
440     * exist to assist the user while editing/completing the form in a UI. 
441     * 
442     * @return the wrapped DataForm.
443     */
444    public DataForm getDataFormToSend() {
445        if (isSubmitType()) {
446            // Create a new DataForm that contains only the answered fields 
447            DataForm dataFormToSend = new DataForm(getType());
448            for (FormField field : getFields()) {
449                if (!field.getValues().isEmpty()) {
450                    dataFormToSend.addField(field);
451                }
452            }
453            return dataFormToSend;
454        }
455        return dataForm;
456    }
457
458    /**
459     * Returns true if the form is a form to fill out.
460     * 
461     * @return if the form is a form to fill out.
462     */
463    private boolean isFormType() {
464        return DataForm.Type.form == dataForm.getType();
465    }
466
467    /**
468     * Returns true if the form is a form to submit.
469     * 
470     * @return if the form is a form to submit.
471     */
472    private boolean isSubmitType() {
473        return DataForm.Type.submit == dataForm.getType();
474    }
475
476    /**
477     * Returns a new Form to submit the completed values. The new Form will include all the fields
478     * of the original form except for the fields of type FIXED. Only the HIDDEN fields will 
479     * include the same value of the original form. The other fields of the new form MUST be 
480     * completed. If a field remains with no answer when sending the completed form, then it won't 
481     * be included as part of the completed form.<p>
482     * 
483     * The reason why the fields with variables are included in the new form is to provide a model 
484     * for binding with any UI. This means that the UIs will use the original form (of type 
485     * "form") to learn how to render the form, but the UIs will bind the fields to the form of
486     * type submit.
487     * 
488     * @return a Form to submit the completed values.
489     */
490    public Form createAnswerForm() {
491        if (!isFormType()) {
492            throw new IllegalStateException("Only forms of type \"form\" could be answered");
493        }
494        // Create a new Form
495        Form form = new Form(DataForm.Type.submit);
496        for (FormField field : getFields()) {
497            // Add to the new form any type of field that includes a variable.
498            // Note: The fields of type FIXED are the only ones that don't specify a variable
499            if (field.getVariable() != null) {
500                FormField newField = new FormField(field.getVariable());
501                newField.setType(field.getType());
502                form.addField(newField);
503                // Set the answer ONLY to the hidden fields 
504                if (field.getType() == FormField.Type.hidden) {
505                    // Since a hidden field could have many values we need to collect them 
506                    // in a list
507                    List<String> values = new ArrayList<>();
508                    values.addAll(field.getValues());
509                    form.setAnswer(field.getVariable(), values);
510                }
511            }
512        }
513        return form;
514    }
515
516}