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.codehaus.jackson.JsonGenerationException; 027import org.codehaus.jackson.JsonGenerator; 028import org.nuxeo.common.utils.URIUtils; 029import org.nuxeo.ecm.core.api.Blob; 030import org.nuxeo.ecm.core.api.PropertyException; 031import org.nuxeo.ecm.core.api.model.Property; 032import org.nuxeo.ecm.core.api.model.impl.ArrayProperty; 033import org.nuxeo.ecm.core.api.model.impl.ComplexProperty; 034import org.nuxeo.ecm.core.api.model.impl.ListProperty; 035import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty; 036import org.nuxeo.ecm.core.schema.types.ListType; 037import org.nuxeo.ecm.core.schema.types.Type; 038import org.nuxeo.ecm.core.schema.types.primitives.BinaryType; 039import org.nuxeo.ecm.core.schema.types.primitives.BooleanType; 040import org.nuxeo.ecm.core.schema.types.primitives.DateType; 041import org.nuxeo.ecm.core.schema.types.primitives.DoubleType; 042import org.nuxeo.ecm.core.schema.types.primitives.IntegerType; 043import org.nuxeo.ecm.core.schema.types.primitives.LongType; 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) 175 throws PropertyException, JsonGenerationException, IOException { 176 PropertyConsumer fieldNameWriter; 177 if (prefix == null) { 178 fieldNameWriter = (j, p) -> j.writeFieldName(p.getName()); 179 } else { 180 fieldNameWriter = (j, p) -> j.writeFieldName(prefix + ':' + p.getField().getName().getLocalName()); 181 } 182 writeProperty(jg, prop, fieldNameWriter); 183 } 184 185 /** 186 * Converts the value of the given core property to JSON. 187 * 188 * @param fieldNameWriter the field name writer is used to write the field name depending on writer configuration, 189 * this parameter also allows us to handle different cases: field with prefix, field under complex 190 * property, or nothing for arrays and lists 191 */ 192 protected void writeProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter) 193 throws PropertyException, JsonGenerationException, IOException { 194 if (prop.isScalar()) { 195 writeScalarProperty(jg, prop, fieldNameWriter); 196 } else if (prop.isList()) { 197 writeListProperty(jg, prop, fieldNameWriter); 198 } else { 199 if (prop.isPhantom()) { 200 if (writeNull) { 201 fieldNameWriter.accept(jg, prop); 202 jg.writeNull(); 203 } 204 } else if (prop instanceof BlobProperty) { // a blob 205 writeBlobProperty(jg, prop, fieldNameWriter); 206 } else { // a complex property 207 writeMapProperty(jg, (ComplexProperty) prop, fieldNameWriter); 208 } 209 } 210 } 211 212 protected void writeScalarProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter) 213 throws PropertyException, JsonGenerationException, IOException { 214 Type type = prop.getType(); 215 Object v = prop.getValue(); 216 if (v == null) { 217 if (writeNull) { 218 fieldNameWriter.accept(jg, prop); 219 jg.writeNull(); 220 } 221 } else { 222 fieldNameWriter.accept(jg, prop); 223 if (type instanceof BooleanType) { 224 jg.writeBoolean((Boolean) v); 225 } else if (type instanceof LongType) { 226 jg.writeNumber((Long) v); 227 } else if (type instanceof DoubleType) { 228 jg.writeNumber((Double) v); 229 } else if (type instanceof IntegerType) { 230 jg.writeNumber((Integer) v); 231 } else if (type instanceof BinaryType) { 232 jg.writeBinary((byte[]) v); 233 } else if (type instanceof DateType && dateTimeFormat == DateTimeFormat.TIME_IN_MILLIS) { 234 if (v instanceof Date) { 235 jg.writeNumber(((Date) v).getTime()); 236 } else if (v instanceof Calendar) { 237 jg.writeNumber(((Calendar) v).getTimeInMillis()); 238 } else { 239 throw new PropertyException("Unknown class for DateType: " + v.getClass().getName() + ", " + v); 240 } 241 } else { 242 jg.writeString(type.encode(v)); 243 } 244 } 245 } 246 247 protected void writeListProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter) 248 throws PropertyException, JsonGenerationException, IOException { 249 // test if array/list is empty - don't write empty case 250 if (!writeEmpty && (prop == null || (prop instanceof ArrayProperty && prop.getValue() == null) 251 || (prop instanceof ListProperty && prop.getChildren().isEmpty()))) { 252 return; 253 } 254 fieldNameWriter.accept(jg, prop); 255 jg.writeStartArray(); 256 if (prop instanceof ArrayProperty) { 257 Object[] ar = (Object[]) prop.getValue(); 258 if (ar == null) { 259 jg.writeEndArray(); 260 return; 261 } 262 Type type = ((ListType) prop.getType()).getFieldType(); 263 for (Object o : ar) { 264 jg.writeString(type.encode(o)); 265 } 266 } else { 267 for (Property p : prop.getChildren()) { 268 // it's a list of complex object, don't write field names 269 writeProperty(jg, p, PropertyConsumer.nothing()); 270 } 271 } 272 jg.writeEndArray(); 273 } 274 275 protected void writeMapProperty(JsonGenerator jg, ComplexProperty prop, PropertyConsumer fieldNameWriter) 276 throws PropertyException, JsonGenerationException, IOException { 277 if (!writeEmpty && (prop == null || prop.getChildren().isEmpty())) { 278 return; 279 } 280 fieldNameWriter.accept(jg, prop); 281 jg.writeStartObject(); 282 PropertyConsumer childFieldWriter = (j, p) -> j.writeFieldName(p.getName()); 283 for (Property p : prop.getChildren()) { 284 writeProperty(jg, p, childFieldWriter); 285 } 286 jg.writeEndObject(); 287 } 288 289 protected void writeBlobProperty(JsonGenerator jg, Property prop, PropertyConsumer fieldNameWriter) 290 throws PropertyException, JsonGenerationException, IOException { 291 Blob blob = (Blob) prop.getValue(); 292 if (blob == null) { 293 if (writeNull) { 294 fieldNameWriter.accept(jg, prop); 295 jg.writeNull(); 296 } 297 } else { 298 fieldNameWriter.accept(jg, prop); 299 jg.writeStartObject(); 300 String v = blob.getFilename(); 301 if (v == null) { 302 if (writeNull) { 303 jg.writeNullField("name"); 304 } 305 } else { 306 jg.writeStringField("name", v); 307 } 308 v = blob.getMimeType(); 309 if (v == null) { 310 if (writeNull) { 311 jg.writeNullField("mime-type"); 312 } 313 } else { 314 jg.writeStringField("mime-type", v); 315 } 316 v = blob.getEncoding(); 317 if (v == null) { 318 if (writeNull) { 319 jg.writeNullField("encoding"); 320 } 321 } else { 322 jg.writeStringField("encoding", v); 323 } 324 v = blob.getDigest(); 325 if (v == null) { 326 if (writeNull) { 327 jg.writeNullField("digest"); 328 } 329 } else { 330 jg.writeStringField("digest", v); 331 } 332 jg.writeNumberField("length", blob.getLength()); 333 if (filesBaseUrl != null) { 334 jg.writeStringField("data", getBlobUrl(prop, filesBaseUrl)); 335 } 336 jg.writeEndObject(); 337 } 338 } 339 340 /** 341 * Gets the full URL of where a blob can be downloaded. 342 * 343 * @since 5.9.3 344 */ 345 private static String getBlobUrl(Property prop, String filesBaseUrl) 346 throws UnsupportedEncodingException, 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, JsonGenerationException, 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 JsonGenerationException, IOException; 377 378 static PropertyConsumer nothing() { 379 return (jg, prop) -> { 380 }; 381 } 382 383 } 384 385}