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        jg.writeBooleanField("isVersion", doc.isVersion());
148        jg.writeBooleanField("isProxy", doc.isProxy());
149        jg.writeStringField("changeToken", doc.getChangeToken());
150        jg.writeBooleanField("isTrashed", doc.getRef() != null && doc.isTrashed());
151        jg.writeStringField("title", doc.getTitle());
152        if (mustFetch("versionLabel")) {
153            String versionLabel = doc.getVersionLabel();
154            jg.writeStringField("versionLabel", versionLabel != null ? versionLabel : "");
155        }
156        if (mustFetch("lock")) {
157            Lock lock = doc.getLockInfo();
158            if (lock != null) {
159                jg.writeStringField("lockOwner", lock.getOwner());
160                jg.writeStringField("lockCreated", ISODateTimeFormat.dateTime().print(new DateTime(lock.getCreated())));
161            }
162        }
163        if (doc.hasSchema("dublincore")) {
164            Calendar cal = (Calendar) doc.getPropertyValue("dc:modified");
165            if (cal != null) {
166                jg.writeStringField("lastModified", DateParser.formatW3CDateTime(cal.getTime()));
167            }
168        }
169
170        try (Closeable resource = ctx.wrap().controlDepth().open()) {
171            Set<String> schemas = ctx.getProperties();
172            if (schemas.size() > 0) {
173                jg.writeObjectFieldStart("properties");
174                if (schemas.contains(WILDCARD_VALUE)) {
175                    // full document
176                    for (String schema : doc.getSchemas()) {
177                        writeSchemaProperties(jg, doc, schema);
178                    }
179                } else {
180                    for (String schema : schemas) {
181                        if (doc.hasSchema(schema)) {
182                            writeSchemaProperties(jg, doc, schema);
183                        }
184                    }
185                }
186                jg.writeEndObject();
187            }
188        } catch (MaxDepthReachedException e) {
189            // do not load properties
190        }
191
192        jg.writeArrayFieldStart("facets");
193        for (String facet : doc.getFacets()) {
194            jg.writeString(facet);
195        }
196        jg.writeEndArray();
197    }
198
199    private void writeSchemaProperties(JsonGenerator jg, DocumentModel doc, String schemaName) throws IOException {
200        Writer<Property> propertyWriter = registry.getWriter(ctx, Property.class, APPLICATION_JSON_TYPE);
201        // provides the current document to the property marshaller
202        try (Closeable resource = ctx.wrap().with(ENTITY_TYPE, doc).open()) {
203            Schema schema = schemaManager.getSchema(schemaName);
204            String prefix = schema.getNamespace().prefix;
205            if (prefix == null || prefix.length() == 0) {
206                prefix = schemaName;
207            }
208            prefix = prefix + ":";
209            for (Field field : schema.getFields()) {
210                String prefixedName = prefix + field.getName().getLocalName();
211                jg.writeFieldName(prefixedName);
212                Property property = doc.getProperty(prefixedName);
213                OutputStream out = new OutputStreamWithJsonWriter(jg);
214                propertyWriter.write(property, Property.class, Property.class, APPLICATION_JSON_TYPE, out);
215            }
216        }
217    }
218
219}