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