001/* 002 * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the Eclipse Public License v1.0 006 * which accompanies this distribution, and is available at 007 * http://www.eclipse.org/legal/epl-v10.html 008 * 009 * Contributors: 010 * bstefanescu 011 */ 012package org.nuxeo.ecm.automation.client.jaxrs.spi; 013 014import java.io.IOException; 015import java.io.StringWriter; 016import java.lang.reflect.Type; 017import java.util.HashMap; 018import java.util.Map; 019import java.util.Stack; 020import java.util.concurrent.ConcurrentHashMap; 021 022import org.apache.commons.logging.Log; 023import org.apache.commons.logging.LogFactory; 024import org.codehaus.jackson.JsonFactory; 025import org.codehaus.jackson.JsonGenerator; 026import org.codehaus.jackson.JsonNode; 027import org.codehaus.jackson.JsonParser; 028import org.codehaus.jackson.JsonProcessingException; 029import org.codehaus.jackson.JsonToken; 030import org.codehaus.jackson.Version; 031import org.codehaus.jackson.map.DeserializationConfig; 032import org.codehaus.jackson.map.DeserializationContext; 033import org.codehaus.jackson.map.DeserializationProblemHandler; 034import org.codehaus.jackson.map.JsonDeserializer; 035import org.codehaus.jackson.map.ObjectMapper; 036import org.codehaus.jackson.map.annotate.JsonCachable; 037import org.codehaus.jackson.map.deser.BeanDeserializer; 038import org.codehaus.jackson.map.deser.BeanDeserializerModifier; 039import org.codehaus.jackson.map.introspect.BasicBeanDescription; 040import org.codehaus.jackson.map.module.SimpleModule; 041import org.codehaus.jackson.map.type.TypeBindings; 042import org.codehaus.jackson.map.type.TypeFactory; 043import org.codehaus.jackson.map.type.TypeModifier; 044import org.codehaus.jackson.type.JavaType; 045import org.nuxeo.ecm.automation.client.Constants; 046import org.nuxeo.ecm.automation.client.OperationRequest; 047import org.nuxeo.ecm.automation.client.RemoteThrowable; 048import org.nuxeo.ecm.automation.client.jaxrs.impl.AutomationClientActivator; 049import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.BooleanMarshaller; 050import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.DateMarshaller; 051import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.DocumentMarshaller; 052import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.DocumentsMarshaller; 053import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.ExceptionMarshaller; 054import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.LoginMarshaller; 055import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.NumberMarshaller; 056import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.RecordSetMarshaller; 057import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.StringMarshaller; 058import org.nuxeo.ecm.automation.client.jaxrs.util.JsonOperationMarshaller; 059import org.nuxeo.ecm.automation.client.model.OperationDocumentation; 060import org.nuxeo.ecm.automation.client.model.OperationInput; 061import org.nuxeo.ecm.automation.client.model.OperationRegistry; 062import org.nuxeo.ecm.automation.client.model.PropertyMap; 063 064/** 065 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 066 */ 067public class JsonMarshalling { 068 069 protected static final Log log = LogFactory.getLog(JsonMarshalling.class); 070 071 /** 072 * @author matic 073 * @since 5.5 074 */ 075 public static class ThowrableTypeModifier extends TypeModifier { 076 @Override 077 public JavaType modifyType(JavaType type, Type jdkType, TypeBindings context, TypeFactory typeFactory) { 078 Class<?> raw = type.getRawClass(); 079 if (raw.equals(Throwable.class)) { 080 return typeFactory.constructType(RemoteThrowable.class); 081 } 082 return type; 083 } 084 } 085 086 @JsonCachable(false) 087 public static class ThrowableDeserializer extends org.codehaus.jackson.map.deser.ThrowableDeserializer { 088 089 protected Stack<Map<String, JsonNode>> unknownStack = new Stack<>(); 090 091 public ThrowableDeserializer(BeanDeserializer src) { 092 super(src); 093 } 094 095 @Override 096 public Object deserializeFromObject(JsonParser jp, DeserializationContext ctxt) 097 throws IOException, JsonProcessingException { 098 unknownStack.push(new HashMap<String, JsonNode>()); 099 try { 100 RemoteThrowable t = (RemoteThrowable) super.deserializeFromObject(jp, ctxt); 101 t.getOtherNodes().putAll(unknownStack.peek()); 102 return t; 103 } finally { 104 unknownStack.pop(); 105 } 106 } 107 } 108 109 private JsonMarshalling() { 110 } 111 112 protected static JsonFactory factory = newJsonFactory(); 113 114 protected static final Map<String, JsonMarshaller<?>> marshallersByType = new ConcurrentHashMap<String, JsonMarshaller<?>>(); 115 116 protected static final Map<Class<?>, JsonMarshaller<?>> marshallersByJavaType = new ConcurrentHashMap<Class<?>, JsonMarshaller<?>>(); 117 118 public static JsonFactory getFactory() { 119 return factory; 120 } 121 122 public static JsonFactory newJsonFactory() { 123 JsonFactory jf = new JsonFactory(); 124 ObjectMapper oc = new ObjectMapper(jf); 125 final TypeFactory typeFactoryWithModifier = oc.getTypeFactory().withModifier(new ThowrableTypeModifier()); 126 oc.setTypeFactory(typeFactoryWithModifier); 127 oc.getDeserializationConfig().addHandler(new DeserializationProblemHandler() { 128 @Override 129 public boolean handleUnknownProperty(DeserializationContext ctxt, JsonDeserializer<?> deserializer, 130 Object beanOrClass, String propertyName) throws IOException, JsonProcessingException { 131 if (deserializer instanceof ThrowableDeserializer) { 132 JsonParser jp = ctxt.getParser(); 133 JsonNode propertyNode = jp.readValueAsTree(); 134 ((ThrowableDeserializer) deserializer).unknownStack.peek().put(propertyName, propertyNode); 135 return true; 136 } 137 return false; 138 } 139 }); 140 final SimpleModule module = new SimpleModule("automation", Version.unknownVersion()) { 141 142 @Override 143 public void setupModule(SetupContext context) { 144 super.setupModule(context); 145 146 context.addBeanDeserializerModifier(new BeanDeserializerModifier() { 147 148 @Override 149 public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, 150 BasicBeanDescription beanDesc, JsonDeserializer<?> deserializer) { 151 if (!Throwable.class.isAssignableFrom(beanDesc.getBeanClass())) { 152 return super.modifyDeserializer(config, beanDesc, deserializer); 153 } 154 return new ThrowableDeserializer((BeanDeserializer) deserializer); 155 } 156 }); 157 } 158 }; 159 oc.registerModule(module); 160 jf.setCodec(oc); 161 return jf; 162 } 163 164 static { 165 addMarshaller(new DocumentMarshaller()); 166 addMarshaller(new DocumentsMarshaller()); 167 addMarshaller(new ExceptionMarshaller()); 168 addMarshaller(new LoginMarshaller()); 169 addMarshaller(new RecordSetMarshaller()); 170 addMarshaller(new StringMarshaller()); 171 addMarshaller(new BooleanMarshaller()); 172 addMarshaller(new NumberMarshaller()); 173 addMarshaller(new DateMarshaller()); 174 } 175 176 public static void addMarshaller(JsonMarshaller<?> marshaller) { 177 marshallersByType.put(marshaller.getType(), marshaller); 178 marshallersByJavaType.put(marshaller.getJavaType(), marshaller); 179 } 180 181 @SuppressWarnings("unchecked") 182 public static <T> JsonMarshaller<T> getMarshaller(String type) { 183 return (JsonMarshaller<T>) marshallersByType.get(type); 184 } 185 186 @SuppressWarnings("unchecked") 187 public static <T> JsonMarshaller<T> getMarshaller(Class<T> clazz) { 188 return (JsonMarshaller<T>) marshallersByJavaType.get(clazz); 189 } 190 191 public static OperationRegistry readRegistry(String content) throws IOException { 192 HashMap<String, OperationDocumentation> ops = new HashMap<String, OperationDocumentation>(); 193 HashMap<String, OperationDocumentation> chains = new HashMap<String, OperationDocumentation>(); 194 HashMap<String, String> paths = new HashMap<String, String>(); 195 196 JsonParser jp = factory.createJsonParser(content); 197 jp.nextToken(); // start_obj 198 JsonToken tok = jp.nextToken(); 199 while (tok != null && tok != JsonToken.END_OBJECT) { 200 String key = jp.getCurrentName(); 201 if ("operations".equals(key)) { 202 readOperations(jp, ops); 203 } else if ("chains".equals(key)) { 204 readChains(jp, chains); 205 } else if ("paths".equals(key)) { 206 readPaths(jp, paths); 207 } 208 tok = jp.nextToken(); 209 } 210 if (tok == null) { 211 throw new IllegalArgumentException("Unexpected end of stream."); 212 } 213 return new OperationRegistry(paths, ops, chains); 214 } 215 216 private static void readOperations(JsonParser jp, Map<String, OperationDocumentation> ops) throws IOException { 217 jp.nextToken(); // skip [ 218 JsonToken tok = jp.nextToken(); 219 while (tok != null && tok != JsonToken.END_ARRAY) { 220 OperationDocumentation op = JsonOperationMarshaller.read(jp); 221 ops.put(op.id, op); 222 if (op.aliases != null) { 223 for (String alias : op.aliases) { 224 ops.put(alias, op); 225 } 226 } 227 tok = jp.nextToken(); 228 } 229 } 230 231 private static void readChains(JsonParser jp, Map<String, OperationDocumentation> chains) throws IOException { 232 jp.nextToken(); // skip [ 233 JsonToken tok = jp.nextToken(); 234 while (tok != null && tok != JsonToken.END_ARRAY) { 235 OperationDocumentation op = JsonOperationMarshaller.read(jp); 236 chains.put(op.id, op); 237 tok = jp.nextToken(); 238 } 239 } 240 241 private static void readPaths(JsonParser jp, Map<String, String> paths) throws IOException { 242 jp.nextToken(); // skip { 243 JsonToken tok = jp.nextToken(); 244 while (tok != null && tok != JsonToken.END_OBJECT) { 245 jp.nextToken(); 246 paths.put(jp.getCurrentName(), jp.getText()); 247 tok = jp.nextToken(); 248 } 249 if (tok == null) { 250 throw new IllegalArgumentException("Unexpected end of stream."); 251 } 252 253 } 254 255 public static Object readEntity(String content) throws IOException { 256 if (content.length() == 0) { // void response 257 return null; 258 } 259 JsonParser jp = factory.createJsonParser(content); 260 jp.nextToken(); // will return JsonToken.START_OBJECT (verify?) 261 jp.nextToken(); 262 if (!Constants.KEY_ENTITY_TYPE.equals(jp.getText())) { 263 throw new RuntimeException("unuspported respone type. No entity-type key found at top of the object"); 264 } 265 jp.nextToken(); 266 String etype = jp.getText(); 267 JsonMarshaller<?> jm = getMarshaller(etype); 268 if (jm == null) { 269 // fall-back on generic java class loading in case etype matches a 270 // valid class name 271 try { 272 // Introspect bundle context to load marshalling class 273 AutomationClientActivator automationClientActivator = AutomationClientActivator.getInstance(); 274 Class<?> loadClass; 275 // Java mode or OSGi mode 276 if (automationClientActivator == null) { 277 loadClass = Thread.currentThread().getContextClassLoader().loadClass(etype); 278 } else { 279 loadClass = automationClientActivator.getContext().getBundle().loadClass(etype); 280 } 281 ObjectMapper mapper = new ObjectMapper(); 282 jp.nextToken(); // move to next field 283 jp.nextToken(); // value field name 284 jp.nextToken(); // value field content 285 return mapper.readValue(jp, loadClass); 286 } catch (ClassNotFoundException e) { 287 log.warn("No marshaller for " + etype + " and not a valid Java class name either."); 288 jp = factory.createJsonParser(content); 289 return jp.readValueAsTree(); 290 } 291 } 292 return jm.read(jp); 293 } 294 295 public static String writeRequest(OperationRequest req) throws IOException { 296 StringWriter writer = new StringWriter(); 297 Object input = req.getInput(); 298 JsonGenerator jg = factory.createJsonGenerator(writer); 299 jg.writeStartObject(); 300 if (input instanceof OperationInput) { 301 // Custom String serialization 302 OperationInput operationInput = (OperationInput) input; 303 String ref = operationInput.getInputRef(); 304 if (ref != null) { 305 jg.writeStringField("input", ref); 306 } 307 } else if (input != null) { 308 309 JsonMarshaller<?> marshaller = getMarshaller(input.getClass()); 310 if (marshaller != null) { 311 // use the registered marshaller for this type 312 jg.writeFieldName("input"); 313 marshaller.write(jg, input); 314 } else { 315 // fall-back to direct POJO to JSON mapping 316 jg.writeObjectField("input", input); 317 } 318 } 319 jg.writeObjectFieldStart("params"); 320 writeMap(jg, req.getParameters()); 321 jg.writeEndObject(); 322 jg.writeObjectFieldStart("context"); 323 writeMap(jg, req.getContextParameters()); 324 jg.writeEndObject(); 325 jg.writeEndObject(); 326 jg.close(); 327 return writer.toString(); 328 } 329 330 public static void writeMap(JsonGenerator jg, Map<String, Object> map) throws IOException { 331 for (Map.Entry<String, Object> entry : map.entrySet()) { 332 Object param = entry.getValue(); 333 jg.writeFieldName(entry.getKey()); 334 write(jg, param); 335 } 336 } 337 338 public static void write(JsonGenerator jg, Object obj) throws IOException { 339 if (obj != null) { 340 JsonMarshaller<?> marshaller = getMarshaller(obj.getClass()); 341 if (marshaller != null) { 342 try { 343 marshaller.write(jg, obj); 344 } catch (UnsupportedOperationException e) { 345 // Catch this exception to handle builtin marshaller exceptions 346 jg.writeObject(obj); 347 } 348 } else if (obj instanceof String) { 349 jg.writeString((String) obj); 350 } else if (obj instanceof PropertyMap || obj instanceof OperationInput) { 351 jg.writeString(obj.toString()); 352 } else if (obj instanceof Iterable) { 353 jg.writeStartArray(); 354 for (Object object : (Iterable) obj) { 355 write(jg, object); 356 } 357 jg.writeEndArray(); 358 } else if (obj.getClass().isArray()) { 359 jg.writeStartArray(); 360 for (Object object : (Object[]) obj) { 361 write(jg, object); 362 } 363 jg.writeEndArray(); 364 } else { 365 jg.writeObject(obj); 366 } 367 } 368 } 369 370}