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