001/*
002 * (C) Copyright 2015-2018 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 */
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.lang3.StringUtils;
036import org.apache.commons.lang3.reflect.TypeUtils;
037import org.nuxeo.ecm.core.api.Blob;
038import org.nuxeo.ecm.core.api.CoreSession;
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.DocumentType;
053import org.nuxeo.ecm.core.schema.SchemaManager;
054import org.nuxeo.ecm.core.schema.types.Field;
055import org.nuxeo.ecm.core.schema.types.Schema;
056import org.nuxeo.runtime.api.Framework;
057
058import com.fasterxml.jackson.databind.JsonNode;
059
060/**
061 * Convert Json as {@link DocumentModel}.
062 * <p>
063 * Format is (any additional json property is ignored):
064 *
065 * <pre>
066 * {
067 *   "entity-type": "document",
068 *   "uid": "EXISTING_DOCUMENT_UID", <- use it to update an existing document
069 *   "name": "DOCUMENT_NAME", <- use it to create an new document
070 *   "type": "DOCUMENT_TYPE", <- use it to create an new document
071 *   "changeToken": "CHANGE_TOKEN", <- pass the previous change token for optimistic locking
072 *   "properties": ...  <-- see {@link DocumentPropertiesJsonReader}
073 * }
074 * </pre>
075 * </p>
076 *
077 * @since 7.2
078 */
079@Setup(mode = SINGLETON, priority = REFERENCE)
080public class DocumentModelJsonReader extends EntityJsonReader<DocumentModel> {
081
082    public static final String LEGACY_MODE_READER = "DocumentModelLegacyModeReader";
083
084    public DocumentModelJsonReader() {
085        super(ENTITY_TYPE);
086    }
087
088    @Override
089    public DocumentModel read(Class<?> clazz, Type genericType, MediaType mediaType, InputStream in)
090            throws IOException {
091        Reader<DocumentModel> reader = ctx.getParameter(LEGACY_MODE_READER);
092        if (reader != null) {
093            return reader.read(clazz, genericType, mediaType, in);
094        } else {
095            return super.read(clazz, genericType, mediaType, in);
096        }
097    }
098
099    @Override
100    protected DocumentModel readEntity(JsonNode jn) throws IOException {
101
102        SimpleDocumentModel simpleDoc = new SimpleDocumentModel();
103        String name = getStringField(jn, "name");
104        if (StringUtils.isNotBlank(name)) {
105            simpleDoc.setPathInfo(null, name);
106        }
107        String type = getStringField(jn, "type");
108        if (StringUtils.isNotBlank(type)) {
109            simpleDoc.setType(type);
110        }
111
112        JsonNode propsNode = jn.get("properties");
113        if (propsNode != null && !propsNode.isNull() && propsNode.isObject()) {
114            ParameterizedType genericType = TypeUtils.parameterize(List.class, Property.class);
115            List<Property> properties = readEntity(List.class, genericType, propsNode);
116            for (Property property : properties) {
117                String propertyName = property.getName();
118                // handle schema with no prefix
119                if (!propertyName.contains(":")) {
120                    propertyName = property.getField().getDeclaringType().getName() + ":" + propertyName;
121                }
122                simpleDoc.setPropertyValue(propertyName, property.getValue());
123            }
124        }
125
126        DocumentModel doc;
127
128        String uid = getStringField(jn, "uid");
129        if (StringUtils.isNotBlank(uid)) {
130            try (SessionWrapper wrapper = ctx.getSession(null)) {
131                doc = wrapper.getSession().getDocument(new IdRef(uid));
132            }
133            avoidBlobUpdate(simpleDoc, doc);
134            applyDirtyPropertyValues(simpleDoc, doc);
135            String changeToken = getStringField(jn, "changeToken");
136            doc.putContextData(CoreSession.CHANGE_TOKEN, changeToken);
137        } else if (StringUtils.isNotBlank(type)) {
138            SimpleDocumentModel createdDoc = new SimpleDocumentModel();
139            if (StringUtils.isNotBlank(name)) {
140                createdDoc.setPathInfo(null, name);
141            }
142            createdDoc.setType(simpleDoc.getType());
143            applyAllPropertyValues(simpleDoc, createdDoc);
144            doc = createdDoc;
145        } else {
146            doc = simpleDoc;
147        }
148
149        return doc;
150    }
151
152    /**
153     * Avoid the blob updates. It's managed by custom ways.
154     */
155    private static void avoidBlobUpdate(DocumentModel docToClean, DocumentModel docRef) {
156        for (String schema : docToClean.getSchemas()) {
157            for (String field : docToClean.getDataModel(schema).getDirtyFields()) {
158                avoidBlobUpdate(docToClean.getProperty(field), docRef);
159            }
160        }
161    }
162
163    private static void avoidBlobUpdate(Property propToClean, DocumentModel docRef) {
164        if (propToClean instanceof BlobProperty) {
165            // if the blob used to exist
166            if (propToClean.getValue() == null) {
167                try {
168                    Serializable value = docRef.getPropertyValue(propToClean.getXPath());
169                    propToClean.setValue(value);
170                } catch (PropertyNotFoundException e) {
171                    // As the blob property doesn't exist in the document in the first place, ignore the operation
172                }
173            }
174        } else if (propToClean instanceof ComplexProperty) {
175            ComplexProperty complexPropToClean = (ComplexProperty) propToClean;
176            for (Field field : complexPropToClean.getType().getFields()) {
177                Property childPropToClean = complexPropToClean.get(field.getName().getLocalName());
178                avoidBlobUpdate(childPropToClean, docRef);
179            }
180        } else if (propToClean instanceof ListProperty) {
181            ListProperty listPropToClean = (ListProperty) propToClean;
182            for (int i = 0; i < listPropToClean.size(); i++) {
183                Property elPropToClean = listPropToClean.get(i);
184                avoidBlobUpdate(elPropToClean, docRef);
185            }
186        }
187    }
188
189    public static void applyPropertyValues(DocumentModel src, DocumentModel dst) {
190        avoidBlobUpdate(src, dst);
191        applyPropertyValues(src, dst, true);
192        // copy change token
193        dst.getContextData().putAll(src.getContextData());
194    }
195
196    public static void applyPropertyValues(DocumentModel src, DocumentModel dst, boolean dirtyOnly) {
197        // if not "dirty only", it handles all the schemas for the given type
198        // so it will trigger the default values initialization
199        if (dirtyOnly) {
200            applyDirtyPropertyValues(src, dst);
201        } else {
202            applyAllPropertyValues(src, dst);
203        }
204    }
205
206    public static void applyDirtyPropertyValues(DocumentModel src, DocumentModel dst) {
207        String[] schemas = src.getSchemas();
208        for (String schema : schemas) {
209            DataModelImpl dataModel = (DataModelImpl) dst.getDataModel(schema);
210            DataModelImpl fromDataModel = (DataModelImpl) src.getDataModel(schema);
211            for (String field : fromDataModel.getDirtyFields()) {
212                Serializable data = (Serializable) fromDataModel.getData(field);
213                try {
214                    if (!(dataModel.getDocumentPart().get(field) instanceof BlobProperty)) {
215                        dataModel.setData(field, data);
216                    } else {
217                        dataModel.setData(field, decodeBlob(data));
218                    }
219                } catch (PropertyNotFoundException e) {
220                    continue;
221                }
222            }
223        }
224    }
225
226    public static void applyAllPropertyValues(DocumentModel src, DocumentModel dst) {
227        SchemaManager service = Framework.getService(SchemaManager.class);
228        DocumentType type = service.getDocumentType(src.getType());
229        String[] schemas = type.getSchemaNames();
230        for (String schemaName : schemas) {
231            Schema schema = service.getSchema(schemaName);
232            DataModelImpl dataModel = (DataModelImpl) dst.getDataModel(schemaName);
233            DataModelImpl fromDataModel = (DataModelImpl) src.getDataModel(schemaName);
234            for (Field field : schema.getFields()) {
235                String fieldName = field.getName().getLocalName();
236                Serializable data = (Serializable) fromDataModel.getData(fieldName);
237                try {
238                    if (!(dataModel.getDocumentPart().get(fieldName) instanceof BlobProperty)) {
239                        dataModel.setData(fieldName, data);
240                    } else {
241                        dataModel.setData(fieldName, decodeBlob(data));
242                    }
243                } catch (PropertyNotFoundException e) {
244                    continue;
245                }
246            }
247        }
248    }
249
250    private static Serializable decodeBlob(Serializable data) {
251        if (data instanceof Blob) {
252            return data;
253        } else {
254            return null;
255        }
256    }
257
258}