001/*
002 * (C) Copyright 2015-2020 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Nicolas Chapurlat <nchapurlat@nuxeo.com>
018 */
019package org.nuxeo.ecm.core.io.marshallers.json.document;
020
021import static org.nuxeo.ecm.core.io.registry.reflect.Instantiations.SINGLETON;
022import static org.nuxeo.ecm.core.io.registry.reflect.Priorities.REFERENCE;
023
024import java.io.IOException;
025import java.lang.reflect.Array;
026import java.util.ArrayList;
027import java.util.Calendar;
028import java.util.Iterator;
029import java.util.List;
030import java.util.Map.Entry;
031
032import javax.inject.Inject;
033
034import org.apache.logging.log4j.LogManager;
035import org.apache.logging.log4j.Logger;
036import org.nuxeo.ecm.core.api.Blob;
037import org.nuxeo.ecm.core.api.model.Property;
038import org.nuxeo.ecm.core.api.model.impl.ArrayProperty;
039import org.nuxeo.ecm.core.api.model.impl.ComplexProperty;
040import org.nuxeo.ecm.core.api.model.impl.DocumentPartImpl;
041import org.nuxeo.ecm.core.api.model.impl.PropertyFactory;
042import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty;
043import org.nuxeo.ecm.core.io.marshallers.json.AbstractJsonReader;
044import org.nuxeo.ecm.core.io.registry.MarshallingException;
045import org.nuxeo.ecm.core.io.registry.reflect.Setup;
046import org.nuxeo.ecm.core.schema.SchemaManager;
047import org.nuxeo.ecm.core.schema.types.ComplexType;
048import org.nuxeo.ecm.core.schema.types.Field;
049import org.nuxeo.ecm.core.schema.types.ListType;
050import org.nuxeo.ecm.core.schema.types.Schema;
051import org.nuxeo.ecm.core.schema.types.SimpleType;
052import org.nuxeo.ecm.core.schema.types.Type;
053import org.nuxeo.ecm.core.schema.types.primitives.BinaryType;
054import org.nuxeo.ecm.core.schema.types.primitives.BooleanType;
055import org.nuxeo.ecm.core.schema.types.primitives.DateType;
056import org.nuxeo.ecm.core.schema.types.primitives.DoubleType;
057import org.nuxeo.ecm.core.schema.types.primitives.IntegerType;
058import org.nuxeo.ecm.core.schema.types.primitives.LongType;
059import org.nuxeo.ecm.core.schema.types.primitives.StringType;
060import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolver;
061
062import com.fasterxml.jackson.databind.JsonNode;
063
064/**
065 * Convert Json as {@link List}&lt;{@link Property}&gt;.
066 * <p>
067 * Format is:
068 *
069 * <pre>
070 * {
071 *   "schema1Prefix:stringProperty": "stringPropertyValue", &lt;-- each property may be marshall as object if a resolver is associated with that property and if a marshaller exists for the object, in this case, the resulting property will have the corresponding reference value.
072 *   "schema1Prefix:booleanProperty": true|false,
073 *   "schema2Prefix:integerProperty": 123,
074 *   ...
075 *   "schema3Prefix:complexProperty": {
076 *      "subProperty": ...,
077 *      ...
078 *   },
079 *   "schema4Prefix:listProperty": [
080 *      ...
081 *   ]
082 * }
083 * </pre>
084 *
085 * @since 7.2
086 */
087@Setup(mode = SINGLETON, priority = REFERENCE)
088public class DocumentPropertiesJsonReader extends AbstractJsonReader<List<Property>> {
089
090    private static final Logger log = LogManager.getLogger(DocumentPropertiesJsonReader.class);
091
092    public static final String DEFAULT_SCHEMA_NAME = "DEFAULT_SCHEMA_NAME";
093
094    /** @since 11.2 */
095    public static final String FALLBACK_RESOLVER = "resolver.";
096
097    @Inject
098    private SchemaManager schemaManager;
099
100    @Override
101    public List<Property> read(JsonNode jn) throws IOException {
102        List<Property> properties = new ArrayList<>();
103        Iterator<Entry<String, JsonNode>> propertyNodes = jn.fields();
104        while (propertyNodes.hasNext()) {
105            Entry<String, JsonNode> propertyNode = propertyNodes.next();
106            String propertyName = propertyNode.getKey();
107            Field field;
108            Property parent;
109            if (propertyName.contains(":")) {
110                field = schemaManager.getField(propertyName);
111                if (field == null) {
112                    continue;
113                }
114                parent = new DocumentPartImpl(field.getDeclaringType().getSchema());
115            } else {
116                String shemaName = ctx.getParameter(DEFAULT_SCHEMA_NAME);
117                Schema schema = schemaManager.getSchema(shemaName);
118                if (schema == null) {
119                    continue;
120                }
121                field = schema.getField(propertyName);
122                parent = new DocumentPartImpl(schema);
123            }
124            if (field == null) {
125                continue;
126            }
127            Property property = readProperty(parent, field, propertyNode.getValue());
128            if (property != null) {
129                properties.add(property);
130            }
131        }
132        return properties;
133    }
134
135    protected Property readProperty(Property parent, Field field, JsonNode jn) throws IOException {
136        Property property = PropertyFactory.createProperty(parent, field, 0);
137        property.setForceDirty(true);
138        if (jn.isNull()) {
139            property.setValue(null);
140        } else if (property.isScalar()) {
141            fillScalarProperty(property, jn);
142        } else if (property.isList()) {
143            fillListProperty(property, jn);
144        } else {
145            if (!(property instanceof BlobProperty)) {
146                fillComplexProperty(property, jn);
147            } else {
148                Blob blob = readEntity(Blob.class, Blob.class, jn);
149                property.setValue(blob);
150            }
151        }
152        property.setForceDirty(false);
153        return property;
154    }
155
156    private void fillScalarProperty(Property property, JsonNode jn) throws IOException {
157        if ((property instanceof ArrayProperty) && jn.isArray()) {
158            List<Object> values = new ArrayList<>();
159            Iterator<JsonNode> it = jn.elements();
160            JsonNode item;
161            Type fieldType = ((ListType) property.getType()).getFieldType();
162            while (it.hasNext()) {
163                item = it.next();
164                values.add(getScalarPropertyValue(property, item, fieldType));
165            }
166            property.setValue(castArrayPropertyValue(((SimpleType) fieldType).getPrimitiveType(), values));
167        } else {
168            property.setValue(getScalarPropertyValue(property, jn, property.getType()));
169        }
170    }
171
172    @SuppressWarnings({ "unchecked" })
173    private <T> T[] castArrayPropertyValue(Type type, List<Object> values) {
174        if (type instanceof StringType) {
175            return values.toArray((T[]) Array.newInstance(String.class, values.size()));
176        } else if (type instanceof BooleanType) {
177            return values.toArray((T[]) Array.newInstance(Boolean.class, values.size()));
178        } else if (type instanceof LongType) {
179            return values.toArray((T[]) Array.newInstance(Long.class, values.size()));
180        } else if (type instanceof DoubleType) {
181            return values.toArray((T[]) Array.newInstance(Double.class, values.size()));
182        } else if (type instanceof IntegerType) {
183            return values.toArray((T[]) Array.newInstance(Integer.class, values.size()));
184        } else if (type instanceof BinaryType) {
185            return values.toArray((T[]) Array.newInstance(Byte.class, values.size()));
186        } else if (type instanceof DateType) {
187            return values.toArray((T[]) Array.newInstance(Calendar.class, values.size()));
188        }
189        throw new MarshallingException("Primitive type not found: " + type.getName());
190    }
191
192    private Object getScalarPropertyValue(Property property, JsonNode jn, Type type) throws IOException {
193        Object value;
194        if (jn.isObject()) {
195            ObjectResolver resolver = type.getObjectResolver();
196            if (resolver == null) {
197                // fallback on resolver present in rendering context (for example xvocabulary parent field)
198                resolver = ctx.getParameter(FALLBACK_RESOLVER + property.getName());
199            }
200            if (resolver == null) {
201                // Let's assume it is a blob of which content has to be stored in a string property.
202                if (type.getSuperType() instanceof StringType) {
203                    Blob blob = readEntity(Blob.class, Blob.class, jn);
204                    if (blob != null) {
205                        return blob.getString();
206                    }
207                }
208                throw new MarshallingException("Unable to parse the property " +  property.getXPath());
209            }
210            Object object = null;
211            for (Class<?> clazz : resolver.getManagedClasses()) {
212                try {
213                    object = readEntity(clazz, clazz, jn);
214                    if (object != null) {
215                        break;
216                    }
217                } catch (MarshallingException e) {
218                    log.info("Unable to read the entity - {}", e::getMessage, () -> e);
219                }
220            }
221            if (object == null) {
222                throw new MarshallingException("Unable to parse the property " + property.getXPath());
223            }
224            value = resolver.getReference(object);
225            if (value == null) {
226                throw new MarshallingException("Property " + property.getXPath()
227                        + " value cannot be resolved by the matching resolver " + resolver.getName());
228            }
229        } else {
230            value = getPropertyValue(((SimpleType) type).getPrimitiveType(), jn);
231        }
232        return value;
233    }
234
235    private Object getPropertyValue(Type type, JsonNode jn) throws IOException {
236        Object value;
237        if (jn.isNull()) {
238            value = null;
239        } else if (type instanceof BooleanType) {
240            value = jn.asBoolean();
241        } else if (type instanceof LongType) {
242            value = jn.asLong();
243        } else if (type instanceof DoubleType) {
244            value = jn.asDouble();
245        } else if (type instanceof IntegerType) {
246            value = jn.asInt();
247        } else if (type instanceof BinaryType) {
248            value = jn.binaryValue();
249        } else {
250            value = type.decode(jn.asText());
251        }
252        return value;
253    }
254
255    private void fillListProperty(Property property, JsonNode jn) throws IOException {
256        ListType listType = (ListType) property.getType();
257        if (property instanceof ArrayProperty) {
258            fillScalarProperty(property, jn);
259        } else if (jn.isEmpty()) {
260            property.setValue(null);
261        } else {
262            JsonNode elNode;
263            Iterator<JsonNode> it = jn.elements();
264            while (it.hasNext()) {
265                elNode = it.next();
266                Property child = readProperty(property, listType.getField(), elNode);
267                property.addValue(child.getValue());
268            }
269        }
270    }
271
272    private void fillComplexProperty(Property property, JsonNode jn) throws IOException {
273        Entry<String, JsonNode> elNode;
274        Iterator<Entry<String, JsonNode>> it = jn.fields();
275        ComplexProperty complexProperty = (ComplexProperty) property;
276        ComplexType type = complexProperty.getType();
277        while (it.hasNext()) {
278            elNode = it.next();
279            String elName = elNode.getKey();
280            Field field = type.getField(elName);
281            if (field != null) {
282                Property child = readProperty(property, field, elNode.getValue());
283                property.set(elName, child);
284            }
285        }
286    }
287
288}