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