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