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.marshallers.json.document.DocumentModelJsonWriter.ENTITY_TYPE;
023import static org.nuxeo.ecm.core.io.registry.reflect.Instantiations.SINGLETON;
024import static org.nuxeo.ecm.core.io.registry.reflect.Priorities.REFERENCE;
025
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.Serializable;
029import java.lang.reflect.ParameterizedType;
030import java.lang.reflect.Type;
031import java.util.List;
032
033import javax.ws.rs.core.MediaType;
034
035import org.apache.commons.lang.StringUtils;
036import org.apache.commons.lang3.reflect.TypeUtils;
037import org.codehaus.jackson.JsonNode;
038import org.nuxeo.ecm.core.api.Blob;
039import org.nuxeo.ecm.core.api.DocumentModel;
040import org.nuxeo.ecm.core.api.IdRef;
041import org.nuxeo.ecm.core.api.impl.DataModelImpl;
042import org.nuxeo.ecm.core.api.impl.SimpleDocumentModel;
043import org.nuxeo.ecm.core.api.model.Property;
044import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
045import org.nuxeo.ecm.core.api.model.impl.ComplexProperty;
046import org.nuxeo.ecm.core.api.model.impl.ListProperty;
047import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty;
048import org.nuxeo.ecm.core.io.marshallers.json.EntityJsonReader;
049import org.nuxeo.ecm.core.io.registry.Reader;
050import org.nuxeo.ecm.core.io.registry.context.RenderingContext.SessionWrapper;
051import org.nuxeo.ecm.core.io.registry.reflect.Setup;
052import org.nuxeo.ecm.core.schema.types.Field;
053
054/**
055 * Convert Json as {@link DocumentModel}.
056 * <p>
057 * Format is (any additional json property is ignored):
058 *
059 * <pre>
060 * {
061 *   "entity-type": "document",
062 *   "uid": "EXISTING_DOCUMENT_UID", <- use it to update an existing document
063 *   "repository": "REPOSITORY_NAME" , <- explicitely specify the repository name
064 *   "name": "DOCUMENT_NAME", <- use it to create an new document
065 *   "type": "DOCUMENT_TYPE", <- use it to create an new document
066 *   "properties": ...  <-- see {@link DocumentPropertiesJsonReader}
067 * }
068 * </pre>
069 *
070 * </p>
071 *
072 * @since 7.2
073 */
074@Setup(mode = SINGLETON, priority = REFERENCE)
075public class DocumentModelJsonReader extends EntityJsonReader<DocumentModel> {
076
077    public static final String LEGACY_MODE_READER = "DocumentModelLegacyModeReader";
078
079    public DocumentModelJsonReader() {
080        super(ENTITY_TYPE);
081    }
082
083    @Override
084    public DocumentModel read(Class<?> clazz, Type genericType, MediaType mediaType, InputStream in) throws IOException {
085        Reader<DocumentModel> reader = ctx.getParameter(LEGACY_MODE_READER);
086        if (reader != null) {
087            DocumentModel doc = reader.read(clazz, genericType, mediaType, in);
088            return doc;
089        } else {
090            return super.read(clazz, genericType, mediaType, in);
091        }
092    }
093
094    @Override
095    protected DocumentModel readEntity(JsonNode jn) throws IOException {
096
097        SimpleDocumentModel simpleDoc = new SimpleDocumentModel();
098        String name = getStringField(jn, "name");
099        if (StringUtils.isNotBlank(name)) {
100            simpleDoc.setPathInfo(null, name);
101        }
102        String type = getStringField(jn, "type");
103        if (StringUtils.isNotBlank(type)) {
104            simpleDoc.setType(type);
105        }
106
107        JsonNode propsNode = jn.get("properties");
108        if (propsNode != null && !propsNode.isNull() && propsNode.isObject()) {
109            ParameterizedType genericType = TypeUtils.parameterize(List.class, Property.class);
110            List<Property> properties = readEntity(List.class, genericType, propsNode);
111            for (Property property : properties) {
112                String propertyName = property.getName();
113                // handle schema with no prefix
114                if (!propertyName.contains(":")) {
115                    propertyName = property.getField().getDeclaringType().getName() + ":" + propertyName;
116                }
117                simpleDoc.setPropertyValue(propertyName, property.getValue());
118            }
119        }
120
121        DocumentModel doc = null;
122
123        String uid = getStringField(jn, "uid");
124        if (StringUtils.isNotBlank(uid)) {
125            try (SessionWrapper wrapper = ctx.getSession(null)) {
126                doc = wrapper.getSession().getDocument(new IdRef(uid));
127            }
128            avoidBlobUpdate(simpleDoc, doc);
129            applyPropertyValues(simpleDoc, doc);
130        } else {
131            doc = simpleDoc;
132        }
133
134        return doc;
135    }
136
137    /**
138     * Avoid the blob updates. It's managed by custom ways.
139     */
140    private void avoidBlobUpdate(DocumentModel docToClean, DocumentModel docRef) {
141        for (String schema : docToClean.getSchemas()) {
142            for (String field : docToClean.getDataModel(schema).getDirtyFields()) {
143                avoidBlobUpdate(docToClean.getProperty(field), docRef);
144            }
145        }
146    }
147
148    private void avoidBlobUpdate(Property propToClean, DocumentModel docRef) {
149        if (propToClean instanceof BlobProperty) {
150            // if the blob used to exist
151            if (propToClean.getValue() == null) {
152                Serializable value = docRef.getPropertyValue(propToClean.getPath());
153                propToClean.setValue(value);
154            }
155        } else if (propToClean instanceof ComplexProperty) {
156            ComplexProperty complexPropToClean = (ComplexProperty) propToClean;
157            for (Field field : complexPropToClean.getType().getFields()) {
158                Property childPropToClean = complexPropToClean.get(field.getName().getLocalName());
159                avoidBlobUpdate(childPropToClean, docRef);
160            }
161        } else if (propToClean instanceof ListProperty) {
162            ListProperty listPropToClean = (ListProperty) propToClean;
163            for (int i = 0; i < listPropToClean.size(); i++) {
164                Property elPropToClean = listPropToClean.get(i);
165                avoidBlobUpdate(elPropToClean, docRef);
166            }
167        }
168    }
169
170    public static void applyPropertyValues(DocumentModel src, DocumentModel dst) {
171        for (String schema : src.getSchemas()) {
172            DataModelImpl dataModel = (DataModelImpl) dst.getDataModel(schema);
173            DataModelImpl fromDataModel = (DataModelImpl) src.getDataModel(schema);
174
175            for (String field : fromDataModel.getDirtyFields()) {
176                Serializable data = (Serializable) fromDataModel.getData(field);
177                try {
178                    if (!(dataModel.getDocumentPart().get(field) instanceof BlobProperty)) {
179                        dataModel.setData(field, data);
180                    } else {
181                        dataModel.setData(field, decodeBlob(data));
182                    }
183                } catch (PropertyNotFoundException e) {
184                    continue;
185                }
186            }
187        }
188    }
189
190    private static Serializable decodeBlob(Serializable data) {
191        if (data instanceof Blob) {
192            return data;
193        } else {
194            return null;
195        }
196    }
197
198}