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