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