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