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;
032import java.util.stream.Stream;
033
034import javax.ws.rs.core.MediaType;
035
036import org.apache.commons.lang3.StringUtils;
037import org.apache.commons.lang3.reflect.TypeUtils;
038import org.apache.logging.log4j.LogManager;
039import org.apache.logging.log4j.Logger;
040import org.nuxeo.ecm.core.api.CoreSession;
041import org.nuxeo.ecm.core.api.DocumentModel;
042import org.nuxeo.ecm.core.api.IdRef;
043import org.nuxeo.ecm.core.api.impl.DataModelImpl;
044import org.nuxeo.ecm.core.api.impl.SimpleDocumentModel;
045import org.nuxeo.ecm.core.api.model.DocumentPart;
046import org.nuxeo.ecm.core.api.model.Property;
047import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
048import org.nuxeo.ecm.core.api.model.ReadOnlyPropertyException;
049import org.nuxeo.ecm.core.io.marshallers.json.EntityJsonReader;
050import org.nuxeo.ecm.core.io.registry.Reader;
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.runtime.api.Framework;
055
056import com.fasterxml.jackson.databind.JsonNode;
057
058/**
059 * Convert Json as {@link DocumentModel}.
060 * <p>
061 * Format is (any additional json property is ignored):
062 *
063 * <pre>
064 * {
065 *   "entity-type": "document",
066 *   "uid": "EXISTING_DOCUMENT_UID", &lt;- use it to update an existing document
067 *   "name": "DOCUMENT_NAME", &lt;- use it to create an new document
068 *   "type": "DOCUMENT_TYPE", &lt;- use it to create an new document
069 *   "changeToken": "CHANGE_TOKEN", &lt;- pass the previous change token for optimistic locking
070 *   "properties": ...  &lt;-- see {@link DocumentPropertiesJsonReader}
071 * }
072 * </pre>
073 *
074 * @since 7.2
075 */
076@Setup(mode = SINGLETON, priority = REFERENCE)
077public class DocumentModelJsonReader extends EntityJsonReader<DocumentModel> {
078
079    private static final Logger log = LogManager.getLogger(DocumentModelJsonReader.class);
080
081    public static final String LEGACY_MODE_READER = "DocumentModelLegacyModeReader";
082
083    public DocumentModelJsonReader() {
084        super(ENTITY_TYPE);
085    }
086
087    @Override
088    public DocumentModel read(Class<?> clazz, Type genericType, MediaType mediaType, InputStream in)
089            throws IOException {
090        Reader<DocumentModel> reader = ctx.getParameter(LEGACY_MODE_READER);
091        if (reader != null) {
092            return reader.read(clazz, genericType, mediaType, in);
093        } else {
094            return super.read(clazz, genericType, mediaType, in);
095        }
096    }
097
098    @Override
099    @SuppressWarnings("deprecation")
100    protected DocumentModel readEntity(JsonNode jn) throws IOException {
101        DocumentModel doc = getDocument(jn);
102
103        JsonNode propsNode = jn.get("properties");
104        if (propsNode != null && !propsNode.isNull() && propsNode.isObject()) {
105            ParameterizedType genericType = TypeUtils.parameterize(List.class, Property.class);
106            List<Property> properties = readEntity(List.class, genericType, propsNode);
107            for (Property property : properties) {
108                // security has been applied in previous step while reading properties
109                Framework.doPrivileged(() -> {
110                    DocumentPart part = doc.getPart(property.getSchema().getName());
111                    if (part != null) {
112                        part.set(property.getName(), property);
113                    }
114                });
115            }
116        }
117
118        return doc;
119    }
120
121    /**
122     * @since 11.1
123     */
124    protected DocumentModel getDocument(JsonNode jn) throws IOException {
125        String uid = getStringField(jn, "uid");
126        if (StringUtils.isNotBlank(uid) && ctx != null) {
127            try (var wrapper = ctx.getSession(null)) {
128                DocumentModel doc = wrapper.getSession().getDocument(new IdRef(uid));
129                String changeToken = getStringField(jn, "changeToken");
130                doc.putContextData(CoreSession.CHANGE_TOKEN, changeToken);
131                return doc;
132            }
133        } else {
134            String type = getStringField(jn, "type");
135            DocumentModel doc = StringUtils.isNotBlank(type) ? SimpleDocumentModel.ofType(type)
136                    : SimpleDocumentModel.empty();
137            String name = getStringField(jn, "name");
138            if (StringUtils.isNotBlank(name)) {
139                doc.setPathInfo(null, name);
140            }
141            return doc;
142        }
143    }
144
145    public static void applyPropertyValues(DocumentModel src, DocumentModel dst) {
146        applyPropertyValues(src, dst, true);
147        // copy change token
148        dst.getContextData().putAll(src.getContextData());
149    }
150
151    public static void applyPropertyValues(DocumentModel src, DocumentModel dst, boolean dirtyOnly) {
152        // if not "dirty only", it handles all the schemas for the given type
153        // so it will trigger the default values initialization
154        if (dirtyOnly) {
155            applyDirtyPropertyValues(src, dst);
156        } else {
157            applyAllPropertyValues(src, dst);
158        }
159    }
160
161    public static void applyDirtyPropertyValues(DocumentModel src, DocumentModel dst) {
162        Stream.of(src.getSchemas())
163              .flatMap(s -> src.getPropertyObjects(s).stream())
164              .filter(Property::isDirty)
165              .forEach(p -> applyPropertyValue(p, dst));
166    }
167
168    public static void applyAllPropertyValues(DocumentModel src, DocumentModel dst) {
169        SchemaManager service = Framework.getService(SchemaManager.class);
170        DocumentType type = service.getDocumentType(src.getType());
171        Stream.of(type.getSchemaNames())
172              .flatMap(s -> src.getPropertyObjects(s).stream())
173              .forEach(p -> applyPropertyValue(p, dst));
174    }
175
176    protected static void applyPropertyValue(Property property, DocumentModel dst) {
177        try {
178            dst.setPropertyValue(getXPath(property), property.getValue());
179        } catch (PropertyNotFoundException | ReadOnlyPropertyException e) {
180            log.trace("Can't apply property: {} to dst: {}", property, dst, e);
181        }
182    }
183
184    protected static String getXPath(Property property) {
185        String xpath = property.getXPath();
186        // if no prefix, use schema name as prefix
187        if (!xpath.contains(":")) {
188            xpath = property.getSchema().getName() + ":" + xpath;
189        }
190        return xpath;
191    }
192
193    /**
194     * @deprecated since 11.1. Not used anymore.
195     */
196    @Deprecated(since = "11.1")
197    protected static void applyPropertyValue(DataModelImpl srcDataModel, DataModelImpl dstDataModel, String fieldName) {
198        Serializable data = (Serializable) srcDataModel.getData(fieldName);
199        try {
200            dstDataModel.setData(fieldName, data);
201        } catch (PropertyNotFoundException | ReadOnlyPropertyException e) {
202            log.trace("Can't apply value: {} to src: {}", data, srcDataModel, e);
203        }
204
205    }
206}