001/*
002 * (C) Copyright 2006-2019 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 *    Bogdan Stefanescu
018 */
019package org.nuxeo.ecm.automation.core.util;
020
021import java.io.IOException;
022import java.util.Calendar;
023import java.util.Date;
024
025import org.nuxeo.common.utils.URIUtils;
026import org.nuxeo.ecm.core.api.Blob;
027import org.nuxeo.ecm.core.api.PropertyException;
028import org.nuxeo.ecm.core.api.model.Property;
029import org.nuxeo.ecm.core.api.model.impl.ArrayProperty;
030import org.nuxeo.ecm.core.api.model.impl.ComplexProperty;
031import org.nuxeo.ecm.core.api.model.impl.ListProperty;
032import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty;
033import org.nuxeo.ecm.core.schema.types.ListType;
034import org.nuxeo.ecm.core.schema.types.SimpleType;
035import org.nuxeo.ecm.core.schema.types.Type;
036import org.nuxeo.ecm.core.schema.types.primitives.BinaryType;
037import org.nuxeo.ecm.core.schema.types.primitives.BooleanType;
038import org.nuxeo.ecm.core.schema.types.primitives.DateType;
039import org.nuxeo.ecm.core.schema.types.primitives.DoubleType;
040import org.nuxeo.ecm.core.schema.types.primitives.IntegerType;
041import org.nuxeo.ecm.core.schema.types.primitives.LongType;
042
043import com.fasterxml.jackson.core.JsonGenerator;
044
045/**
046 * Helper to marshaling properties into JSON.
047 *
048 * @since 7.1
049 */
050public class JSONPropertyWriter {
051
052    /**
053     * The date time format.
054     *
055     * @since 9.1
056     */
057    protected DateTimeFormat dateTimeFormat = DateTimeFormat.W3C;
058
059    /**
060     * The baseUrl that can be used to locate blob content.
061     *
062     * @since 9.1
063     */
064    protected String filesBaseUrl;
065
066    /**
067     * The prefix to append to field name.
068     *
069     * @since 9.1
070     */
071    protected String prefix;
072
073    /**
074     * Whether or not this writer write null values.
075     *
076     * @since 9.1
077     */
078    protected boolean writeNull = true;
079
080    /**
081     * Whether or not this writer write empty list or object.
082     *
083     * @since 9.1
084     */
085    protected boolean writeEmpty = true;
086
087    /**
088     * Instantiate a JSONPropertyWriter.
089     */
090    protected JSONPropertyWriter() {
091        // Default constructor
092    }
093
094    /**
095     * Copy constructor.
096     *
097     * @since 9.1
098     */
099    protected JSONPropertyWriter(JSONPropertyWriter writer) {
100        this.dateTimeFormat = writer.dateTimeFormat;
101        this.filesBaseUrl = writer.filesBaseUrl;
102        this.prefix = writer.prefix;
103        this.writeNull = writer.writeNull;
104    }
105
106    /**
107     * @return a {@link JSONPropertyWriter} instance with {@link DateTimeFormat#W3C} as date time formatter.
108     * @since 9.1
109     */
110    public static JSONPropertyWriter create() {
111        return new JSONPropertyWriter();
112    }
113
114    /**
115     * @return this {@link JSONPropertyWriter} filled with the previous configuration and the input dateTimeFormat.
116     * @since 9.1
117     */
118    public JSONPropertyWriter dateTimeFormat(DateTimeFormat dateTimeFormat) {
119        this.dateTimeFormat = dateTimeFormat;
120        return this;
121    }
122
123    /**
124     * @param filesBaseUrl the baseUrl that can be used to locate blob content
125     * @return this {@link JSONPropertyWriter} filled with the previous configuration and the input filesBaseUrl.
126     * @since 9.1
127     */
128    public JSONPropertyWriter filesBaseUrl(String filesBaseUrl) {
129        this.filesBaseUrl = filesBaseUrl;
130        if (this.filesBaseUrl != null && !this.filesBaseUrl.endsWith("/")) {
131            this.filesBaseUrl += "/";
132        }
133        return this;
134    }
135
136    /**
137     * @param prefix the prefix to append for each property
138     * @return this {@link JSONPropertyWriter} filled with the previous configuration and the input prefix.
139     * @since 9.1
140     */
141    public JSONPropertyWriter prefix(String prefix) {
142        this.prefix = prefix;
143        return this;
144    }
145
146    /**
147     * @param writeNull whether or not this writer might write null values
148     * @return this {@link JSONPropertyWriter} filled with the previous configuration and the input writeNull value.
149     * @since 9.1
150     */
151    public JSONPropertyWriter writeNull(boolean writeNull) {
152        this.writeNull = writeNull;
153        return this;
154    }
155
156    /**
157     * @param writeEmpty whether or not this writer might write empty array/list/object
158     * @return this {@link JSONPropertyWriter} filled with the previous configuration and the input writeEmpty value.
159     * @since 9.1
160     */
161    public JSONPropertyWriter writeEmpty(boolean writeEmpty) {
162        this.writeEmpty = writeEmpty;
163        return this;
164    }
165
166    /**
167     * Converts the value of the given core property to JSON.
168     * <p />
169     * CAUTION: this method will write the field name to {@link JsonGenerator} with its prefix without writing the start
170     * and the end of object.
171     *
172     * @since 9.1
173     */
174    public void writeProperty(JsonGenerator jg, Property prop) throws PropertyException, IOException {
175        PropertyConsumer fieldNameWriter;
176        if (prefix == null) {
177            fieldNameWriter = (j, p) -> j.writeFieldName(p.getName());
178        } else {
179            fieldNameWriter = (j, p) -> j.writeFieldName(prefix + ':' + p.getField().getName().getLocalName());
180        }
181        writeProperty(jg, prop, fieldNameWriter);
182    }
183
184    /**
185     * Converts the value of the given core property to JSON.
186     *
187     * @param fieldNameWriter the field name writer is used to write the field name depending on writer configuration,
188     *            this parameter also allows us to handle different cases: field with prefix, field under complex
189     *            property, or nothing for arrays and lists
190     */
191    protected void writeProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter)
192            throws PropertyException, IOException {
193        if (prop.isScalar()) {
194            writeScalarProperty(jg, prop, fieldNameWriter);
195        } else if (prop.isList()) {
196            writeListProperty(jg, prop, fieldNameWriter);
197        } else {
198            if (prop.isPhantom()) {
199                if (writeNull) {
200                    fieldNameWriter.accept(jg, prop);
201                    jg.writeNull();
202                }
203            } else if (prop instanceof BlobProperty) { // a blob
204                writeBlobProperty(jg, prop, fieldNameWriter);
205            } else { // a complex property
206                writeMapProperty(jg, (ComplexProperty) prop, fieldNameWriter);
207            }
208        }
209    }
210
211    protected void writeScalarProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter)
212            throws PropertyException, IOException {
213        Type type = prop.getType();
214        if (type instanceof SimpleType) {
215            type = ((SimpleType) type).getPrimitiveType();
216        }
217        Object v = prop.getValue();
218        if (v == null) {
219            if (writeNull) {
220                fieldNameWriter.accept(jg, prop);
221                jg.writeNull();
222            }
223        } else {
224            fieldNameWriter.accept(jg, prop);
225            if (type instanceof BooleanType) {
226                jg.writeBoolean((Boolean) v);
227            } else if (type instanceof LongType) {
228                jg.writeNumber((Long) v);
229            } else if (type instanceof DoubleType) {
230                jg.writeNumber((Double) v);
231            } else if (type instanceof IntegerType) {
232                jg.writeNumber((Integer) v);
233            } else if (type instanceof BinaryType) {
234                jg.writeBinary((byte[]) v);
235            } else if (type instanceof DateType && dateTimeFormat == DateTimeFormat.TIME_IN_MILLIS) {
236                if (v instanceof Date) {
237                    jg.writeNumber(((Date) v).getTime());
238                } else if (v instanceof Calendar) {
239                    jg.writeNumber(((Calendar) v).getTimeInMillis());
240                } else {
241                    throw new PropertyException("Unknown class for DateType: " + v.getClass().getName() + ", " + v);
242                }
243            } else {
244                jg.writeString(type.encode(v));
245            }
246        }
247    }
248
249    protected void writeListProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter)
250            throws PropertyException, IOException {
251        // test if array/list is empty - don't write empty case
252        if (!writeEmpty && (prop == null || (prop instanceof ArrayProperty && prop.getValue() == null)
253                || (prop instanceof ListProperty && prop.getChildren().isEmpty()))) {
254            return;
255        }
256        fieldNameWriter.accept(jg, prop);
257        jg.writeStartArray();
258        if (prop instanceof ArrayProperty) {
259            Object[] ar = (Object[]) prop.getValue();
260            if (ar == null) {
261                jg.writeEndArray();
262                return;
263            }
264            Type type = ((ListType) prop.getType()).getFieldType();
265            for (Object o : ar) {
266                jg.writeString(type.encode(o));
267            }
268        } else {
269            for (Property p : prop.getChildren()) {
270                // it's a list of complex object, don't write field names
271                writeProperty(jg, p, PropertyConsumer.nothing());
272            }
273        }
274        jg.writeEndArray();
275    }
276
277    protected void writeMapProperty(JsonGenerator jg, ComplexProperty prop, PropertyConsumer fieldNameWriter)
278            throws PropertyException, IOException {
279        if (!writeEmpty && (prop == null || prop.getChildren().isEmpty())) {
280            return;
281        }
282        fieldNameWriter.accept(jg, prop);
283        jg.writeStartObject();
284        PropertyConsumer childFieldWriter = (j, p) -> j.writeFieldName(p.getName());
285        for (Property p : prop.getChildren()) {
286            writeProperty(jg, p, childFieldWriter);
287        }
288        jg.writeEndObject();
289    }
290
291    protected void writeBlobProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter)
292            throws PropertyException, IOException {
293        Blob blob = (Blob) prop.getValue();
294        if (blob == null) {
295            if (writeNull) {
296                fieldNameWriter.accept(jg, prop);
297                jg.writeNull();
298            }
299        } else {
300            fieldNameWriter.accept(jg, prop);
301            jg.writeStartObject();
302            String v = blob.getFilename();
303            if (v == null) {
304                if (writeNull) {
305                    jg.writeNullField("name");
306                }
307            } else {
308                jg.writeStringField("name", v);
309            }
310            v = blob.getMimeType();
311            if (v == null) {
312                if (writeNull) {
313                    jg.writeNullField("mime-type");
314                }
315            } else {
316                jg.writeStringField("mime-type", v);
317            }
318            v = blob.getEncoding();
319            if (v == null) {
320                if (writeNull) {
321                    jg.writeNullField("encoding");
322                }
323            } else {
324                jg.writeStringField("encoding", v);
325            }
326            v = blob.getDigest();
327            if (v == null) {
328                if (writeNull) {
329                    jg.writeNullField("digest");
330                }
331            } else {
332                jg.writeStringField("digest", v);
333            }
334            jg.writeNumberField("length", blob.getLength());
335            if (filesBaseUrl != null) {
336                jg.writeStringField("data", getBlobUrl(prop, filesBaseUrl));
337            }
338            jg.writeEndObject();
339        }
340    }
341
342    /**
343     * Gets the full URL of where a blob can be downloaded.
344     *
345     * @since 5.9.3
346     */
347    private static String getBlobUrl(Property prop, String filesBaseUrl) throws PropertyException {
348        StringBuilder blobUrlBuilder = new StringBuilder(filesBaseUrl);
349        String xpath = prop.getXPath();
350        if (!xpath.contains(":")) {
351            // if no prefix, use schema name as prefix:
352            xpath = prop.getSchema().getName() + ":" + xpath;
353        }
354        blobUrlBuilder.append(xpath);
355        blobUrlBuilder.append("/");
356        String filename = ((Blob) prop.getValue()).getFilename();
357        if (filename != null) {
358            blobUrlBuilder.append(URIUtils.quoteURIPathComponent(filename, true));
359        }
360        return blobUrlBuilder.toString();
361    }
362
363    /**
364     * Converts the value of the given core property to JSON. The given filesBaseUrl is the baseUrl that can be used to
365     * locate blob content and is useful to generate blob URLs.
366     */
367    public static void writePropertyValue(JsonGenerator jg, Property prop, DateTimeFormat dateTimeFormat,
368            String filesBaseUrl) throws PropertyException, IOException {
369        JSONPropertyWriter writer = create().dateTimeFormat(dateTimeFormat).filesBaseUrl(filesBaseUrl);
370        // as we just want to write property value, give a nothing consumer
371        writer.writeProperty(jg, prop, PropertyConsumer.nothing());
372    }
373
374    @FunctionalInterface
375    public interface PropertyConsumer {
376
377        void accept(JsonGenerator jg, Property prop) throws IOException;
378
379        static PropertyConsumer nothing() {
380            return (jg, prop) -> {
381            };
382        }
383
384    }
385
386}