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