/* * (C) Copyright 2006-2015 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Bogdan Stefanescu */ package org.nuxeo.ecm.automation.core.util; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.Calendar; import java.util.Date; import org.codehaus.jackson.JsonGenerationException; import org.codehaus.jackson.JsonGenerator; import org.nuxeo.common.utils.URIUtils; import org.nuxeo.ecm.core.api.Blob; import org.nuxeo.ecm.core.api.PropertyException; import org.nuxeo.ecm.core.api.model.Property; import org.nuxeo.ecm.core.api.model.impl.ArrayProperty; import org.nuxeo.ecm.core.api.model.impl.ComplexProperty; import org.nuxeo.ecm.core.api.model.impl.ListProperty; import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty; import org.nuxeo.ecm.core.schema.types.ListType; import org.nuxeo.ecm.core.schema.types.Type; import org.nuxeo.ecm.core.schema.types.primitives.BinaryType; import org.nuxeo.ecm.core.schema.types.primitives.BooleanType; import org.nuxeo.ecm.core.schema.types.primitives.DateType; import org.nuxeo.ecm.core.schema.types.primitives.DoubleType; import org.nuxeo.ecm.core.schema.types.primitives.IntegerType; import org.nuxeo.ecm.core.schema.types.primitives.LongType; /** * Helper to marshaling properties into JSON. * * @since 7.1 */ public class JSONPropertyWriter { /** * The date time format. * * @since 9.1 */ protected DateTimeFormat dateTimeFormat = DateTimeFormat.W3C; /** * The baseUrl that can be used to locate blob content. * * @since 9.1 */ protected String filesBaseUrl; /** * The prefix to append to field name. * * @since 9.1 */ protected String prefix; /** * Whether or not this writer write null values. * * @since 9.1 */ protected boolean writeNull = true; /** * Whether or not this writer write empty list or object. * * @since 9.1 */ protected boolean writeEmpty = true; /** * Instantiate a JSONPropertyWriter. */ protected JSONPropertyWriter() { // Default constructor } /** * Copy constructor. * * @since 9.1 */ protected JSONPropertyWriter(JSONPropertyWriter writer) { this.dateTimeFormat = writer.dateTimeFormat; this.filesBaseUrl = writer.filesBaseUrl; this.prefix = writer.prefix; this.writeNull = writer.writeNull; } /** * @return a {@link JSONPropertyWriter} instance with {@link DateTimeFormat#W3C} as date time formatter. * @since 9.1 */ public static JSONPropertyWriter create() { return new JSONPropertyWriter(); } /** * @return this {@link JSONPropertyWriter} filled with the previous configuration and the input dateTimeFormat. * @since 9.1 */ public JSONPropertyWriter dateTimeFormat(DateTimeFormat dateTimeFormat) { this.dateTimeFormat = dateTimeFormat; return this; } /** * @param filesBaseUrl the baseUrl that can be used to locate blob content * @return this {@link JSONPropertyWriter} filled with the previous configuration and the input filesBaseUrl. * @since 9.1 */ public JSONPropertyWriter filesBaseUrl(String filesBaseUrl) { this.filesBaseUrl = filesBaseUrl; if (this.filesBaseUrl != null && !this.filesBaseUrl.endsWith("/")) { this.filesBaseUrl += "/"; } return this; } /** * @param prefix the prefix to append for each property * @return this {@link JSONPropertyWriter} filled with the previous configuration and the input prefix. * @since 9.1 */ public JSONPropertyWriter prefix(String prefix) { this.prefix = prefix; return this; } /** * @param writeNull whether or not this writer might write null values * @return this {@link JSONPropertyWriter} filled with the previous configuration and the input writeNull value. * @since 9.1 */ public JSONPropertyWriter writeNull(boolean writeNull) { this.writeNull = writeNull; return this; } /** * @param writeEmpty whether or not this writer might write empty array/list/object * @return this {@link JSONPropertyWriter} filled with the previous configuration and the input writeEmpty value. * @since 9.1 */ public JSONPropertyWriter writeEmpty(boolean writeEmpty) { this.writeEmpty = writeEmpty; return this; } /** * Converts the value of the given core property to JSON. * <p /> * CAUTION: this method will write the field name to {@link JsonGenerator} with its prefix without writing the start * and the end of object. * * @since 9.1 */ public void writeProperty(JsonGenerator jg, Property prop) throws PropertyException, JsonGenerationException, IOException { PropertyConsumer fieldNameWriter; if (prefix == null) { fieldNameWriter = (j, p) -> j.writeFieldName(p.getName()); } else { fieldNameWriter = (j, p) -> j.writeFieldName(prefix + ':' + p.getField().getName().getLocalName()); } writeProperty(jg, prop, fieldNameWriter); } /** * Converts the value of the given core property to JSON. * * @param fieldNameWriter the field name writer is used to write the field name depending on writer configuration, * this parameter also allows us to handle different cases: field with prefix, field under complex * property, or nothing for arrays and lists */ protected void writeProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter) throws PropertyException, JsonGenerationException, IOException { if (prop.isScalar()) { writeScalarProperty(jg, prop, fieldNameWriter); } else if (prop.isList()) { writeListProperty(jg, prop, fieldNameWriter); } else { if (prop.isPhantom()) { if (writeNull) { fieldNameWriter.accept(jg, prop); jg.writeNull(); } } else if (prop instanceof BlobProperty) { // a blob writeBlobProperty(jg, prop, fieldNameWriter); } else { // a complex property writeMapProperty(jg, (ComplexProperty) prop, fieldNameWriter); } } } protected void writeScalarProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter) throws PropertyException, JsonGenerationException, IOException { Type type = prop.getType(); Object v = prop.getValue(); if (v == null) { if (writeNull) { fieldNameWriter.accept(jg, prop); jg.writeNull(); } } else { fieldNameWriter.accept(jg, prop); if (type instanceof BooleanType) { jg.writeBoolean((Boolean) v); } else if (type instanceof LongType) { jg.writeNumber((Long) v); } else if (type instanceof DoubleType) { jg.writeNumber((Double) v); } else if (type instanceof IntegerType) { jg.writeNumber((Integer) v); } else if (type instanceof BinaryType) { jg.writeBinary((byte[]) v); } else if (type instanceof DateType && dateTimeFormat == DateTimeFormat.TIME_IN_MILLIS) { if (v instanceof Date) { jg.writeNumber(((Date) v).getTime()); } else if (v instanceof Calendar) { jg.writeNumber(((Calendar) v).getTimeInMillis()); } else { throw new PropertyException("Unknown class for DateType: " + v.getClass().getName() + ", " + v); } } else { jg.writeString(type.encode(v)); } } } protected void writeListProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter) throws PropertyException, JsonGenerationException, IOException { // test if array/list is empty - don't write empty case if (!writeEmpty && (prop == null || (prop instanceof ArrayProperty && prop.getValue() == null) || (prop instanceof ListProperty && prop.getChildren().isEmpty()))) { return; } fieldNameWriter.accept(jg, prop); jg.writeStartArray(); if (prop instanceof ArrayProperty) { Object[] ar = (Object[]) prop.getValue(); if (ar == null) { jg.writeEndArray(); return; } Type type = ((ListType) prop.getType()).getFieldType(); for (Object o : ar) { jg.writeString(type.encode(o)); } } else { for (Property p : prop.getChildren()) { // it's a list of complex object, don't write field names writeProperty(jg, p, PropertyConsumer.nothing()); } } jg.writeEndArray(); } protected void writeMapProperty(JsonGenerator jg, ComplexProperty prop, PropertyConsumer fieldNameWriter) throws PropertyException, JsonGenerationException, IOException { if (!writeEmpty && (prop == null || prop.getChildren().isEmpty())) { return; } fieldNameWriter.accept(jg, prop); jg.writeStartObject(); PropertyConsumer childFieldWriter = (j, p) -> j.writeFieldName(p.getName()); for (Property p : prop.getChildren()) { writeProperty(jg, p, childFieldWriter); } jg.writeEndObject(); } protected void writeBlobProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter) throws PropertyException, JsonGenerationException, IOException { Blob blob = (Blob) prop.getValue(); if (blob == null) { if (writeNull) { fieldNameWriter.accept(jg, prop); jg.writeNull(); } } else { fieldNameWriter.accept(jg, prop); jg.writeStartObject(); String v = blob.getFilename(); if (v == null) { if (writeNull) { jg.writeNullField("name"); } } else { jg.writeStringField("name", v); } v = blob.getMimeType(); if (v == null) { if (writeNull) { jg.writeNullField("mime-type"); } } else { jg.writeStringField("mime-type", v); } v = blob.getEncoding(); if (v == null) { if (writeNull) { jg.writeNullField("encoding"); } } else { jg.writeStringField("encoding", v); } v = blob.getDigest(); if (v == null) { if (writeNull) { jg.writeNullField("digest"); } } else { jg.writeStringField("digest", v); } jg.writeNumberField("length", blob.getLength()); if (filesBaseUrl != null) { jg.writeStringField("data", getBlobUrl(prop, filesBaseUrl)); } jg.writeEndObject(); } } /** * Gets the full URL of where a blob can be downloaded. * * @since 5.9.3 */ private static String getBlobUrl(Property prop, String filesBaseUrl) throws UnsupportedEncodingException, PropertyException { StringBuilder blobUrlBuilder = new StringBuilder(filesBaseUrl); String xpath = prop.getXPath(); if (!xpath.contains(":")) { // if no prefix, use schema name as prefix: xpath = prop.getSchema().getName() + ":" + xpath; } blobUrlBuilder.append(xpath); blobUrlBuilder.append("/"); String filename = ((Blob) prop.getValue()).getFilename(); if (filename != null) { blobUrlBuilder.append(URIUtils.quoteURIPathComponent(filename, true)); } return blobUrlBuilder.toString(); } /** * Converts the value of the given core property to JSON. The given filesBaseUrl is the baseUrl that can be used to * locate blob content and is useful to generate blob URLs. */ public static void writePropertyValue(JsonGenerator jg, Property prop, DateTimeFormat dateTimeFormat, String filesBaseUrl) throws PropertyException, JsonGenerationException, IOException { JSONPropertyWriter writer = create().dateTimeFormat(dateTimeFormat).filesBaseUrl(filesBaseUrl); // as we just want to write property value, give a nothing consumer writer.writeProperty(jg, prop, PropertyConsumer.nothing()); } @FunctionalInterface public interface PropertyConsumer { void accept(JsonGenerator jg, Property prop) throws JsonGenerationException, IOException; static PropertyConsumer nothing() { return (jg, prop) -> { }; } } }