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     *
169     * @implNote CAUTION: this method will write the field name to {@link JsonGenerator} with its prefix without writing
170     *           the start and the end of object.
171     * @since 9.1
172     */
173    public void writeProperty(JsonGenerator jg, Property prop) throws PropertyException, IOException {
174        PropertyConsumer fieldNameWriter;
175        if (prefix == null) {
176            fieldNameWriter = (j, p) -> j.writeFieldName(p.getName());
177        } else {
178            fieldNameWriter = (j, p) -> j.writeFieldName(prefix + ':' + p.getField().getName().getLocalName());
179        }
180        writeProperty(jg, prop, fieldNameWriter);
181    }
182
183    /**
184     * Converts the value of the given core property to JSON.
185     *
186     * @param fieldNameWriter the field name writer is used to write the field name depending on writer configuration,
187     *            this parameter also allows us to handle different cases: field with prefix, field under complex
188     *            property, or nothing for arrays and lists
189     */
190    protected void writeProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter)
191            throws PropertyException, IOException {
192        if (prop.isScalar()) {
193            writeScalarProperty(jg, prop, fieldNameWriter);
194        } else if (prop.isList()) {
195            writeListProperty(jg, prop, fieldNameWriter);
196        } else {
197            if (prop.isPhantom()) {
198                if (writeNull) {
199                    fieldNameWriter.accept(jg, prop);
200                    jg.writeNull();
201                }
202            } else if (prop instanceof BlobProperty) { // a blob
203                writeBlobProperty(jg, prop, fieldNameWriter);
204            } else { // a complex property
205                writeMapProperty(jg, (ComplexProperty) prop, fieldNameWriter);
206            }
207        }
208    }
209
210    protected void writeScalarProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter)
211            throws PropertyException, IOException {
212        Type type = prop.getType();
213        if (type instanceof SimpleType) {
214            type = ((SimpleType) type).getPrimitiveType();
215        }
216        Object v = prop.getValue();
217        if (v == null) {
218            if (writeNull) {
219                fieldNameWriter.accept(jg, prop);
220                jg.writeNull();
221            }
222        } else {
223            fieldNameWriter.accept(jg, prop);
224            if (type instanceof BooleanType) {
225                jg.writeBoolean((Boolean) v);
226            } else if (type instanceof LongType) {
227                jg.writeNumber(((Number) v).longValue());
228            } else if (type instanceof DoubleType) {
229                jg.writeNumber(((Number) v).doubleValue());
230            } else if (type instanceof IntegerType) {
231                jg.writeNumber(((Number) v).intValue());
232            } else if (type instanceof BinaryType) {
233                jg.writeBinary((byte[]) v);
234            } else if (type instanceof DateType && dateTimeFormat == DateTimeFormat.TIME_IN_MILLIS) {
235                if (v instanceof Date) {
236                    jg.writeNumber(((Date) v).getTime());
237                } else if (v instanceof Calendar) {
238                    jg.writeNumber(((Calendar) v).getTimeInMillis());
239                } else {
240                    throw new PropertyException("Unknown class for DateType: " + v.getClass().getName() + ", " + v);
241                }
242            } else {
243                jg.writeString(type.encode(v));
244            }
245        }
246    }
247
248    protected void writeListProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter)
249            throws PropertyException, IOException {
250        // test if array/list is empty - don't write empty case
251        if (!writeEmpty && (prop == null || (prop instanceof ArrayProperty && prop.getValue() == null)
252                || (prop instanceof ListProperty && prop.getChildren().isEmpty()))) {
253            return;
254        }
255        fieldNameWriter.accept(jg, prop);
256        jg.writeStartArray();
257        if (prop instanceof ArrayProperty) {
258            Object[] ar = (Object[]) prop.getValue();
259            if (ar == null) {
260                jg.writeEndArray();
261                return;
262            }
263            Type type = ((ListType) prop.getType()).getFieldType();
264            for (Object o : ar) {
265                jg.writeString(type.encode(o));
266            }
267        } else {
268            for (Property p : prop.getChildren()) {
269                // it's a list of complex object, don't write field names
270                writeProperty(jg, p, PropertyConsumer.nothing());
271            }
272        }
273        jg.writeEndArray();
274    }
275
276    protected void writeMapProperty(JsonGenerator jg, ComplexProperty prop, PropertyConsumer fieldNameWriter)
277            throws PropertyException, IOException {
278        if (!writeEmpty && (prop == null || prop.getChildren().isEmpty())) {
279            return;
280        }
281        fieldNameWriter.accept(jg, prop);
282        jg.writeStartObject();
283        PropertyConsumer childFieldWriter = (j, p) -> j.writeFieldName(p.getName());
284        for (Property p : prop.getChildren()) {
285            writeProperty(jg, p, childFieldWriter);
286        }
287        jg.writeEndObject();
288    }
289
290    protected void writeBlobProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter)
291            throws PropertyException, IOException {
292        Blob blob = (Blob) prop.getValue();
293        if (blob == null) {
294            if (writeNull) {
295                fieldNameWriter.accept(jg, prop);
296                jg.writeNull();
297            }
298        } else {
299            fieldNameWriter.accept(jg, prop);
300            jg.writeStartObject();
301            String v = blob.getFilename();
302            if (v == null) {
303                if (writeNull) {
304                    jg.writeNullField("name");
305                }
306            } else {
307                jg.writeStringField("name", v);
308            }
309            v = blob.getMimeType();
310            if (v == null) {
311                if (writeNull) {
312                    jg.writeNullField("mime-type");
313                }
314            } else {
315                jg.writeStringField("mime-type", v);
316            }
317            v = blob.getEncoding();
318            if (v == null) {
319                if (writeNull) {
320                    jg.writeNullField("encoding");
321                }
322            } else {
323                jg.writeStringField("encoding", v);
324            }
325            v = blob.getDigest();
326            if (v == null) {
327                if (writeNull) {
328                    jg.writeNullField("digest");
329                }
330            } else {
331                jg.writeStringField("digest", v);
332            }
333            jg.writeNumberField("length", blob.getLength());
334            if (filesBaseUrl != null) {
335                jg.writeStringField("data", getBlobUrl(prop, filesBaseUrl));
336            }
337            jg.writeEndObject();
338        }
339    }
340
341    /**
342     * Gets the full URL of where a blob can be downloaded.
343     *
344     * @since 5.9.3
345     */
346    private static String getBlobUrl(Property prop, String filesBaseUrl) throws PropertyException {
347        StringBuilder blobUrlBuilder = new StringBuilder(filesBaseUrl);
348        String xpath = prop.getXPath();
349        if (!xpath.contains(":")) {
350            // if no prefix, use schema name as prefix:
351            xpath = prop.getSchema().getName() + ":" + xpath;
352        }
353        blobUrlBuilder.append(xpath);
354        blobUrlBuilder.append("/");
355        String filename = ((Blob) prop.getValue()).getFilename();
356        if (filename != null) {
357            blobUrlBuilder.append(URIUtils.quoteURIPathComponent(filename, true));
358        }
359        return blobUrlBuilder.toString();
360    }
361
362    /**
363     * Converts the value of the given core property to JSON. The given filesBaseUrl is the baseUrl that can be used to
364     * locate blob content and is useful to generate blob URLs.
365     */
366    public static void writePropertyValue(JsonGenerator jg, Property prop, DateTimeFormat dateTimeFormat,
367            String filesBaseUrl) throws PropertyException, IOException {
368        JSONPropertyWriter writer = create().dateTimeFormat(dateTimeFormat).filesBaseUrl(filesBaseUrl);
369        // as we just want to write property value, give a nothing consumer
370        writer.writeProperty(jg, prop, PropertyConsumer.nothing());
371    }
372
373    @FunctionalInterface
374    public interface PropertyConsumer {
375
376        void accept(JsonGenerator jg, Property prop) throws IOException;
377
378        static PropertyConsumer nothing() {
379            return (jg, prop) -> {
380            };
381        }
382
383    }
384
385}