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