/** * Copyright 2008-2009 Dan Pritchett * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * */ package org.addsimplicity.anicetus.io; import java.io.CharArrayReader; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import org.addsimplicity.anicetus.entity.EntityTypeRegistry; import org.addsimplicity.anicetus.entity.GlobalInfo; import org.addsimplicity.anicetus.entity.JsonConstants; import org.codehaus.jackson.JsonFactory; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.JsonParser; import org.codehaus.jackson.map.ObjectMapper; /** * The decoder translates a JSON encoded telemetry into a Java object. In * addition to decoding the telemetry maps, the decoder will also attempt to * decode simple Java beans. It relies upon the application adding packages for * searching for types to detect a bean. If it encounters a type it doesn't * recognize, it decodes it into a TelemetryState which is just a simple map. * * When converting strings into types, the decoder looks for getter methods of * the appropriate name. The return type of the getter is used to drive the * conversion. If it is a primitive, the appropriate primitive converter is * used. If it is not a primitive, it will look for static fromString and * valueOf methods that take String as a single argument. The methods are * searched in that order to provide for custom decoders on Enum types. * * @author Dan Pritchett (driveawedge@yahoo.com) * */ public class JsonDecoder implements TelemetryDecoder { private static final Map<Class<?>, Class<?>> s_primitiveBox = new HashMap<Class<?>, Class<?>>(); static { EntityTypeRegistry.addSearchPackage(GlobalInfo.class.getPackage() .getName()); } static { s_primitiveBox.put(Boolean.TYPE, Boolean.class); s_primitiveBox.put(Character.TYPE, Character.class); s_primitiveBox.put(Byte.TYPE, Byte.class); s_primitiveBox.put(Short.TYPE, Short.class); s_primitiveBox.put(Integer.TYPE, Integer.class); s_primitiveBox.put(Long.TYPE, Long.class); s_primitiveBox.put(Float.TYPE, Float.class); s_primitiveBox.put(Double.TYPE, Double.class); } private final Map<String, Class<? extends Object>> m_typeCache = new HashMap<String, Class<? extends Object>>(); private ExceptionHandler m_exceptionHandler = new SystemErrorExceptionHandler(); /** * Convert a character array that represents a JSON encoded object. The * entire object graph will be decoded and returned as the appropriate root * telemetry artifact. * * @param jsonEncoded * The encoded JSON object as a character array. */ public GlobalInfo decode(char[] jsonEncoded) { CharArrayReader in = new CharArrayReader(jsonEncoded); JsonFactory fact = new JsonFactory(); try { JsonParser parser = fact.createJsonParser(in); ObjectMapper mapper = new ObjectMapper(); JsonNode node = mapper.readTree(parser); Object root = getTypedObject(node); if (root instanceof GlobalInfo) { fillType(node, root); } else { return null; } return (GlobalInfo) root; } catch (IOException ioe) { m_exceptionHandler.exceptionCaught(ioe); return null; } } private void fillType(JsonNode node, Object type) { Iterator<String> names = node.getFieldNames(); while (names.hasNext()) { String name = names.next(); JsonNode field = node.get(name); String pname = EntityTypeRegistry.getPropKey(name); if (pname != null) { name = pname; } if (field.isArray()) { Method adder = getAdderMethod(name, type.getClass()); if (field.get(0).isObject() || adder != null) { for (int e = 0; e < field.size(); e++) { JsonNode ele = field.get(e); Object child = getTypedObject(ele); if (child != null) { fillType(ele, child); try { adder.invoke(type, child); } catch (IllegalAccessException iae) { m_exceptionHandler.exceptionCaught(iae); } catch (InvocationTargetException ite) { m_exceptionHandler.exceptionCaught(ite); } } else { try { adder.invoke(type, ele.getTextValue()); } catch (IllegalAccessException iae) { m_exceptionHandler.exceptionCaught(iae); } catch (InvocationTargetException ite) { m_exceptionHandler.exceptionCaught(ite); } } } } else { String ar[] = new String[field.size()]; for (int e = 0; e < field.size(); e++) { ar[e] = field.get(e).getTextValue(); } if (type instanceof GlobalInfo) { ((GlobalInfo) type).put(name, ar); } else { setTypedProperty(type, name, ar); } } } else if (field.isObject()) { Object value = getTypedObject(field); if (value != null) { fillType(field, value); } else { m_exceptionHandler .exceptionCaught(new MissingPropertyException(name, type)); } if (type instanceof GlobalInfo) { ((GlobalInfo) type).put(name, value); } else { setTypedProperty(type, name, value); } } else { Object value = getTypedValue(name, type.getClass(), field.getTextValue()); if (type instanceof GlobalInfo) { ((GlobalInfo) type).put(name, value); } else { setTypedProperty(type, name, value); } } } } private Method getAdderMethod(String name, Class<?> type) { String mname = makeArraySetter(name); for (Method m : type.getMethods()) { if (mname.equals(m.getName())) { return m; } } return null; } /** * Return the exception handler in effect for the decoder. * * @return the exception handler in effect. */ public ExceptionHandler getExceptionHandler() { return m_exceptionHandler; } private Object getTypedObject(JsonNode node) { JsonNode tnode = node.get(JsonConstants.EntityType); if (tnode == null) { return null; } String tname = tnode.getTextValue(); Class<? extends Object> cls = null; // Have we seen this type before? If so, grab the class. But note that // it may be null. The type cache does negative caching as well. // if (m_typeCache.containsKey(tname)) { cls = m_typeCache.get(tname); } else { // Is this a distinguished name? // cls = EntityTypeRegistry.getClassFromName(tname); if (cls == null) { // Okay, try the raw name and see if it just resolves. // try { cls = Class.forName(tname); } catch (ClassNotFoundException cfe) { // We can fall out here. Only interested in null. } } // If we didn't get it there, then we have to walk through each // package // and try. // if (cls == null) { for (String p : EntityTypeRegistry.getSearchPackages()) { StringBuffer fqcn = new StringBuffer(p); fqcn.append("."); fqcn.append(tname); try { cls = Class.forName(fqcn.toString()); } catch (ClassNotFoundException cfe) { // Again, looking for null } if (cls != null) { break; } } } // No matter what the answer right here, we cache the result. // m_typeCache.put(tname, cls); } Object result = null; if (cls != null) { try { result = cls.newInstance(); } catch (IllegalAccessException iae) { m_exceptionHandler.exceptionCaught(iae); } catch (InstantiationException ie) { m_exceptionHandler.exceptionCaught(ie); } } else { m_exceptionHandler .exceptionCaught(new ClassNotFoundException(tname)); } return result; } private Object getTypedValue(String name, Class<? extends Object> cls, String value) { try { Method m = cls.getMethod(makeGetter(name), (Class[]) null); Class<?> ret = m.getReturnType(); if (ret.equals(String.class)) { return value; } if (ret.isPrimitive()) { ret = s_primitiveBox.get(ret); } // Okay, now we want to see if we can invoke a converter. Only two // supported: // valueOf(String s) // fromString(String s) // Method conv = null; try { conv = ret.getMethod("fromString", String.class); } catch (NoSuchMethodException nse) { // weird way to say not found... } if (conv == null) { // Here we'll just fall out to the outside exception and return // a // string. // conv = ret.getMethod("valueOf", String.class); } if (conv != null) { try { return conv.invoke(null, value); } catch (InvocationTargetException ite) { m_exceptionHandler.exceptionCaught(ite); } catch (IllegalAccessException ie) { m_exceptionHandler.exceptionCaught(ie); } } return value; } catch (NoSuchMethodException e) { return value; } } private String makeArraySetter(String name) { StringBuilder sb = new StringBuilder("add"); sb.append(name.substring(0, 1).toUpperCase()); sb.append(name.substring(1)); return sb.toString(); } private String makeGetter(String name) { StringBuilder sb = new StringBuilder("get"); sb.append(name.substring(0, 1).toUpperCase()); sb.append(name.substring(1)); return sb.toString(); } /** * Set the exception handler that will receive any exception that occurs * during decoding. * * @param exceptionHandler * The exception handler. */ public void setExceptionHandler(ExceptionHandler exceptionHandler) { m_exceptionHandler = exceptionHandler; } private void setTypedProperty(Object type, String name, Object value) { String setter = "set" + name.substring(0, 1).toUpperCase() + name.substring(1); Class<?> cls = type.getClass(); try { Method m = cls.getMethod(setter, value.getClass()); m.invoke(type, value); } catch (NoSuchMethodException nsme) { // Again, what could we do? } catch (InvocationTargetException ite) { // Ignore } catch (IllegalAccessException iae) { // Ignore } } }