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