001/* 002 * (C) Copyright 2006-2015 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.io.UnsupportedEncodingException; 023import java.util.Calendar; 024import java.util.Date; 025 026import org.nuxeo.common.utils.URIUtils; 027import org.nuxeo.ecm.core.api.Blob; 028import org.nuxeo.ecm.core.api.PropertyException; 029import org.nuxeo.ecm.core.api.model.Property; 030import org.nuxeo.ecm.core.api.model.impl.ArrayProperty; 031import org.nuxeo.ecm.core.api.model.impl.ComplexProperty; 032import org.nuxeo.ecm.core.api.model.impl.ListProperty; 033import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty; 034import org.nuxeo.ecm.core.schema.types.ListType; 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.JsonGenerationException; 044import com.fasterxml.jackson.core.JsonGenerator; 045 046/** 047 * Helper to marshaling properties into JSON. 048 * 049 * @since 7.1 050 */ 051public class JSONPropertyWriter { 052 053 /** 054 * The date time format. 055 * 056 * @since 9.1 057 */ 058 protected DateTimeFormat dateTimeFormat = DateTimeFormat.W3C; 059 060 /** 061 * The baseUrl that can be used to locate blob content. 062 * 063 * @since 9.1 064 */ 065 protected String filesBaseUrl; 066 067 /** 068 * The prefix to append to field name. 069 * 070 * @since 9.1 071 */ 072 protected String prefix; 073 074 /** 075 * Whether or not this writer write null values. 076 * 077 * @since 9.1 078 */ 079 protected boolean writeNull = true; 080 081 /** 082 * Whether or not this writer write empty list or object. 083 * 084 * @since 9.1 085 */ 086 protected boolean writeEmpty = true; 087 088 /** 089 * Instantiate a JSONPropertyWriter. 090 */ 091 protected JSONPropertyWriter() { 092 // Default constructor 093 } 094 095 /** 096 * Copy constructor. 097 * 098 * @since 9.1 099 */ 100 protected JSONPropertyWriter(JSONPropertyWriter writer) { 101 this.dateTimeFormat = writer.dateTimeFormat; 102 this.filesBaseUrl = writer.filesBaseUrl; 103 this.prefix = writer.prefix; 104 this.writeNull = writer.writeNull; 105 } 106 107 /** 108 * @return a {@link JSONPropertyWriter} instance with {@link DateTimeFormat#W3C} as date time formatter. 109 * @since 9.1 110 */ 111 public static JSONPropertyWriter create() { 112 return new JSONPropertyWriter(); 113 } 114 115 /** 116 * @return this {@link JSONPropertyWriter} filled with the previous configuration and the input dateTimeFormat. 117 * @since 9.1 118 */ 119 public JSONPropertyWriter dateTimeFormat(DateTimeFormat dateTimeFormat) { 120 this.dateTimeFormat = dateTimeFormat; 121 return this; 122 } 123 124 /** 125 * @param filesBaseUrl the baseUrl that can be used to locate blob content 126 * @return this {@link JSONPropertyWriter} filled with the previous configuration and the input filesBaseUrl. 127 * @since 9.1 128 */ 129 public JSONPropertyWriter filesBaseUrl(String filesBaseUrl) { 130 this.filesBaseUrl = filesBaseUrl; 131 if (this.filesBaseUrl != null && !this.filesBaseUrl.endsWith("/")) { 132 this.filesBaseUrl += "/"; 133 } 134 return this; 135 } 136 137 /** 138 * @param prefix the prefix to append for each property 139 * @return this {@link JSONPropertyWriter} filled with the previous configuration and the input prefix. 140 * @since 9.1 141 */ 142 public JSONPropertyWriter prefix(String prefix) { 143 this.prefix = prefix; 144 return this; 145 } 146 147 /** 148 * @param writeNull whether or not this writer might write null values 149 * @return this {@link JSONPropertyWriter} filled with the previous configuration and the input writeNull value. 150 * @since 9.1 151 */ 152 public JSONPropertyWriter writeNull(boolean writeNull) { 153 this.writeNull = writeNull; 154 return this; 155 } 156 157 /** 158 * @param writeEmpty whether or not this writer might write empty array/list/object 159 * @return this {@link JSONPropertyWriter} filled with the previous configuration and the input writeEmpty value. 160 * @since 9.1 161 */ 162 public JSONPropertyWriter writeEmpty(boolean writeEmpty) { 163 this.writeEmpty = writeEmpty; 164 return this; 165 } 166 167 /** 168 * Converts the value of the given core property to JSON. 169 * <p /> 170 * CAUTION: this method will write the field name to {@link JsonGenerator} with its prefix without writing the start 171 * and the end of object. 172 * 173 * @since 9.1 174 */ 175 public void writeProperty(JsonGenerator jg, Property prop) 176 throws PropertyException, JsonGenerationException, IOException { 177 PropertyConsumer fieldNameWriter; 178 if (prefix == null) { 179 fieldNameWriter = (j, p) -> j.writeFieldName(p.getName()); 180 } else { 181 fieldNameWriter = (j, p) -> j.writeFieldName(prefix + ':' + p.getField().getName().getLocalName()); 182 } 183 writeProperty(jg, prop, fieldNameWriter); 184 } 185 186 /** 187 * Converts the value of the given core property to JSON. 188 * 189 * @param fieldNameWriter the field name writer is used to write the field name depending on writer configuration, 190 * this parameter also allows us to handle different cases: field with prefix, field under complex 191 * property, or nothing for arrays and lists 192 */ 193 protected void writeProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter) 194 throws PropertyException, JsonGenerationException, IOException { 195 if (prop.isScalar()) { 196 writeScalarProperty(jg, prop, fieldNameWriter); 197 } else if (prop.isList()) { 198 writeListProperty(jg, prop, fieldNameWriter); 199 } else { 200 if (prop.isPhantom()) { 201 if (writeNull) { 202 fieldNameWriter.accept(jg, prop); 203 jg.writeNull(); 204 } 205 } else if (prop instanceof BlobProperty) { // a blob 206 writeBlobProperty(jg, prop, fieldNameWriter); 207 } else { // a complex property 208 writeMapProperty(jg, (ComplexProperty) prop, fieldNameWriter); 209 } 210 } 211 } 212 213 protected void writeScalarProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter) 214 throws PropertyException, JsonGenerationException, IOException { 215 Type type = prop.getType(); 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((Long) v); 228 } else if (type instanceof DoubleType) { 229 jg.writeNumber((Double) v); 230 } else if (type instanceof IntegerType) { 231 jg.writeNumber((Integer) v); 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, JsonGenerationException, 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, JsonGenerationException, 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, JsonGenerationException, 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) 347 throws UnsupportedEncodingException, 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, JsonGenerationException, 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 JsonGenerationException, IOException; 378 379 static PropertyConsumer nothing() { 380 return (jg, prop) -> { 381 }; 382 } 383 384 } 385 386}