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