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}