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