001/**
002 *
003 * Copyright 2020-2021 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.formtypes;
018
019import java.util.HashMap;
020import java.util.Map;
021import java.util.concurrent.ConcurrentHashMap;
022import java.util.logging.Logger;
023
024import org.jivesoftware.smack.util.Objects;
025import org.jivesoftware.smack.util.StringUtils;
026import org.jivesoftware.smack.util.XmlUtil;
027
028import org.jivesoftware.smackx.xdata.FormField;
029import org.jivesoftware.smackx.xdata.TextSingleFormField;
030import org.jivesoftware.smackx.xdata.packet.DataForm;
031
032public class FormFieldRegistry {
033
034    private static final Logger LOGGER = Logger.getLogger(FormFieldRegistry.class.getName());
035
036    private static final Map<String, Map<String, FormField.Type>> REGISTRY = new HashMap<>();
037
038    private static final Map<String, FormField.Type> CLARK_NOTATION_FIELD_REGISTRY = new ConcurrentHashMap<>();
039
040    private static final Map<String, FormField.Type> LOOKASIDE_FIELD_REGISTRY = new ConcurrentHashMap<>();
041
042    @SuppressWarnings("ReferenceEquality")
043    public static void register(DataForm dataForm) {
044        // TODO: Also allow forms of type 'result'?
045        if (dataForm.getType() != DataForm.Type.form) {
046            throw new IllegalArgumentException();
047        }
048
049        String formType = null;
050        TextSingleFormField hiddenFormTypeField = dataForm.getHiddenFormTypeField();
051        if (hiddenFormTypeField != null) {
052            formType = hiddenFormTypeField.getValue();
053        }
054
055        for (FormField formField : dataForm.getFields()) {
056            // Note that we can compare here by reference equality to skip the hidden form type field.
057            if (formField == hiddenFormTypeField) {
058                continue;
059            }
060
061            FormField.Type type = formField.getType();
062            if (type == FormField.Type.fixed) {
063                continue;
064            }
065
066            String fieldName = formField.getFieldName();
067            register(formType, fieldName, type);
068        }
069    }
070
071    public static void register(String formType, FormField.Type fieldType, String... fieldNames) {
072        for (String fieldName : fieldNames) {
073            register(formType, fieldName, fieldType);
074        }
075    }
076
077    public static void register(String formType, String fieldName, FormField.Type fieldType) {
078        StringUtils.requireNotNullNorEmpty(fieldName, "fieldName must be provided");
079        Objects.requireNonNull(fieldType);
080
081        if (formType == null) {
082            if (XmlUtil.isClarkNotation(fieldName)) {
083                CLARK_NOTATION_FIELD_REGISTRY.put(fieldName, fieldType);
084            }
085            return;
086        }
087
088        FormField.Type previousType;
089        synchronized (REGISTRY) {
090            Map<String, FormField.Type> fieldNameToType = REGISTRY.get(formType);
091            if (fieldNameToType == null) {
092                fieldNameToType = new HashMap<>();
093                REGISTRY.put(formType, fieldNameToType);
094            } else {
095                previousType = fieldNameToType.get(fieldName);
096                if (previousType != null && previousType != fieldType) {
097                    String message = "The field '" + fieldName + "' from form type '" + formType
098                                    + "' was already registered with field type '" + previousType
099                                    + "' while it is now seen with type '" + fieldType
100                                    + "'. Form field types have to be unambigiously."
101                                    + " XMPP uses a registry for form field tpes, scoped by the form type."
102                                    + " You may find the correct value at https://xmpp.org/registrar/formtypes.html";
103                    throw new IllegalArgumentException(message);
104                }
105            }
106            previousType = fieldNameToType.put(fieldName, fieldType);
107        }
108        if (previousType != null && fieldType != previousType) {
109            LOGGER.warning("Form field registry inconsitency detected: Registered field '" + fieldName + "' of type " + fieldType + " but previous type was " + previousType);
110        }
111
112    }
113
114    public static FormField.Type lookup(String formType, String fieldName) {
115        if (formType == null) {
116            if (XmlUtil.isClarkNotation(fieldName)) {
117                return CLARK_NOTATION_FIELD_REGISTRY.get(fieldName);
118            }
119
120            return LOOKASIDE_FIELD_REGISTRY.get(fieldName);
121        }
122
123        synchronized (REGISTRY) {
124            Map<String, FormField.Type> fieldNameToTypeMap = REGISTRY.get(formType);
125            if (fieldNameToTypeMap != null) {
126                FormField.Type type = fieldNameToTypeMap.get(fieldName);
127                if (type != null) {
128                    return type;
129                }
130            }
131        }
132
133        return null;
134    }
135
136    public static synchronized FormField.Type lookup(String fieldName) {
137        return lookup(null, fieldName);
138    }
139
140    public static void addLookasideFieldRegistryEntry(String fieldName, FormField.Type formFieldType) {
141        LOOKASIDE_FIELD_REGISTRY.put(fieldName, formFieldType);
142    }
143}