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", <-- 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", <-- if dublincore is present and if dc:modified is not null 086 * "versionLabel": "DOCUMENT_VERSION", <-- only activated with parameter fetch.document=versionLabel or system property nuxeo.document.json.fetch.heavy=true 087 * "lockOwner": "LOCK_OWNER", <-- only activated if locked and with parameter fetch.document=lock or system property nuxeo.document.json.fetch.heavy=true 088 * "lockCreated": "LOCK_DATE", <-- only activated if locked and with parameter fetch.document=lock or system property nuxeo.document.json.fetch.heavy=true 089 * "properties": { <-- only present with parameter properties=schema1,schema2,... see {@link DocumentPropertyJsonWriter} for format 090 * "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 091 * "schemaPrefix:booleanProperty": true|false, 092 * "schemaPrefix:integerProperty": 123, 093 * ... 094 * "schemaPrefix:complexProperty": { 095 * "subProperty": ..., 096 * ... 097 * }, 098 * "schemaPrefix:listProperty": [ 099 * ... 100 * ] 101 * } 102 * <-- contextParameters if there are enrichers activated 103 * <-- 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}