001/*
002 * (C) Copyright 2015-2017 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.Iterator;
028import java.util.List;
029import java.util.Map.Entry;
030
031import javax.inject.Inject;
032
033import org.codehaus.jackson.JsonNode;
034import org.nuxeo.ecm.core.api.Blob;
035import org.nuxeo.ecm.core.api.model.Property;
036import org.nuxeo.ecm.core.api.model.impl.ArrayProperty;
037import org.nuxeo.ecm.core.api.model.impl.ComplexProperty;
038import org.nuxeo.ecm.core.api.model.impl.DocumentPartImpl;
039import org.nuxeo.ecm.core.api.model.impl.PropertyFactory;
040import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty;
041import org.nuxeo.ecm.core.io.marshallers.json.AbstractJsonReader;
042import org.nuxeo.ecm.core.io.registry.MarshallingException;
043import org.nuxeo.ecm.core.io.registry.reflect.Setup;
044import org.nuxeo.ecm.core.schema.SchemaManager;
045import org.nuxeo.ecm.core.schema.types.ComplexType;
046import org.nuxeo.ecm.core.schema.types.Field;
047import org.nuxeo.ecm.core.schema.types.ListType;
048import org.nuxeo.ecm.core.schema.types.Schema;
049import org.nuxeo.ecm.core.schema.types.SimpleType;
050import org.nuxeo.ecm.core.schema.types.Type;
051import org.nuxeo.ecm.core.schema.types.primitives.BinaryType;
052import org.nuxeo.ecm.core.schema.types.primitives.BooleanType;
053import org.nuxeo.ecm.core.schema.types.primitives.DoubleType;
054import org.nuxeo.ecm.core.schema.types.primitives.IntegerType;
055import org.nuxeo.ecm.core.schema.types.primitives.LongType;
056import org.nuxeo.ecm.core.schema.types.primitives.StringType;
057import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolver;
058
059/**
060 * Convert Json as {@link List<Property>}.
061 * <p>
062 * Format is:
063 *
064 * <pre>
065 * {
066 *   "schema1Prefix:stringProperty": "stringPropertyValue", <-- 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.
067 *   "schema1Prefix:booleanProperty": true|false,
068 *   "schema2Prefix:integerProperty": 123,
069 *   ...
070 *   "schema3Prefix:complexProperty": {
071 *      "subProperty": ...,
072 *      ...
073 *   },
074 *   "schema4Prefix:listProperty": [
075 *      ...
076 *   ]
077 * }
078 * </pre>
079 * </p>
080 *
081 * @since 7.2
082 */
083@Setup(mode = SINGLETON, priority = REFERENCE)
084public class DocumentPropertiesJsonReader extends AbstractJsonReader<List<Property>> {
085
086    public static String DEFAULT_SCHEMA_NAME = "DEFAULT_SCHEMA_NAME";
087
088    @Inject
089    private SchemaManager schemaManager;
090
091    @Override
092    public List<Property> read(JsonNode jn) throws IOException {
093        List<Property> properties = new ArrayList<>();
094        Iterator<Entry<String, JsonNode>> propertyNodes = jn.getFields();
095        while (propertyNodes.hasNext()) {
096            Entry<String, JsonNode> propertyNode = propertyNodes.next();
097            String propertyName = propertyNode.getKey();
098            Field field;
099            Property parent;
100            if (propertyName.contains(":")) {
101                field = schemaManager.getField(propertyName);
102                if (field == null) {
103                    continue;
104                }
105                parent = new DocumentPartImpl(field.getDeclaringType().getSchema());
106            } else {
107                String shemaName = ctx.getParameter(DEFAULT_SCHEMA_NAME);
108                Schema schema = schemaManager.getSchema(shemaName);
109                if (schema == null) {
110                    continue;
111                }
112                field = schema.getField(propertyName);
113                parent = new DocumentPartImpl(schema);
114            }
115            if (field == null) {
116                continue;
117            }
118            Property property = readProperty(parent, field, propertyNode.getValue());
119            if (property != null) {
120                properties.add(property);
121            }
122        }
123        return properties;
124    }
125
126    protected Property readProperty(Property parent, Field field, JsonNode jn) throws IOException {
127        Property property = PropertyFactory.createProperty(parent, field, 0);
128        if (jn.isNull()) {
129            property.setValue(null);
130        } else if (property.isScalar()) {
131            fillScalarProperty(property, jn);
132        } else if (property.isList()) {
133            fillListProperty(property, jn);
134        } else {
135            if (!(property instanceof BlobProperty)) {
136                fillComplexProperty(property, jn);
137            } else {
138                Blob blob = readEntity(Blob.class, Blob.class, jn);
139                property.setValue(blob);
140            }
141        }
142        return property;
143    }
144
145    private void fillScalarProperty(Property property, JsonNode jn) throws IOException {
146        if ((property instanceof ArrayProperty) && jn.isArray()) {
147            List<Object> values = new ArrayList<>();
148            Iterator<JsonNode> it = jn.getElements();
149            JsonNode item;
150            Type fieldType = ((ListType) property.getType()).getFieldType();
151            while (it.hasNext()) {
152                item = it.next();
153                values.add(getScalarPropertyValue(property, item, fieldType));
154            }
155            property.setValue(castArrayPropertyValue(((SimpleType) fieldType).getPrimitiveType(), values));
156        } else {
157            property.setValue(getScalarPropertyValue(property, jn, property.getType()));
158        }
159    }
160
161    @SuppressWarnings({ "unchecked" })
162    private <T> T[] castArrayPropertyValue(Type type, List<Object> values) throws IOException {
163        if (type instanceof StringType) {
164            return values.toArray((T[]) Array.newInstance(String.class, values.size()));
165        } else if (type instanceof BooleanType) {
166            return values.toArray((T[]) Array.newInstance(Boolean.class, values.size()));
167        } else if (type instanceof LongType) {
168            return values.toArray((T[]) Array.newInstance(Long.class, values.size()));
169        } else if (type instanceof DoubleType) {
170            return values.toArray((T[]) Array.newInstance(Double.class, values.size()));
171        } else if (type instanceof IntegerType) {
172            return values.toArray((T[]) Array.newInstance(Integer.class, values.size()));
173        } else if (type instanceof BinaryType) {
174            return values.toArray((T[]) Array.newInstance(Byte.class, values.size()));
175        }
176        throw new MarshallingException("Primitive type not found: " + type.getName());
177    }
178
179    private Object getScalarPropertyValue(Property property, JsonNode jn, Type type) throws IOException {
180        Object value;
181        if (jn.isObject()) {
182            ObjectResolver resolver = type.getObjectResolver();
183            if (resolver == null) {
184                if (type.getSuperType() != null && type.getSuperType() instanceof StringType) {
185                    // Let's assume it is a blob of which content has to be stored in a string property.
186                    Blob blob = readEntity(Blob.class, Blob.class, jn);
187                    if (blob == null) {
188                        throw new MarshallingException("Unable to parse the property " + property.getXPath());
189                    }
190                    return blob.getString();
191                }
192            }
193            Object object = null;
194            for (Class<?> clazz : resolver.getManagedClasses()) {
195                try {
196                    object = readEntity(clazz, clazz, jn);
197                } catch (Exception e) {
198                    continue;
199                }
200            }
201            if (object == null) {
202                throw new MarshallingException("Unable to parse the property " + property.getXPath());
203            }
204            value = resolver.getReference(object);
205            if (value == null) {
206                throw new MarshallingException("Property " + property.getXPath()
207                        + " value cannot be resolved by the matching resolver " + resolver.getName());
208            }
209        } else {
210            value = getPropertyValue(((SimpleType) type).getPrimitiveType(), jn);
211        }
212        return value;
213    }
214
215    private Object getPropertyValue(Type type, JsonNode jn) throws IOException {
216        Object value;
217        if (jn.isNull()) {
218            value = null;
219        } else if (type instanceof BooleanType) {
220            value = jn.getValueAsBoolean();
221        } else if (type instanceof LongType) {
222            value = jn.getValueAsLong();
223        } else if (type instanceof DoubleType) {
224            value = jn.getValueAsDouble();
225        } else if (type instanceof IntegerType) {
226            value = jn.getValueAsInt();
227        } else if (type instanceof BinaryType) {
228            value = jn.getBinaryValue();
229        } else {
230            value = type.decode(jn.getValueAsText());
231        }
232        return value;
233    }
234
235    private void fillListProperty(Property property, JsonNode jn) throws IOException {
236        ListType listType = (ListType) property.getType();
237        if (property instanceof ArrayProperty) {
238            fillScalarProperty(property, jn);
239        } else {
240            JsonNode elNode;
241            Iterator<JsonNode> it = jn.getElements();
242            while (it.hasNext()) {
243                elNode = it.next();
244                Property child = readProperty(property, listType.getField(), elNode);
245                property.addValue(child.getValue());
246            }
247        }
248    }
249
250    private void fillComplexProperty(Property property, JsonNode jn) throws IOException {
251        Entry<String, JsonNode> elNode;
252        Iterator<Entry<String, JsonNode>> it = jn.getFields();
253        ComplexProperty complexProperty = (ComplexProperty) property;
254        ComplexType type = complexProperty.getType();
255        while (it.hasNext()) {
256            elNode = it.next();
257            String elName = elNode.getKey();
258            Field field = type.getField(elName);
259            if (field != null) {
260                Property child = readProperty(property, field, elNode.getValue());
261                property.setValue(elName, child.getValue());
262            }
263        }
264    }
265
266}