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