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