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 org.nuxeo.ecm.core.io.marshallers.json.document.DocumentModelJsonWriter.ENTITY_TYPE;
021import static org.nuxeo.ecm.core.io.registry.reflect.Instantiations.SINGLETON;
022import static org.nuxeo.ecm.core.io.registry.reflect.Priorities.REFERENCE;
023
024import java.io.ByteArrayOutputStream;
025import java.io.IOException;
026import java.util.Set;
027
028import org.apache.commons.logging.Log;
029import org.apache.commons.logging.LogFactory;
030import org.codehaus.jackson.JsonGenerator;
031import org.nuxeo.ecm.core.api.Blob;
032import org.nuxeo.ecm.core.api.DocumentModel;
033import org.nuxeo.ecm.core.api.model.Property;
034import org.nuxeo.ecm.core.api.model.impl.ArrayProperty;
035import org.nuxeo.ecm.core.api.model.impl.ListProperty;
036import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty;
037import org.nuxeo.ecm.core.io.download.DownloadService;
038import org.nuxeo.ecm.core.io.marshallers.json.AbstractJsonWriter;
039import org.nuxeo.ecm.core.io.registry.MarshallingException;
040import org.nuxeo.ecm.core.io.registry.reflect.Setup;
041import org.nuxeo.ecm.core.schema.types.ComplexTypeImpl;
042import org.nuxeo.ecm.core.schema.types.ListType;
043import org.nuxeo.ecm.core.schema.types.SimpleType;
044import org.nuxeo.ecm.core.schema.types.Type;
045import org.nuxeo.ecm.core.schema.types.primitives.BinaryType;
046import org.nuxeo.ecm.core.schema.types.primitives.BooleanType;
047import org.nuxeo.ecm.core.schema.types.primitives.DoubleType;
048import org.nuxeo.ecm.core.schema.types.primitives.IntegerType;
049import org.nuxeo.ecm.core.schema.types.primitives.LongType;
050import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolver;
051import org.nuxeo.runtime.api.Framework;
052
053/**
054 * Convert {@link Property} to Json.
055 * <p>
056 * Format is:
057 *
058 * <pre>
059 * "stringPropertyValue"  <-- for string property, 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
060 * or
061 * true|false  <- for boolean property
062 * or
063 * 123  <- for int property
064 * ...
065 * {  <- for complex property
066 *   "subProperty": ...,
067 *    ...
068 * },
069 * [ ... ] <- for list property
070 * }
071 * </pre>
072 *
073 * </p>
074 *
075 * @since 7.2
076 */
077@Setup(mode = SINGLETON, priority = REFERENCE)
078public class DocumentPropertyJsonWriter extends AbstractJsonWriter<Property> {
079
080    private static final Log log = LogFactory.getLog(DocumentPropertyJsonWriter.class);
081
082    @Override
083    public void write(Property prop, JsonGenerator jg) throws IOException {
084        writeProperty(jg, prop);
085        jg.flush();
086    }
087
088    protected void writeProperty(JsonGenerator jg, Property prop) throws IOException {
089        if (prop.isScalar()) {
090            writeScalarProperty(jg, prop);
091        } else if (prop.isList()) {
092            writeListProperty(jg, prop);
093        } else {
094            if (prop.isPhantom()) {
095                jg.writeNull();
096            } else if (prop instanceof BlobProperty) { // a blob
097                writeBlobProperty(jg, prop);
098            } else { // a complex property
099                writeComplexProperty(jg, prop);
100            }
101        }
102    }
103
104    protected void writeScalarProperty(JsonGenerator jg, Property prop) throws IOException {
105        Type type = prop.getType();
106        Object value = prop.getValue();
107        if (!fetchProperty(jg, prop.getType().getObjectResolver(), value, prop.getPath())) {
108            writeScalarPropertyValue(jg, ((SimpleType) type).getPrimitiveType(), value);
109        }
110    }
111
112    private void writeScalarPropertyValue(JsonGenerator jg, Type type, Object value) throws IOException {
113        if (value == null) {
114            jg.writeNull();
115        } else if (type instanceof BooleanType) {
116            jg.writeBoolean((Boolean) value);
117        } else if (type instanceof LongType) {
118            jg.writeNumber((Long) value);
119        } else if (type instanceof DoubleType) {
120            jg.writeNumber((Double) value);
121        } else if (type instanceof IntegerType) {
122            jg.writeNumber((Integer) value);
123        } else if (type instanceof BinaryType) {
124            jg.writeBinary((byte[]) value);
125        } else {
126            jg.writeString(type.encode(value));
127        }
128    }
129
130    protected boolean fetchProperty(JsonGenerator jg, ObjectResolver resolver, Object value, String path)
131            throws IOException {
132        if (value == null) {
133            return false;
134        }
135        boolean fetched = false;
136        if (resolver != null) {
137            String propertyPath = path.replaceFirst("/", "");
138            String genericPropertyPath = propertyPath.replaceAll("\\[[0-9]*\\]", "");
139            Set<String> fetchElements = ctx.getFetched(ENTITY_TYPE);
140            boolean fetch = false;
141            for (String fetchElement : fetchElements) {
142                if ("properties".equals(fetchElement) || propertyPath.startsWith(fetchElement)
143                        || genericPropertyPath.startsWith(fetchElement)) {
144                    fetch = true;
145                    break;
146                }
147            }
148            if (fetch) {
149                Object object = resolver.fetch(value);
150                if (object != null) {
151                    try {
152                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
153                        writeEntity(object, baos);
154                        jg.writeRawValue(baos.toString());
155                        fetched = true;
156                    } catch (MarshallingException e) {
157                        log.error("Unable to marshall as json the entity referenced by the property " + path, e);
158                    }
159                }
160            }
161        }
162        return fetched;
163    }
164
165    protected void writeListProperty(JsonGenerator jg, Property prop) throws IOException {
166        jg.writeStartArray();
167        if (prop instanceof ArrayProperty) {
168            Object[] ar = (Object[]) prop.getValue();
169            if (ar == null) {
170                jg.writeEndArray();
171                return;
172            }
173            Type itemType = ((ListType) prop.getType()).getFieldType();
174            ObjectResolver resolver = itemType.getObjectResolver();
175            String path = prop.getPath();
176            for (Object o : ar) {
177                if (!fetchProperty(jg, resolver, o, path)) {
178                    writeScalarPropertyValue(jg, ((SimpleType) itemType).getPrimitiveType(), o);
179                }
180            }
181        } else {
182            ListProperty listp = (ListProperty) prop;
183            for (Property p : listp.getChildren()) {
184                writeProperty(jg, p);
185            }
186        }
187        jg.writeEndArray();
188    }
189
190    protected void writeComplexProperty(JsonGenerator jg, Property prop) throws IOException {
191        jg.writeStartObject();
192        for (Property p : prop.getChildren()) {
193            jg.writeFieldName(p.getName());
194            writeProperty(jg, p);
195        }
196        jg.writeEndObject();
197    }
198
199    protected void writeBlobProperty(JsonGenerator jg, Property prop) throws IOException {
200        Blob blob = (Blob) prop.getValue();
201        if (blob == null) {
202            jg.writeNull();
203            return;
204        }
205        jg.writeStartObject();
206        String v = blob.getFilename();
207        if (v == null) {
208            jg.writeNullField("name");
209        } else {
210            jg.writeStringField("name", v);
211        }
212        v = blob.getMimeType();
213        if (v == null) {
214            jg.writeNullField("mime-type");
215        } else {
216            jg.writeStringField("mime-type", v);
217        }
218        v = blob.getEncoding();
219        if (v == null) {
220            jg.writeNullField("encoding");
221        } else {
222            jg.writeStringField("encoding", v);
223        }
224        v = blob.getDigestAlgorithm();
225        if (v == null) {
226            jg.writeNullField("digestAlgorithm");
227        } else {
228            jg.writeStringField("digestAlgorithm", v);
229        }
230        v = blob.getDigest();
231        if (v == null) {
232            jg.writeNullField("digest");
233        } else {
234            jg.writeStringField("digest", v);
235        }
236        jg.writeStringField("length", Long.toString(blob.getLength()));
237
238        String blobUrl = getBlobUrl(prop);
239        if (blobUrl == null) {
240            blobUrl = "";
241        }
242        jg.writeStringField("data", blobUrl);
243        jg.writeEndObject();
244    }
245
246    /**
247     * Gets the full URL of where a blob can be downloaded.
248     *
249     * @since 7.2
250     */
251    private String getBlobUrl(Property prop) {
252        DocumentModel doc = ctx.getParameter(ENTITY_TYPE);
253        if (doc == null) {
254            return "";
255        }
256        DownloadService downloadService = Framework.getService(DownloadService.class);
257        String xpath = prop.getSchema().getName() + ":" + ComplexTypeImpl.canonicalXPath(prop.getPath().substring(1));
258        String filename = ((Blob) prop.getValue()).getFilename();
259        return ctx.getBaseUrl() + downloadService.getDownloadUrl(doc, xpath, filename);
260    }
261
262}