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