001/*
002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Nicolas Chapurlat <nchapurlat@nuxeo.com>
016 */
017
018package org.nuxeo.ecm.core.io.marshallers.json.document;
019
020import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
021import static org.nuxeo.ecm.core.io.registry.MarshallingConstants.WILDCARD_VALUE;
022import static org.nuxeo.ecm.core.io.registry.reflect.Instantiations.SINGLETON;
023import static org.nuxeo.ecm.core.io.registry.reflect.Priorities.REFERENCE;
024
025import java.io.Closeable;
026import java.io.IOException;
027import java.io.OutputStream;
028import java.util.Calendar;
029import java.util.Set;
030
031import javax.inject.Inject;
032
033import org.codehaus.jackson.JsonGenerator;
034import org.joda.time.DateTime;
035import org.joda.time.format.ISODateTimeFormat;
036import org.nuxeo.ecm.core.api.DocumentModel;
037import org.nuxeo.ecm.core.api.Lock;
038import org.nuxeo.ecm.core.api.model.Property;
039import org.nuxeo.ecm.core.io.marshallers.json.ExtensibleEntityJsonWriter;
040import org.nuxeo.ecm.core.io.marshallers.json.OutputStreamWithJsonWriter;
041import org.nuxeo.ecm.core.io.marshallers.json.enrichers.AbstractJsonEnricher;
042import org.nuxeo.ecm.core.io.registry.Writer;
043import org.nuxeo.ecm.core.io.registry.context.MaxDepthReachedException;
044import org.nuxeo.ecm.core.io.registry.reflect.Setup;
045import org.nuxeo.ecm.core.schema.SchemaManager;
046import org.nuxeo.ecm.core.schema.types.Field;
047import org.nuxeo.ecm.core.schema.types.Schema;
048import org.nuxeo.ecm.core.schema.utils.DateParser;
049import org.nuxeo.runtime.api.Framework;
050
051import com.thoughtworks.xstream.io.json.JsonWriter;
052
053/**
054 * Convert {@link DocumentModel} to Json.
055 * <p>
056 * This marshaller is enrichable: register class implementing {@link AbstractJsonEnricher} and managing
057 * {@link DocumentModel}.
058 * </p>
059 * <p>
060 * This marshaller is also extensible: extend it and simply override
061 * {@link ExtensibleEntityJsonWriter#extend(DocumentModel, JsonWriter)}.
062 * </p>
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 *   "state": "DOCUMENT_STATE",
074 *   "parentRef": "PARENT_DOCUMENT_UID",
075 *   "isCheckedOut": true|false,
076 *   "changeToken": null|"CHANGE_TOKEN",
077 *   "isCheckedOut": true|false,
078 *   "title": "DOCUMENT_TITLE",
079 *   "lastModified": "DATE_UPDATE",  <-- if dublincore is present and if dc:modified is not null
080 *   "versionLabel": "DOCUMENT_VERSION",  <-- only activated with parameter fetch.document=versionLabel or system property nuxeo.document.json.fetch.heavy=true
081 *   "lockOwner": "LOCK_OWNER",  <-- only activated if locked and with parameter fetch.document=lock or system property nuxeo.document.json.fetch.heavy=true
082 *   "lockCreated": "LOCK_DATE",  <-- only activated if locked and with parameter fetch.document=lock or system property nuxeo.document.json.fetch.heavy=true
083 *   "properties": {   <-- only present with parameter properties=schema1,schema2,... see {@link DocumentPropertyJsonWriter} for format
084 *     "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
085 *     "schemaPrefix:booleanProperty": true|false,
086 *     "schemaPrefix:integerProperty": 123,
087 *     ...
088 *     "schemaPrefix:complexProperty": {
089 *        "subProperty": ...,
090 *        ...
091 *     },
092 *     "schemaPrefix:listProperty": [
093 *        ...
094 *     ]
095 *   }
096 *             <-- contextParameters if there are enrichers activated
097 *             <-- additional property provided by extend() method
098 * }
099 * </pre>
100 *
101 * </p>
102 *
103 * @since 7.2
104 */
105@Setup(mode = SINGLETON, priority = REFERENCE)
106public class DocumentModelJsonWriter extends ExtensibleEntityJsonWriter<DocumentModel> {
107
108    public static final String ENTITY_TYPE = "document";
109
110    public static final String DOCUMENT_JSON_FETCH_HEAVY_KEY = "nuxeo.document.json.fetch.heavy";
111
112    private static Boolean FETCH_HEAVY_VALUES = null;
113
114    private static boolean fetchHeavy() {
115        if (FETCH_HEAVY_VALUES == null) {
116            try {
117                FETCH_HEAVY_VALUES = Framework.isBooleanPropertyTrue("nuxeo.document.json.fetch.heavy");
118            } catch (Exception e) {
119                FETCH_HEAVY_VALUES = false;
120            }
121        }
122        return FETCH_HEAVY_VALUES;
123    }
124
125    private boolean mustFetch(String name) {
126        return ctx.getFetched(ENTITY_TYPE).contains(name) || fetchHeavy();
127    }
128
129    @Inject
130    private SchemaManager schemaManager;
131
132    public DocumentModelJsonWriter() {
133        super(ENTITY_TYPE, DocumentModel.class);
134    }
135
136    @Override
137    protected void writeEntityBody(DocumentModel doc, JsonGenerator jg) throws IOException {
138        jg.writeStringField("repository", doc.getRepositoryName());
139        jg.writeStringField("uid", doc.getId());
140        jg.writeStringField("path", doc.getPathAsString());
141        jg.writeStringField("type", doc.getType());
142        jg.writeStringField("state", doc.getRef() != null ? doc.getCurrentLifeCycleState() : null);
143        jg.writeStringField("parentRef", doc.getParentRef() != null ? doc.getParentRef().toString() : null);
144        jg.writeBooleanField("isCheckedOut", doc.isCheckedOut());
145        jg.writeStringField("changeToken", doc.getChangeToken());
146        jg.writeStringField("title", doc.getTitle());
147        if (mustFetch("versionLabel")) {
148            String versionLabel = doc.getVersionLabel();
149            jg.writeStringField("versionLabel", versionLabel != null ? versionLabel : "");
150        }
151        if (mustFetch("lock")) {
152            Lock lock = doc.getLockInfo();
153            if (lock != null) {
154                jg.writeStringField("lockOwner", lock.getOwner());
155                jg.writeStringField("lockCreated", ISODateTimeFormat.dateTime().print(new DateTime(lock.getCreated())));
156            }
157        }
158        if (doc.hasSchema("dublincore")) {
159            Calendar cal = (Calendar) doc.getPropertyValue("dc:modified");
160            if (cal != null) {
161                jg.writeStringField("lastModified", DateParser.formatW3CDateTime(cal.getTime()));
162            }
163        }
164
165        try (Closeable resource = ctx.wrap().controlDepth().open()) {
166            Set<String> schemas = ctx.getProperties();
167            if (schemas.size() > 0) {
168                jg.writeObjectFieldStart("properties");
169                if (schemas.contains(WILDCARD_VALUE)) {
170                    // full document
171                    for (String schema : doc.getSchemas()) {
172                        writeSchemaProperties(jg, doc, schema);
173                    }
174                } else {
175                    for (String schema : schemas) {
176                        if (doc.hasSchema(schema)) {
177                            writeSchemaProperties(jg, doc, schema);
178                        }
179                    }
180                }
181                jg.writeEndObject();
182            }
183        } catch (MaxDepthReachedException e) {
184            // do not load properties
185        }
186
187        jg.writeArrayFieldStart("facets");
188        for (String facet : doc.getFacets()) {
189            jg.writeString(facet);
190        }
191        jg.writeEndArray();
192    }
193
194    private void writeSchemaProperties(JsonGenerator jg, DocumentModel doc, String schemaName) throws IOException {
195        Writer<Property> propertyWriter = registry.getWriter(ctx, Property.class, APPLICATION_JSON_TYPE);
196        // provides the current document to the property marshaller
197        try (Closeable resource = ctx.wrap().with(ENTITY_TYPE, doc).open()) {
198            Schema schema = schemaManager.getSchema(schemaName);
199            String prefix = schema.getNamespace().prefix;
200            if (prefix == null || prefix.length() == 0) {
201                prefix = schemaName;
202            }
203            prefix = prefix + ":";
204            for (Field field : schema.getFields()) {
205                String prefixedName = prefix + field.getName().getLocalName();
206                jg.writeFieldName(prefixedName);
207                Property property = doc.getProperty(prefixedName);
208                OutputStream out = new OutputStreamWithJsonWriter(jg);
209                propertyWriter.write(property, Property.class, Property.class, APPLICATION_JSON_TYPE, out);
210            }
211        }
212    }
213
214}