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