001/*
002 *
003 * Copyright 2003-2007 Jive Software 2020-2022 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 */
017
018package org.jivesoftware.smackx.xdata.provider;
019
020import java.io.IOException;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.List;
024import java.util.Set;
025import java.util.concurrent.CopyOnWriteArraySet;
026import java.util.logging.Logger;
027
028import javax.xml.namespace.QName;
029
030import org.jivesoftware.smack.packet.XmlEnvironment;
031import org.jivesoftware.smack.parsing.SmackParsingException;
032import org.jivesoftware.smack.parsing.SmackParsingException.RequiredValueMissingException;
033import org.jivesoftware.smack.provider.ExtensionElementProvider;
034import org.jivesoftware.smack.roster.packet.RosterPacket;
035import org.jivesoftware.smack.roster.provider.RosterPacketProvider;
036import org.jivesoftware.smack.util.EqualsUtil;
037import org.jivesoftware.smack.util.HashCode;
038import org.jivesoftware.smack.xml.XmlPullParser;
039import org.jivesoftware.smack.xml.XmlPullParserException;
040
041import org.jivesoftware.smackx.formtypes.FormFieldRegistry;
042import org.jivesoftware.smackx.xdata.AbstractMultiFormField;
043import org.jivesoftware.smackx.xdata.AbstractSingleStringValueFormField;
044import org.jivesoftware.smackx.xdata.BooleanFormField;
045import org.jivesoftware.smackx.xdata.FormField;
046import org.jivesoftware.smackx.xdata.FormFieldChildElement;
047import org.jivesoftware.smackx.xdata.FormFieldWithOptions;
048import org.jivesoftware.smackx.xdata.JidMultiFormField;
049import org.jivesoftware.smackx.xdata.JidSingleFormField;
050import org.jivesoftware.smackx.xdata.ListMultiFormField;
051import org.jivesoftware.smackx.xdata.ListSingleFormField;
052import org.jivesoftware.smackx.xdata.TextSingleFormField;
053import org.jivesoftware.smackx.xdata.packet.DataForm;
054import org.jivesoftware.smackx.xdatalayout.packet.DataLayout;
055import org.jivesoftware.smackx.xdatalayout.provider.DataLayoutProvider;
056
057import org.jxmpp.JxmppContext;
058
059/**
060 * The DataFormProvider parses DataForm packets.
061 *
062 * @author Gaston Dombiak
063 */
064public class DataFormProvider extends ExtensionElementProvider<DataForm> {
065
066    private static final Logger LOGGER = Logger.getLogger(DataFormProvider.class.getName());
067
068    public static final DataFormProvider INSTANCE = new DataFormProvider();
069
070    @Override
071    public DataForm parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment, JxmppContext jxmppContext) throws XmlPullParserException, IOException, SmackParsingException {
072        DataForm.Type dataFormType = DataForm.Type.fromString(parser.getAttributeValue("", "type"));
073        DataForm.Builder dataForm = DataForm.builder(dataFormType);
074
075        String formType = null;
076        DataForm.ReportedData reportedData = null;
077
078        outerloop: while (true) {
079            XmlPullParser.Event eventType = parser.next();
080            switch (eventType) {
081            case START_ELEMENT:
082                String name = parser.getName();
083                String namespace = parser.getNamespace();
084                XmlEnvironment elementXmlEnvironment = XmlEnvironment.from(parser, xmlEnvironment);
085                switch (name) {
086                case "instructions":
087                    dataForm.addInstruction(parser.nextText());
088                    break;
089                case "title":
090                    dataForm.setTitle(parser.nextText());
091                    break;
092                case "field":
093                    // Note that we parse this form field without any potential reportedData. We only use reportedData
094                    // to lookup form field types of fields under <item/>.
095                    FormField formField = parseField(parser, elementXmlEnvironment, jxmppContext, formType);
096
097                    TextSingleFormField hiddenFormTypeField = formField.asHiddenFormTypeFieldIfPossible();
098                    if (hiddenFormTypeField != null) {
099                        if (formType != null) {
100                            throw new SmackParsingException("Multiple hidden form type fields");
101                        }
102                        formType = hiddenFormTypeField.getValue();
103                    }
104
105                    dataForm.addField(formField);
106                    break;
107                case "item":
108                    DataForm.Item item = parseItem(parser, elementXmlEnvironment, jxmppContext, formType, reportedData);
109                    dataForm.addItem(item);
110                    break;
111                case "reported":
112                    if (reportedData != null) {
113                        throw new SmackParsingException("Data form with multiple <reported/> elements");
114                    }
115                    reportedData = parseReported(parser, elementXmlEnvironment, jxmppContext, formType);
116                    dataForm.setReportedData(reportedData);
117                    break;
118                // See XEP-133 Example 32 for a corner case where the data form contains this extension.
119                case RosterPacket.ELEMENT:
120                    if (namespace.equals(RosterPacket.NAMESPACE)) {
121                        dataForm.addExtensionElement(RosterPacketProvider.INSTANCE.parse(parser, null));
122                    }
123                    break;
124                // See XEP-141 Data Forms Layout
125                case DataLayout.ELEMENT:
126                    if (namespace.equals(DataLayout.NAMESPACE)) {
127                        dataForm.addExtensionElement(DataLayoutProvider.parse(parser));
128                    }
129                    break;
130                }
131                break;
132            case END_ELEMENT:
133                if (parser.getDepth() == initialDepth) {
134                    break outerloop;
135                }
136                break;
137            default:
138                // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement.
139                break;
140            }
141        }
142        return dataForm.build();
143    }
144
145    private static FormField parseField(XmlPullParser parser, XmlEnvironment xmlEnvironment, JxmppContext jxmppContext, String formType)
146                    throws XmlPullParserException, IOException, SmackParsingException {
147        return parseField(parser, xmlEnvironment, jxmppContext, formType, null);
148    }
149
150    private static final class FieldNameAndFormType {
151        private final String fieldName;
152        private final String formType;
153
154        private FieldNameAndFormType(String fieldName, String formType) {
155            this.fieldName = fieldName;
156            this.formType = formType;
157        }
158
159        private final HashCode.Cache hashCodeCache = new HashCode.Cache();
160
161        @Override
162        public int hashCode() {
163            return hashCodeCache.getHashCode(b ->
164                           b.append(fieldName)
165                            .append(formType)
166                            .build()
167            );
168        }
169
170        @Override
171        public boolean equals(Object other) {
172            return EqualsUtil.equals(this, other, (e, o) ->
173                e.append(fieldName, o.fieldName)
174                 .append(formType, o.formType)
175            );
176        }
177    }
178
179    private static final Set<FieldNameAndFormType> UNKNOWN_FIELDS = new CopyOnWriteArraySet<>();
180
181    private static FormField parseField(XmlPullParser parser, XmlEnvironment xmlEnvironment, JxmppContext jxmppContext, String formType, DataForm.ReportedData reportedData)
182                    throws XmlPullParserException, IOException, SmackParsingException {
183        final int initialDepth = parser.getDepth();
184
185        final String fieldName = parser.getAttributeValue("var");
186        final String label = parser.getAttributeValue("", "label");
187
188        FormField.Type type = null;
189        {
190            String fieldTypeString = parser.getAttributeValue("type");
191            // FormField.Type.fromString() will return null if its input is null.
192            type = FormField.Type.fromString(fieldTypeString);
193        }
194
195        List<FormField.Value> values = new ArrayList<>();
196        List<FormField.Option> options = new ArrayList<>();
197        List<FormFieldChildElement> childElements = new ArrayList<>();
198        boolean required = false;
199
200        outerloop: while (true) {
201            XmlPullParser.TagEvent eventType = parser.nextTag();
202            switch (eventType) {
203            case START_ELEMENT:
204                QName qname = parser.getQName();
205                if (qname.equals(FormField.Value.QNAME)) {
206                    FormField.Value value = parseValue(parser);
207                    values.add(value);
208                } else if (qname.equals(FormField.Option.QNAME)) {
209                    FormField.Option option = parseOption(parser);
210                    options.add(option);
211                } else if (qname.equals(FormField.Required.QNAME)) {
212                    required = true;
213                } else {
214                    FormFieldChildElementProvider<?> provider = FormFieldChildElementProviderManager.getFormFieldChildElementProvider(
215                                    qname);
216                    if (provider == null) {
217                        LOGGER.warning("Unknown form field child element " + qname + " ignored");
218                        continue;
219                    }
220                    FormFieldChildElement formFieldChildElement = provider.parse(parser,
221                                    XmlEnvironment.from(parser, xmlEnvironment), jxmppContext);
222                    childElements.add(formFieldChildElement);
223                }
224                break;
225            case END_ELEMENT:
226                if (parser.getDepth() == initialDepth) {
227                    break outerloop;
228                }
229                break;
230            }
231        }
232
233        // Data forms of type 'result' may contain <reported/> and <item/> elements. If this is the case, then the type
234        // of the <field/>s within the <item/> elements is determined by the information found in <reported/>. See
235        // XEP-0004 § 3.4 and SMACK-902
236        if (type == null && reportedData != null) {
237            FormField reportedFormField = reportedData.getField(fieldName);
238            if (reportedFormField != null) {
239                type = reportedFormField.getType();
240            }
241        }
242
243        if (type != FormField.Type.fixed && fieldName == null) {
244            String typeString = "unspecified";
245            if (type != null) {
246                typeString = type.toString();
247            }
248            throw new RequiredValueMissingException("The data form field of " + typeString + " type has no 'var' attribute, even though one is required as per XEP-0004 § 3.2");
249        }
250
251        if (type == null) {
252            // The field name 'FORM_TYPE' is magic.
253            if (fieldName.equals(FormField.FORM_TYPE)) {
254                type = FormField.Type.hidden;
255            } else {
256                // If no field type was explicitly provided, then we need to lookup the
257                // field's type in the registry.
258                type = FormFieldRegistry.lookup(formType, fieldName);
259                if (type == null) {
260                    FieldNameAndFormType fieldNameAndFormType = new FieldNameAndFormType(fieldName, formType);
261                    if (!UNKNOWN_FIELDS.contains(fieldNameAndFormType)) {
262                        LOGGER.warning("The Field '" + fieldName + "' from FORM_TYPE '" + formType
263                                        + "' is not registered. Field type is unknown, assuming text-single.");
264                        UNKNOWN_FIELDS.add(fieldNameAndFormType);
265                    }
266                    // As per XEP-0004, text-single is the default form field type, which we use as emergency fallback here.
267                    type = FormField.Type.text_single;
268                }
269            }
270        }
271
272        FormField.Builder<?, ?> builder;
273        switch (type) {
274        case bool:
275            builder = parseBooleanFormField(fieldName, values);
276            break;
277        case fixed:
278            builder = parseSingleKindFormField(FormField.fixedBuilder(fieldName), values);
279            break;
280        case hidden:
281            builder = parseSingleKindFormField(FormField.hiddenBuilder(fieldName), values);
282            break;
283        case jid_multi:
284            JidMultiFormField.Builder jidMultiBuilder = FormField.jidMultiBuilder(fieldName);
285            for (FormField.Value value : values) {
286                jidMultiBuilder.addValue(value);
287            }
288            builder = jidMultiBuilder;
289            break;
290        case jid_single:
291            ensureAtMostSingleValue(type, values);
292            JidSingleFormField.Builder jidSingleBuilder = FormField.jidSingleBuilder(fieldName);
293            if (!values.isEmpty()) {
294                FormField.Value value = values.get(0);
295                jidSingleBuilder.setValue(value);
296            }
297            builder = jidSingleBuilder;
298            break;
299        case list_multi:
300            ListMultiFormField.Builder listMultiBuilder = FormField.listMultiBuilder(fieldName);
301            addOptionsToBuilder(options, listMultiBuilder);
302            builder = parseMultiKindFormField(listMultiBuilder, values);
303            break;
304        case list_single:
305            ListSingleFormField.Builder listSingleBuilder = FormField.listSingleBuilder(fieldName);
306            addOptionsToBuilder(options, listSingleBuilder);
307            builder = parseSingleKindFormField(listSingleBuilder, values);
308            break;
309        case text_multi:
310            builder = parseMultiKindFormField(FormField.textMultiBuilder(fieldName), values);
311            break;
312        case text_private:
313            builder = parseSingleKindFormField(FormField.textPrivateBuilder(fieldName), values);
314            break;
315        case text_single:
316            builder = parseSingleKindFormField(FormField.textSingleBuilder(fieldName), values);
317            break;
318        default:
319            // Should never happen, as we cover all types in the switch/case.
320            throw new AssertionError("Unknown type " + type);
321        }
322
323
324        switch (type) {
325        case list_multi:
326        case list_single:
327            break;
328        default:
329            if (!options.isEmpty()) {
330                throw new SmackParsingException("Form fields of type " + type + " must not have options. This one had "
331                                + options.size());
332            }
333            break;
334        }
335
336        if (label != null) {
337            builder.setLabel(label);
338        }
339        builder.setRequired(required);
340        builder.addFormFieldChildElements(childElements);
341
342        return builder.build();
343    }
344
345    private static FormField.Builder<?, ?> parseBooleanFormField(String fieldName, List<FormField.Value> values) throws SmackParsingException {
346        BooleanFormField.Builder builder = FormField.booleanBuilder(fieldName);
347        ensureAtMostSingleValue(builder.getType(), values);
348        if (values.size() == 1) {
349            FormField.Value value = values.get(0);
350            builder.setValue(value);
351        }
352        return builder;
353    }
354
355    private static AbstractSingleStringValueFormField.Builder<?, ?> parseSingleKindFormField(
356                    AbstractSingleStringValueFormField.Builder<?, ?> builder, List<FormField.Value> values)
357                    throws SmackParsingException {
358        ensureAtMostSingleValue(builder.getType(), values);
359        if (values.size() == 1) {
360            String value = values.get(0).getValue().toString();
361            builder.setValue(value);
362        }
363        return builder;
364    }
365
366    private static AbstractMultiFormField.Builder<?, ?> parseMultiKindFormField(AbstractMultiFormField.Builder<?, ?> builder,
367                    List<FormField.Value> values) {
368        for (FormField.Value value : values) {
369            String rawValue = value.getValue().toString();
370            builder.addValue(rawValue);
371        }
372        return builder;
373    }
374
375    private static DataForm.Item parseItem(XmlPullParser parser, XmlEnvironment xmlEnvironment, JxmppContext jxmppContext, String formType,
376                    DataForm.ReportedData reportedData)
377                    throws XmlPullParserException, IOException, SmackParsingException {
378        final int initialDepth = parser.getDepth();
379        List<FormField> fields = new ArrayList<>();
380        outerloop: while (true) {
381            XmlPullParser.TagEvent eventType = parser.nextTag();
382            switch (eventType) {
383            case START_ELEMENT:
384                String name = parser.getName();
385                switch (name) {
386                case "field":
387                    FormField field = parseField(parser, XmlEnvironment.from(parser, xmlEnvironment), jxmppContext, formType,
388                                    reportedData);
389                    fields.add(field);
390                    break;
391                }
392                break;
393            case END_ELEMENT:
394                if (parser.getDepth() == initialDepth) {
395                    break outerloop;
396                }
397                break;
398            }
399        }
400        return new DataForm.Item(fields);
401    }
402
403    private static DataForm.ReportedData parseReported(XmlPullParser parser, XmlEnvironment xmlEnvironment, JxmppContext jxmppContext, String formType)
404                    throws XmlPullParserException, IOException, SmackParsingException {
405        final int initialDepth = parser.getDepth();
406        List<FormField> fields = new ArrayList<>();
407        outerloop: while (true) {
408            XmlPullParser.TagEvent eventType = parser.nextTag();
409            switch (eventType) {
410            case START_ELEMENT:
411                String name = parser.getName();
412                switch (name) {
413                case "field":
414                    FormField field = parseField(parser, XmlEnvironment.from(parser, xmlEnvironment), jxmppContext, formType);
415                    fields.add(field);
416                    break;
417                }
418                break;
419            case END_ELEMENT:
420                if (parser.getDepth() == initialDepth) {
421                    break outerloop;
422                }
423                break;
424            }
425        }
426        return new DataForm.ReportedData(fields);
427    }
428
429    public static FormField.Value parseValue(XmlPullParser parser) throws IOException, XmlPullParserException {
430        String value = parser.nextText();
431        return new FormField.Value(value);
432    }
433
434    public static FormField.Option parseOption(XmlPullParser parser) throws IOException, XmlPullParserException {
435        int initialDepth = parser.getDepth();
436        FormField.Option option = null;
437        String label = parser.getAttributeValue("", "label");
438        outerloop: while (true) {
439            XmlPullParser.TagEvent eventType = parser.nextTag();
440            switch (eventType) {
441            case START_ELEMENT:
442                String name = parser.getName();
443                switch (name) {
444                case "value":
445                    option = new FormField.Option(label, parser.nextText());
446                    break;
447                }
448                break;
449            case END_ELEMENT:
450                if (parser.getDepth() == initialDepth) {
451                    break outerloop;
452                }
453                break;
454            }
455        }
456        return option;
457    }
458
459    private static void ensureAtMostSingleValue(FormField.Type type, List<FormField.Value> values) throws SmackParsingException {
460        if (values.size() > 1) {
461            throw new SmackParsingException(type + " fields can have at most one value, this one had " + values.size());
462        }
463    }
464
465    private static void addOptionsToBuilder(Collection<FormField.Option> options, FormFieldWithOptions.Builder<?> builder) {
466        for (FormField.Option option : options) {
467            builder.addOption(option);
468        }
469    }
470}