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