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}