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