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 javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
023import static org.nuxeo.common.utils.DateUtils.formatISODateTime;
024import static org.nuxeo.common.utils.DateUtils.nowIfNull;
025import static org.nuxeo.ecm.core.io.registry.MarshallingConstants.WILDCARD_VALUE;
026import static org.nuxeo.ecm.core.io.registry.reflect.Instantiations.SINGLETON;
027import static org.nuxeo.ecm.core.io.registry.reflect.Priorities.REFERENCE;
028
029import java.io.Closeable;
030import java.io.IOException;
031import java.io.OutputStream;
032import java.util.Calendar;
033import java.util.Set;
034
035import javax.inject.Inject;
036
037import org.apache.commons.lang3.StringUtils;
038import org.nuxeo.ecm.core.api.DocumentModel;
039import org.nuxeo.ecm.core.api.Lock;
040import org.nuxeo.ecm.core.api.model.Property;
041import org.nuxeo.ecm.core.io.marshallers.json.ExtensibleEntityJsonWriter;
042import org.nuxeo.ecm.core.io.marshallers.json.OutputStreamWithJsonWriter;
043import org.nuxeo.ecm.core.io.marshallers.json.enrichers.AbstractJsonEnricher;
044import org.nuxeo.ecm.core.io.registry.Writer;
045import org.nuxeo.ecm.core.io.registry.context.MaxDepthReachedException;
046import org.nuxeo.ecm.core.io.registry.reflect.Setup;
047import org.nuxeo.ecm.core.schema.SchemaManager;
048import org.nuxeo.ecm.core.schema.types.Field;
049import org.nuxeo.ecm.core.schema.types.Schema;
050import org.nuxeo.ecm.core.schema.utils.DateParser;
051import org.nuxeo.runtime.api.Framework;
052
053import com.fasterxml.jackson.core.JsonGenerator;
054
055/**
056 * Convert {@link DocumentModel} to Json.
057 * <p>
058 * This marshaller is enrichable: register class implementing {@link AbstractJsonEnricher} and managing
059 * {@link DocumentModel}.
060 * <p>
061 * This marshaller is also extensible: extend it and simply override
062 * {@link ExtensibleEntityJsonWriter#extend(Object, JsonGenerator)}.
063 * <p>
064 * Format is:
065 *
066 * <pre>
067 * {
068 *   "entity-type":"document",
069 *   "repository": "REPOSITORY_NAME",
070 *   "uid": "DOCUMENT_UID",
071 *   "path": "DOCUMENT_PATH",
072 *   "type": "DOCUMENT_TYPE",
073 *   "facets": [ "FACET1", "FACET2", ... ],
074 *   "schemas": [ {"name": SCHEMA1", "prefix": "PREFIX1"}, {"name": SCHEMA2", "prefix": "PREFIX2"}, ... ],
075 *   "state": "DOCUMENT_STATE",
076 *   "parentRef": "PARENT_DOCUMENT_UID",
077 *   "isCheckedOut": true|false,
078 *   "isRecord": true|false,
079 *   "retainUntil": "RETAIN_UNTIL_DATE", &lt;-- or null
080 *   "hasLegalHold": true|false,
081 *   "isUnderRetentionOrLegalHold": true|false,
082 *   "changeToken": null|"CHANGE_TOKEN",
083 *   "isCheckedOut": true|false,
084 *   "title": "DOCUMENT_TITLE",
085 *   "lastModified": "DATE_UPDATE",  &lt;-- if dublincore is present and if dc:modified is not null
086 *   "versionLabel": "DOCUMENT_VERSION",  &lt;-- only activated with parameter fetch.document=versionLabel or system property nuxeo.document.json.fetch.heavy=true
087 *   "lockOwner": "LOCK_OWNER",  &lt;-- only activated if locked and with parameter fetch.document=lock or system property nuxeo.document.json.fetch.heavy=true
088 *   "lockCreated": "LOCK_DATE",  &lt;-- only activated if locked and with parameter fetch.document=lock or system property nuxeo.document.json.fetch.heavy=true
089 *   "properties": {   &lt;-- only present with parameter properties=schema1,schema2,... see {@link DocumentPropertyJsonWriter} for format
090 *     "schemaPrefix:stringProperty": "stringPropertyValue",  &lt;-- each property may be fetched if a resolver is associated with that property and if a parameter fetch.document=propXPath is present, in this case, an object will be marshalled as value
091 *     "schemaPrefix:booleanProperty": true|false,
092 *     "schemaPrefix:integerProperty": 123,
093 *     ...
094 *     "schemaPrefix:complexProperty": {
095 *        "subProperty": ...,
096 *        ...
097 *     },
098 *     "schemaPrefix:listProperty": [
099 *        ...
100 *     ]
101 *   }
102 *             &lt;-- contextParameters if there are enrichers activated
103 *             &lt;-- additional property provided by extend() method
104 * }
105 * </pre>
106 *
107 * @since 7.2
108 */
109@Setup(mode = SINGLETON, priority = REFERENCE)
110public class DocumentModelJsonWriter extends ExtensibleEntityJsonWriter<DocumentModel> {
111
112    public static final String ENTITY_TYPE = "document";
113
114    public static final String DOCUMENT_JSON_FETCH_HEAVY_KEY = "nuxeo.document.json.fetch.heavy";
115
116    protected static Boolean FETCH_HEAVY_VALUES = null;
117
118    protected static boolean fetchHeavy() {
119        if (FETCH_HEAVY_VALUES == null) {
120            try {
121                FETCH_HEAVY_VALUES = Framework.isBooleanPropertyTrue("nuxeo.document.json.fetch.heavy");
122            } catch (Exception e) {
123                FETCH_HEAVY_VALUES = false;
124            }
125        }
126        return FETCH_HEAVY_VALUES;
127    }
128
129    protected boolean mustFetch(String name) {
130        return ctx.getFetched(ENTITY_TYPE).contains(name) || fetchHeavy();
131    }
132
133    @Inject
134    protected SchemaManager schemaManager;
135
136    public DocumentModelJsonWriter() {
137        super(ENTITY_TYPE, DocumentModel.class);
138    }
139
140    @Override
141    protected void writeEntityBody(DocumentModel doc, JsonGenerator jg) throws IOException {
142        jg.writeStringField("repository", doc.getRepositoryName());
143        jg.writeStringField("uid", doc.getId());
144        jg.writeStringField("path", doc.getPathAsString());
145        jg.writeStringField("type", doc.getType());
146        jg.writeStringField("state", doc.getRef() != null ? doc.getCurrentLifeCycleState() : null);
147        jg.writeStringField("parentRef", doc.getParentRef() != null ? doc.getParentRef().toString() : null);
148        jg.writeBooleanField("isCheckedOut", doc.isCheckedOut());
149        jg.writeBooleanField("isRecord", doc.isRecord());
150        Calendar retainUntil = doc.getRetainUntil();
151        jg.writeStringField("retainUntil", retainUntil == null ? null : formatISODateTime(retainUntil));
152        jg.writeBooleanField("hasLegalHold", doc.hasLegalHold());
153        jg.writeBooleanField("isUnderRetentionOrLegalHold", doc.isUnderRetentionOrLegalHold());
154        boolean isVersion = doc.isVersion();
155        jg.writeBooleanField("isVersion", isVersion);
156        boolean isProxy = doc.isProxy();
157        jg.writeBooleanField("isProxy", isProxy);
158        if (isProxy) {
159            jg.writeStringField("proxyTargetId", doc.getSourceId());
160        }
161        if (isVersion || isProxy) {
162            jg.writeStringField("versionableId", doc.getVersionSeriesId());
163        }
164        jg.writeStringField("changeToken", doc.getChangeToken());
165        jg.writeBooleanField("isTrashed", doc.getRef() != null && doc.isTrashed());
166        jg.writeStringField("title", doc.getTitle());
167        if (mustFetch("versionLabel")) {
168            String versionLabel = doc.getVersionLabel();
169            jg.writeStringField("versionLabel", versionLabel != null ? versionLabel : "");
170        }
171        if (mustFetch("lock")) {
172            Lock lock = doc.getLockInfo();
173            if (lock != null) {
174                jg.writeStringField("lockOwner", lock.getOwner());
175                jg.writeStringField("lockCreated", formatISODateTime(nowIfNull(lock.getCreated())));
176            }
177        }
178        if (doc.hasSchema("dublincore")) {
179            Calendar cal = (Calendar) doc.getPropertyValue("dc:modified");
180            if (cal != null) {
181                jg.writeStringField("lastModified", DateParser.formatW3CDateTime(cal.getTime()));
182            }
183        }
184
185        String[] docSchemas  = doc.getSchemas();
186        try (Closeable resource = ctx.wrap().controlDepth().open()) {
187            Set<String> schemas = ctx.getProperties();
188            if (schemas.size() > 0) {
189                jg.writeObjectFieldStart("properties");
190                if (schemas.contains(WILDCARD_VALUE)) {
191                    // full document
192                    for (String schema : docSchemas) {
193                        writeSchemaProperties(jg, doc, schema);
194                    }
195                } else {
196                    for (String schema : schemas) {
197                        if (doc.hasSchema(schema)) {
198                            writeSchemaProperties(jg, doc, schema);
199                        }
200                    }
201                }
202                jg.writeEndObject();
203            }
204        } catch (MaxDepthReachedException e) {
205            // do not load properties
206        }
207
208        jg.writeArrayFieldStart("facets");
209        for (String facet : doc.getFacets()) {
210            jg.writeString(facet);
211        }
212        jg.writeEndArray();
213
214        jg.writeArrayFieldStart("schemas");
215        if (docSchemas.length > 0) {
216            SchemaManager schemaManager = Framework.getService(SchemaManager.class);
217            for (String schemaName : docSchemas) {
218                Schema schema = schemaManager.getSchema(schemaName);
219                if (schema != null) {
220                    jg.writeStartObject();
221                    String name = schema.getName();
222                    String prefix = schema.getNamespace().prefix;
223                    jg.writeStringField("name", name);
224                    jg.writeStringField("prefix", StringUtils.isEmpty(prefix) ? name : prefix);
225                    jg.writeEndObject();
226                }
227            }
228        }
229        jg.writeEndArray();
230
231    }
232
233    protected void writeSchemaProperties(JsonGenerator jg, DocumentModel doc, String schemaName) throws IOException {
234        Writer<Property> propertyWriter = registry.getWriter(ctx, Property.class, APPLICATION_JSON_TYPE);
235        // provides the current document to the property marshaller
236        try (Closeable resource = ctx.wrap().with(ENTITY_TYPE, doc).open()) {
237            Schema schema = schemaManager.getSchema(schemaName);
238            String prefix = schema.getNamespace().prefix;
239            if (prefix == null || prefix.length() == 0) {
240                prefix = schemaName;
241            }
242            prefix = prefix + ":";
243            for (Field field : schema.getFields()) {
244                String prefixedName = prefix + field.getName().getLocalName();
245                Property property = doc.getProperty(prefixedName);
246                if (!DocumentPropertyJsonWriter.skipProperty(ctx, property)) {
247                    jg.writeFieldName(prefixedName);
248                    OutputStream out = new OutputStreamWithJsonWriter(jg);
249                    propertyWriter.write(property, Property.class, Property.class, APPLICATION_JSON_TYPE, out);
250                }
251            }
252        }
253    }
254
255}