001/* 002 * (C) Copyright 2006-2013 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 * bstefanescu 018 * vpasquier 019 * slacoin 020 */ 021package org.nuxeo.ecm.automation.io.services.codec; 022 023import java.io.ByteArrayInputStream; 024import java.io.ByteArrayOutputStream; 025import java.io.IOException; 026import java.io.InputStream; 027import java.io.OutputStream; 028import java.util.ArrayList; 029import java.util.Calendar; 030import java.util.Collection; 031import java.util.Date; 032import java.util.HashMap; 033import java.util.Iterator; 034import java.util.LinkedHashMap; 035import java.util.List; 036import java.util.Map; 037 038import org.apache.commons.logging.Log; 039import org.apache.commons.logging.LogFactory; 040import org.nuxeo.ecm.automation.core.operations.business.adapter.BusinessAdapter; 041import org.nuxeo.ecm.core.api.CoreSession; 042import org.nuxeo.ecm.core.api.DataModel; 043import org.nuxeo.ecm.core.api.DocumentModel; 044import org.nuxeo.ecm.core.api.DocumentModelFactory; 045import org.nuxeo.ecm.core.api.IdRef; 046import org.nuxeo.ecm.core.api.adapter.DocumentAdapterDescriptor; 047import org.nuxeo.ecm.core.api.adapter.DocumentAdapterService; 048import org.nuxeo.ecm.core.schema.utils.DateParser; 049import org.nuxeo.runtime.api.Framework; 050 051import com.fasterxml.jackson.core.JsonEncoding; 052import com.fasterxml.jackson.core.JsonFactory; 053import com.fasterxml.jackson.core.JsonGenerator; 054import com.fasterxml.jackson.core.JsonParser; 055import com.fasterxml.jackson.core.JsonToken; 056import com.fasterxml.jackson.databind.JsonNode; 057import com.fasterxml.jackson.databind.ObjectMapper; 058 059/** 060 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 061 */ 062public class ObjectCodecService { 063 064 protected static final Log log = LogFactory.getLog(ObjectCodecService.class); 065 066 protected Map<Class<?>, ObjectCodec<?>> codecs; 067 068 protected Map<String, ObjectCodec<?>> codecsByName; 069 070 protected Map<Class<?>, ObjectCodec<?>> _codecs; 071 072 protected Map<String, ObjectCodec<?>> _codecsByName; 073 074 private JsonFactory jsonFactory; 075 076 public ObjectCodecService(JsonFactory jsonFactory) { 077 this.jsonFactory = jsonFactory; 078 codecs = new HashMap<Class<?>, ObjectCodec<?>>(); 079 codecsByName = new HashMap<String, ObjectCodec<?>>(); 080 init(); 081 } 082 083 protected void init() { 084 new StringCodec().register(this); 085 new DateCodec().register(this); 086 new CalendarCodec().register(this); 087 new BooleanCodec().register(this); 088 new NumberCodec().register(this); 089 } 090 091 public void postInit() { 092 DocumentAdapterCodec.register(this, Framework.getService(DocumentAdapterService.class)); 093 } 094 095 /** 096 * Get all codecs. 097 */ 098 public Collection<ObjectCodec<?>> getCodecs() { 099 return codecs().values(); 100 } 101 102 public synchronized void addCodec(ObjectCodec<?> codec) { 103 codecs.put(codec.getJavaType(), codec); 104 codecsByName.put(codec.getType(), codec); 105 _codecs = null; 106 _codecsByName = null; 107 } 108 109 public synchronized void removeCodec(String name) { 110 ObjectCodec<?> codec = codecsByName.remove(name); 111 if (codec != null) { 112 codecs.remove(codec.getJavaType()); 113 _codecs = null; 114 _codecsByName = null; 115 } 116 } 117 118 public synchronized void removeCodec(Class<?> objectType) { 119 ObjectCodec<?> codec = codecs.remove(objectType); 120 if (codec != null) { 121 codecsByName.remove(codec.getType()); 122 _codecs = null; 123 _codecsByName = null; 124 } 125 } 126 127 public ObjectCodec<?> getCodec(Class<?> objectType) { 128 return codecs().get(objectType); 129 } 130 131 public ObjectCodec<?> getCodec(String name) { 132 return codecsByName().get(name); 133 } 134 135 public Map<Class<?>, ObjectCodec<?>> codecs() { 136 Map<Class<?>, ObjectCodec<?>> cache = _codecs; 137 if (cache == null) { 138 synchronized (this) { 139 _codecs = new HashMap<Class<?>, ObjectCodec<?>>(codecs); 140 cache = _codecs; 141 } 142 } 143 return cache; 144 } 145 146 public Map<String, ObjectCodec<?>> codecsByName() { 147 Map<String, ObjectCodec<?>> cache = _codecsByName; 148 if (cache == null) { 149 synchronized (this) { 150 _codecsByName = new HashMap<String, ObjectCodec<?>>(codecsByName); 151 cache = _codecsByName; 152 } 153 } 154 return cache; 155 } 156 157 public String toString(Object object) throws IOException { 158 return toString(object, false); 159 } 160 161 public String toString(Object object, boolean preetyPrint) throws IOException { 162 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 163 write(baos, object, preetyPrint); 164 return baos.toString("UTF-8"); 165 } 166 167 public void write(OutputStream out, Object object) throws IOException { 168 write(out, object, false); 169 } 170 171 public void write(OutputStream out, Object object, boolean prettyPint) throws IOException { 172 173 JsonGenerator jg = jsonFactory.createJsonGenerator(out, JsonEncoding.UTF8); 174 if (prettyPint) { 175 jg.useDefaultPrettyPrinter(); 176 } 177 write(jg, object); 178 } 179 180 @SuppressWarnings({ "rawtypes", "unchecked" }) 181 public void write(JsonGenerator jg, Object object) throws IOException { 182 if (object == null) { 183 jg.writeStartObject(); 184 jg.writeStringField("entity-type", "null"); 185 jg.writeFieldName("value"); 186 jg.writeNull(); 187 jg.writeEndObject(); 188 } else { 189 Class<?> clazz = object.getClass(); 190 ObjectCodec<?> codec = getCodec(clazz); 191 if (codec == null) { 192 writeGenericObject(jg, clazz, object); 193 } else { 194 jg.writeStartObject(); 195 jg.writeStringField("entity-type", codec.getType()); 196 jg.writeFieldName("value"); 197 ((ObjectCodec) codec).write(jg, object); 198 jg.writeEndObject(); 199 } 200 } 201 jg.flush(); 202 } 203 204 public Object read(String json, CoreSession session) throws IOException, ClassNotFoundException { 205 return read(json, null, session); 206 } 207 208 public Object read(String json, ClassLoader cl, CoreSession session) throws IOException, ClassNotFoundException { 209 ByteArrayInputStream in = new ByteArrayInputStream(json.getBytes()); 210 return read(in, cl, session); 211 } 212 213 public Object read(InputStream in, CoreSession session) throws IOException, ClassNotFoundException { 214 return read(in, null, session); 215 } 216 217 public Object read(InputStream in, ClassLoader cl, CoreSession session) throws IOException, ClassNotFoundException { 218 try (JsonParser jp = jsonFactory.createParser(in)) { 219 return read(jp, cl, session); 220 } 221 } 222 223 public Object read(JsonParser jp, ClassLoader cl, CoreSession session) throws IOException, ClassNotFoundException { 224 JsonToken tok = jp.getCurrentToken(); 225 if (tok == null) { 226 tok = jp.nextToken(); 227 } 228 if (tok == JsonToken.START_OBJECT) { 229 tok = jp.nextToken(); 230 } else if (tok != JsonToken.FIELD_NAME) { 231 throw new IllegalStateException( 232 "Invalid parser state. Current token must be either start_object or field_name"); 233 } 234 String key = jp.getCurrentName(); 235 if (!"entity-type".equals(key)) { 236 throw new IllegalStateException("Invalid parser state. Current field must be 'entity-type'"); 237 } 238 jp.nextToken(); 239 String name = jp.getText(); 240 if (name == null) { 241 throw new IllegalStateException("Invalid stream. Entity-Type is null"); 242 } 243 jp.nextValue(); // move to next value 244 ObjectCodec<?> codec = codecsByName.get(name); 245 if (codec == null) { 246 return readGenericObject(jp, name, cl); 247 } else { 248 return codec.read(jp, session); 249 } 250 } 251 252 public Object readNode(JsonNode node, ClassLoader cl, CoreSession session) throws IOException { 253 // Handle simple scalar types 254 if (node.isNumber()) { 255 return node.numberValue(); 256 } else if (node.isBoolean()) { 257 return node.booleanValue(); 258 } else if (node.isTextual()) { 259 return node.textValue(); 260 } else if (node.isArray()) { 261 List<Object> result = new ArrayList<>(); 262 Iterator<JsonNode> elements = node.elements(); 263 while (elements.hasNext()) { 264 result.add(readNode(elements.next(), cl, session)); 265 } 266 return result; 267 } 268 JsonNode entityTypeNode = node.get("entity-type"); 269 JsonNode valueNode = node.get("value"); 270 if (entityTypeNode != null && entityTypeNode.isTextual()) { 271 String type = entityTypeNode.textValue(); 272 ObjectCodec<?> codec = codecsByName.get(type); 273 // handle structured entity with an explicit type declaration 274 if (valueNode == null) { 275 try (JsonParser jp = jsonFactory.createParser(node.toString())) { 276 if (codec == null) { 277 return readGenericObject(jp, type, cl); 278 } else { 279 return codec.read(jp, session); 280 } 281 } 282 } 283 try (JsonParser valueParser = valueNode.traverse()) { 284 if (valueParser.getCodec() == null) { 285 valueParser.setCodec(new ObjectMapper()); 286 } 287 if (valueParser.getCurrentToken() == null) { 288 valueParser.nextToken(); 289 } 290 if (codec == null) { 291 return readGenericObject(valueParser, type, cl); 292 } else { 293 return codec.read(valueParser, session); 294 } 295 } 296 } 297 // fallback to returning the original json node 298 return node; 299 } 300 301 public Object readNode(JsonNode node, CoreSession session) throws IOException { 302 return readNode(node, null, session); 303 } 304 305 protected final void writeGenericObject(JsonGenerator jg, Class<?> clazz, Object object) throws IOException { 306 jg.writeStartObject(); 307 if (clazz.isPrimitive()) { 308 if (clazz == Boolean.TYPE) { 309 jg.writeStringField("entity-type", "boolean"); 310 jg.writeBooleanField("value", (Boolean) object); 311 } else if (clazz == Double.TYPE || clazz == Float.TYPE) { 312 jg.writeStringField("entity-type", "number"); 313 jg.writeNumberField("value", ((Number) object).doubleValue()); 314 } else if (clazz == Integer.TYPE || clazz == Long.TYPE || clazz == Short.TYPE || clazz == Byte.TYPE) { 315 jg.writeStringField("entity-type", "number"); 316 jg.writeNumberField("value", ((Number) object).longValue()); 317 } else if (clazz == Character.TYPE) { 318 jg.writeStringField("entity-type", "string"); 319 jg.writeStringField("value", object.toString()); 320 } 321 return; 322 } 323 if (jg.getCodec() == null) { 324 jg.setCodec(new ObjectMapper()); 325 } 326 if (object instanceof Iterable && clazz.getName().startsWith("java.")) { 327 jg.writeStringField("entity-type", "list"); 328 } else if (object instanceof Map && clazz.getName().startsWith("java.")) { 329 if (object instanceof LinkedHashMap) { 330 jg.writeStringField("entity-type", "orderedMap"); 331 } else { 332 jg.writeStringField("entity-type", "map"); 333 } 334 } else { 335 jg.writeStringField("entity-type", clazz.getName()); 336 } 337 jg.writeObjectField("value", object); 338 jg.writeEndObject(); 339 } 340 341 protected final Object readGenericObject(JsonParser jp, String name, ClassLoader cl) throws IOException { 342 if (jp.getCodec() == null) { 343 jp.setCodec(new ObjectMapper()); 344 } 345 if ("list".equals(name)) { 346 return jp.readValueAs(ArrayList.class); 347 } else if ("map".equals(name)) { 348 return jp.readValueAs(HashMap.class); 349 } else if ("orderedMap".equals(name)) { 350 return jp.readValueAs(LinkedHashMap.class); 351 } 352 if (cl == null) { 353 cl = Thread.currentThread().getContextClassLoader(); 354 if (cl == null) { 355 cl = ObjectCodecService.class.getClassLoader(); 356 } 357 } 358 Class<?> clazz; 359 try { 360 clazz = cl.loadClass(name); 361 } catch (ClassNotFoundException e) { 362 throw new IOException(e); 363 } 364 return jp.readValueAs(clazz); 365 } 366 367 public static class StringCodec extends ObjectCodec<String> { 368 public StringCodec() { 369 super(String.class); 370 } 371 372 @Override 373 public String getType() { 374 return "string"; 375 } 376 377 @Override 378 public void write(JsonGenerator jg, String value) throws IOException { 379 jg.writeString(value); 380 } 381 382 @Override 383 public String read(JsonParser jp, CoreSession session) throws IOException { 384 return jp.getText(); 385 } 386 387 @Override 388 public boolean isBuiltin() { 389 return true; 390 } 391 392 public void register(ObjectCodecService service) { 393 service.codecs.put(String.class, this); 394 service.codecsByName.put(getType(), this); 395 } 396 } 397 398 public static class DateCodec extends ObjectCodec<Date> { 399 public DateCodec() { 400 super(Date.class); 401 } 402 403 @Override 404 public String getType() { 405 return "date"; 406 } 407 408 @Override 409 public void write(JsonGenerator jg, Date value) throws IOException { 410 jg.writeString(DateParser.formatW3CDateTime(value)); 411 } 412 413 @Override 414 public Date read(JsonParser jp, CoreSession session) throws IOException { 415 return DateParser.parseW3CDateTime(jp.getText()); 416 } 417 418 @Override 419 public boolean isBuiltin() { 420 return true; 421 } 422 423 public void register(ObjectCodecService service) { 424 service.codecs.put(Date.class, this); 425 service.codecsByName.put(getType(), this); 426 } 427 } 428 429 public static class CalendarCodec extends ObjectCodec<Calendar> { 430 public CalendarCodec() { 431 super(Calendar.class); 432 } 433 434 @Override 435 public String getType() { 436 return "date"; 437 } 438 439 @Override 440 public void write(JsonGenerator jg, Calendar value) throws IOException { 441 jg.writeString(DateParser.formatW3CDateTime(value.getTime())); 442 } 443 444 @Override 445 public Calendar read(JsonParser jp, CoreSession session) throws IOException { 446 Calendar c = Calendar.getInstance(); 447 c.setTime(DateParser.parseW3CDateTime(jp.getText())); 448 return c; 449 } 450 451 @Override 452 public boolean isBuiltin() { 453 return true; 454 } 455 456 public void register(ObjectCodecService service) { 457 service.codecs.put(Calendar.class, this); 458 } 459 } 460 461 public static class BooleanCodec extends ObjectCodec<Boolean> { 462 public BooleanCodec() { 463 super(Boolean.class); 464 } 465 466 @Override 467 public String getType() { 468 return "boolean"; 469 } 470 471 @Override 472 public void write(JsonGenerator jg, Boolean value) throws IOException { 473 jg.writeBoolean(value); 474 } 475 476 @Override 477 public Boolean read(JsonParser jp, CoreSession session) throws IOException { 478 return jp.getBooleanValue(); 479 } 480 481 @Override 482 public boolean isBuiltin() { 483 return true; 484 } 485 486 public void register(ObjectCodecService service) { 487 service.codecs.put(Boolean.class, this); 488 service.codecs.put(Boolean.TYPE, this); 489 service.codecsByName.put(getType(), this); 490 } 491 } 492 493 public static class NumberCodec extends ObjectCodec<Number> { 494 public NumberCodec() { 495 super(Number.class); 496 } 497 498 @Override 499 public String getType() { 500 return "number"; 501 } 502 503 @Override 504 public void write(JsonGenerator jg, Number value) throws IOException { 505 Class<?> cl = value.getClass(); 506 if (cl == Double.class || cl == Float.class) { 507 jg.writeNumber(value.doubleValue()); 508 } else { 509 jg.writeNumber(value.longValue()); 510 } 511 } 512 513 @Override 514 public Number read(JsonParser jp, CoreSession session) throws IOException { 515 if (jp.getCurrentToken() == JsonToken.VALUE_NUMBER_FLOAT) { 516 return jp.getDoubleValue(); 517 } else { 518 return jp.getLongValue(); 519 } 520 } 521 522 @Override 523 public boolean isBuiltin() { 524 return true; 525 } 526 527 public void register(ObjectCodecService service) { 528 service.codecs.put(Integer.class, this); 529 service.codecs.put(Integer.TYPE, this); 530 service.codecs.put(Long.class, this); 531 service.codecs.put(Long.TYPE, this); 532 service.codecs.put(Double.class, this); 533 service.codecs.put(Double.TYPE, this); 534 service.codecs.put(Float.class, this); 535 service.codecs.put(Float.TYPE, this); 536 service.codecs.put(Short.class, this); 537 service.codecs.put(Short.TYPE, this); 538 service.codecs.put(Byte.class, this); 539 service.codecs.put(Byte.TYPE, this); 540 service.codecsByName.put(getType(), this); 541 } 542 } 543 544 public static class DocumentAdapterCodec extends ObjectCodec<BusinessAdapter> { 545 546 protected final DocumentAdapterDescriptor descriptor; 547 548 @SuppressWarnings("unchecked") 549 public DocumentAdapterCodec(DocumentAdapterDescriptor descriptor) { 550 super(descriptor.getInterface()); 551 this.descriptor = descriptor; 552 } 553 554 @Override 555 public String getType() { 556 return descriptor.getInterface().getSimpleName(); 557 } 558 559 public static void register(ObjectCodecService service, DocumentAdapterService adapterService) { 560 for (DocumentAdapterDescriptor desc : adapterService.getAdapterDescriptors()) { 561 if (!BusinessAdapter.class.isAssignableFrom(desc.getInterface())) { 562 continue; 563 } 564 DocumentAdapterCodec codec = new DocumentAdapterCodec(desc); 565 if (service.codecsByName.containsKey(codec.getType())) { 566 log.warn("Be careful, you have already contributed an adapter with the same simple name:" 567 + codec.getType()); 568 continue; 569 } 570 service.codecs.put(desc.getInterface(), codec); 571 service.codecsByName.put(codec.getType(), codec); 572 } 573 } 574 575 /** 576 * When the object codec is called the stream is positioned on the first value. For inlined objects this is the 577 * first value after the "entity-type" property. For non inlined objects this will be the object itself (i.e. 578 * '{' or '[') 579 * 580 * @param jp 581 * @return 582 * @throws IOException 583 */ 584 @Override 585 public BusinessAdapter read(JsonParser jp, CoreSession session) throws IOException { 586 if (jp.getCodec() == null) { 587 jp.setCodec(new ObjectMapper()); 588 } 589 BusinessAdapter fromBa = jp.readValueAs(type); 590 DocumentModel doc = fromBa.getId() != null ? session.getDocument(new IdRef(fromBa.getId())) 591 : DocumentModelFactory.createDocumentModel(fromBa.getType()); 592 BusinessAdapter ba = doc.getAdapter(fromBa.getClass()); 593 594 // And finally copy the fields sets from the adapter 595 for (String schema : fromBa.getDocument().getSchemas()) { 596 DataModel dataModel = ba.getDocument().getDataModel(schema); 597 DataModel fromDataModel = fromBa.getDocument().getDataModel(schema); 598 599 for (String field : fromDataModel.getDirtyFields()) { 600 dataModel.setData(field, fromDataModel.getData(field)); 601 } 602 } 603 return ba; 604 } 605 } 606 607}