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