001/**
002 *
003 * Copyright 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 */
017package org.jivesoftware.smackx.xdata.form;
018
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.Date;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028
029import org.jivesoftware.smackx.xdata.AbstractMultiFormField;
030import org.jivesoftware.smackx.xdata.AbstractSingleStringValueFormField;
031import org.jivesoftware.smackx.xdata.FormField;
032import org.jivesoftware.smackx.xdata.FormField.Type;
033import org.jivesoftware.smackx.xdata.FormFieldChildElement;
034import org.jivesoftware.smackx.xdata.packet.DataForm;
035
036import org.jxmpp.jid.Jid;
037import org.jxmpp.jid.impl.JidCreate;
038import org.jxmpp.jid.util.JidUtil;
039import org.jxmpp.stringprep.XmppStringprepException;
040import org.jxmpp.util.XmppDateTime;
041
042public class FillableForm extends FilledForm {
043
044    private final Set<String> requiredFields;
045
046    private final Set<String> filledRequiredFields = new HashSet<>();
047    private final Set<String> missingRequiredFields = new HashSet<>();
048
049    private final Map<String, FormField> filledFields = new HashMap<>();
050
051    public FillableForm(DataForm dataForm) {
052        super(dataForm);
053        if (dataForm.getType() != DataForm.Type.form) {
054            throw new IllegalArgumentException();
055        }
056
057        Set<String> requiredFields = new HashSet<>();
058        for (FormField formField : dataForm.getFields()) {
059            if (formField.isRequired()) {
060                String fieldName = formField.getFieldName();
061                requiredFields.add(fieldName);
062
063                if (formField.hasValueSet()) {
064                    // This is a form field with a default value.
065                    write(formField);
066                } else {
067                    missingRequiredFields.add(fieldName);
068                }
069            }
070        }
071        this.requiredFields = Collections.unmodifiableSet(requiredFields);
072    }
073
074    protected void writeListMulti(String fieldName, List<? extends CharSequence> values) {
075        FormField formField = FormField.listMultiBuilder(fieldName)
076                        .addValues(values)
077                        .build();
078        write(formField);
079    }
080
081    protected void writeTextSingle(String fieldName, CharSequence value) {
082        FormField formField = FormField.textSingleBuilder(fieldName)
083                        .setValue(value)
084                        .build();
085        write(formField);
086    }
087
088    protected void writeBoolean(String fieldName, boolean value) {
089        FormField formField = FormField.booleanBuilder(fieldName)
090                        .setValue(value)
091                        .build();
092        write(formField);
093    }
094
095    protected void write(String fieldName, int value) {
096        writeTextSingle(fieldName, Integer.toString(value));
097    }
098
099    protected void write(String fieldName, Date date) {
100        writeTextSingle(fieldName, XmppDateTime.formatXEP0082Date(date));
101    }
102
103    public void setAnswer(String fieldName, Collection<? extends CharSequence> answers) {
104        FormField blankField = getFieldOrThrow(fieldName);
105        FormField.Type type = blankField.getType();
106
107        FormField filledFormField;
108        switch (type) {
109        case list_multi:
110        case text_multi:
111            filledFormField = createMultiKindFieldbuilder(fieldName, type)
112                .addValues(answers)
113                .build();
114            break;
115        case jid_multi:
116            List<Jid> jids = new ArrayList<>(answers.size());
117            List<XmppStringprepException> exceptions = new ArrayList<>();
118            JidUtil.jidsFrom(answers, jids, exceptions);
119            if (!exceptions.isEmpty()) {
120                // TODO: Report all exceptions here.
121                throw new IllegalArgumentException(exceptions.get(0));
122            }
123            filledFormField = FormField.jidMultiBuilder(fieldName)
124                            .addValues(jids)
125                            .build();
126            break;
127        default:
128            throw new IllegalArgumentException("");
129        }
130        write(filledFormField);
131    }
132
133    private static AbstractMultiFormField.Builder<?, ?> createMultiKindFieldbuilder(String fieldName, FormField.Type type) {
134        switch (type) {
135        case list_multi:
136            return FormField.listMultiBuilder(fieldName);
137        case text_multi:
138            return FormField.textMultiBuilder(fieldName);
139        default:
140            throw new IllegalArgumentException();
141        }
142    }
143
144    public void setAnswer(String fieldName, int answer) {
145        setAnswer(fieldName, Integer.toString(answer));
146    }
147
148    public void setAnswer(String fieldName, CharSequence answer) {
149        FormField blankField = getFieldOrThrow(fieldName);
150        FormField.Type type = blankField.getType();
151
152        FormField filledFormField;
153        switch (type) {
154        case list_multi:
155        case jid_multi:
156            throw new IllegalArgumentException("Can not answer fields of type '" + type + "' with a CharSequence");
157        case fixed:
158            throw new IllegalArgumentException("Fields of type 'fixed' are not answerable");
159        case list_single:
160        case text_private:
161        case text_single:
162        case hidden:
163            filledFormField = createSingleKindFieldBuilder(fieldName, type)
164                .setValue(answer)
165                .build();
166            break;
167        case bool:
168            filledFormField = FormField.booleanBuilder(fieldName)
169                .setValue(answer)
170                .build();
171            break;
172        case jid_single:
173            Jid jid;
174            try {
175                jid = JidCreate.from(answer);
176            } catch (XmppStringprepException e) {
177                throw new IllegalArgumentException(e);
178            }
179            filledFormField = FormField.jidSingleBuilder(fieldName)
180                .setValue(jid)
181                .build();
182            break;
183        case text_multi:
184            filledFormField = createMultiKindFieldbuilder(fieldName, type)
185                .addValue(answer)
186                .build();
187            break;
188        default:
189            throw new AssertionError();
190        }
191        write(filledFormField);
192    }
193
194    private static AbstractSingleStringValueFormField.Builder<?, ?> createSingleKindFieldBuilder(String fieldName, FormField.Type type) {
195        switch (type) {
196        case text_private:
197            return FormField.textPrivateBuilder(fieldName);
198        case text_single:
199            return FormField.textSingleBuilder(fieldName);
200        case hidden:
201            return FormField.hiddenBuilder(fieldName);
202        case list_single:
203            return FormField.listSingleBuilder(fieldName);
204        default:
205            throw new IllegalArgumentException("Unsupported type: " + type);
206        }
207    }
208
209    public void setAnswer(String fieldName, boolean answer) {
210        FormField blankField = getFieldOrThrow(fieldName);
211        if (blankField.getType() != Type.bool) {
212            throw new IllegalArgumentException();
213        }
214
215        FormField filledFormField = FormField.booleanBuilder(fieldName)
216                        .setValue(answer)
217                        .build();
218        write(filledFormField);
219    }
220
221    public final void write(FormField filledFormField) {
222        if (filledFormField.getType() == FormField.Type.fixed) {
223            throw new IllegalArgumentException();
224        }
225        if (!filledFormField.hasValueSet()) {
226            throw new IllegalArgumentException();
227        }
228
229        String fieldName = filledFormField.getFieldName();
230        if (!getDataForm().hasField(fieldName)) {
231            throw new IllegalArgumentException();
232        }
233
234        // Perform validation, e.g. using XEP-0122.
235        // TODO: We could also perform list-* option validation, but this has to take xep122's <open/> into account.
236        FormField formFieldPrototype = getDataForm().getField(fieldName);
237        for (FormFieldChildElement formFieldChildelement : formFieldPrototype.getFormFieldChildElements()) {
238            formFieldChildelement.validate(filledFormField);
239        }
240
241        filledFields.put(fieldName, filledFormField);
242        if (requiredFields.contains(fieldName)) {
243            filledRequiredFields.add(fieldName);
244            missingRequiredFields.remove(fieldName);
245        }
246    }
247
248    @Override
249    public FormField getField(String fieldName) {
250        FormField filledField = filledFields.get(fieldName);
251        if (filledField != null) {
252            return filledField;
253        }
254
255        return super.getField(fieldName);
256    }
257
258    public DataForm getDataFormToSubmit() {
259        if (!missingRequiredFields.isEmpty()) {
260            throw new IllegalStateException("Not all required fields filled. Missing: " + missingRequiredFields);
261        }
262        DataForm.Builder builder = DataForm.builder();
263
264        // the submit form has the same FORM_TYPE as the form.
265        if (formTypeFormField != null) {
266            builder.addField(formTypeFormField);
267        }
268
269        builder.addFields(filledFields.values());
270
271        return builder.build();
272    }
273
274}