package org.myrobotlab.codec; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Set; import org.apache.commons.codec.binary.Base64; import org.myrobotlab.framework.MRLListener; import org.myrobotlab.framework.Message; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.slf4j.Logger; import com.google.gson.Gson; import com.google.gson.GsonBuilder; /** * handles all encoding and decoding of MRL messages or api(s) assumed context - * services can add an assumed context as a prefix * /api/returnEncoding/inputEncoding/service/method/param1/param2/ ... * * xmpp for example assumes (/api/string/gson)/service/method/param1/param2/ ... * * scheme = alpha *( alpha | digit | "+" | "-" | "." ) Components of all URIs: [ * <scheme>:]<scheme-specific-part>[#<fragment>] * http://stackoverflow.com/questions/3641722/valid-characters-for-uri-schemes * * branch API test 5 */ public class CodecUtils { public final static Logger log = LoggerFactory.getLogger(CodecUtils.class); // uri schemes public final static String SCHEME_MRL = "mrl"; public final static String SCHEME_BASE64 = "base64"; public final static String API_TYPE_MESSAGES = "messages"; public final static String API_TYPE_SERVICES = "services"; // TODO change to mime-type public final static String TYPE_MESSAGES = "messages"; public final static String TYPE_JSON = "json"; public final static String TYPE_URI = "uri"; // mime-types public final static String MIME_TYPE_JSON = "application/json"; public final static String MIME_TYPE_MESSAGES = "application/mrl-json"; // disableHtmlEscaping to prevent encoding or "=" - // private transient static Gson gson = new // GsonBuilder().setDateFormat("yyyy-MM-dd // HH:mm:ss.SSS").setPrettyPrinting().disableHtmlEscaping().create(); private transient static Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss.SSS").setPrettyPrinting().disableHtmlEscaping().create(); // FIXME - switch to Jackson private static boolean initialized = false; public final static String PREFIX_API = "api"; public final static String makeFullTypeName(String type) { if (type == null) { return null; } if (!type.contains(".")) { return String.format("org.myrobotlab.service.%s", type); } return type; } public static final Set<Class<?>> WRAPPER_TYPES = new HashSet<Class<?>>( Arrays.asList(Boolean.class, Character.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, Void.class)); public static final Set<String> WRAPPER_TYPES_CANONICAL = new HashSet<String>( Arrays.asList(Boolean.class.getCanonicalName(), Character.class.getCanonicalName(), Byte.class.getCanonicalName(), Short.class.getCanonicalName(), Integer.class.getCanonicalName(), Long.class.getCanonicalName(), Float.class.getCanonicalName(), Double.class.getCanonicalName(), Void.class.getCanonicalName())); final static HashMap<String, Method> methodCache = new HashMap<String, Method>(); /** * a method signature map based on name and number of methods - the String[] * will be the keys into the methodCache A method key is generated by input * from some encoded protocol - the method key is object name + method name + * parameter number - this returns a full method signature key which is used * to look up the method in the methodCache */ final static HashMap<String, ArrayList<Method>> methodOrdinal = new HashMap<String, ArrayList<Method>>(); final static HashSet<String> objectsCached = new HashSet<String>(); final static HashMap<String, String> keyToMimeType = new HashMap<String, String>(); public static final Message base64ToMsg(String base64) { String data = base64; if (base64.startsWith(String.format("%s://", SCHEME_BASE64))) { data = base64.substring(SCHEME_BASE64.length() + 3); } final ByteArrayInputStream dataStream = new ByteArrayInputStream(Base64.decodeBase64(data)); try { final ObjectInputStream objectStream = new ObjectInputStream(dataStream); Message msg = (Message) objectStream.readObject(); return msg; } catch (Exception e) { Logging.logError(e); return null; } } public static final String capitalize(final String line) { return Character.toUpperCase(line.charAt(0)) + line.substring(1); } public final static <T extends Object> T fromJson(String json, Class<T> clazz) { return gson.fromJson(json, clazz); } static public final byte[] getBytes(Object o) throws IOException { ByteArrayOutputStream byteStream = new ByteArrayOutputStream(5000); ObjectOutputStream os = new ObjectOutputStream(new BufferedOutputStream(byteStream)); os.flush(); os.writeObject(o); os.flush(); return byteStream.toByteArray(); } static public final String getCallBackName(String topicMethod) { // replacements if (topicMethod.startsWith("publish")) { return String.format("on%s", capitalize(topicMethod.substring("publish".length()))); } else if (topicMethod.startsWith("get")) { return String.format("on%s", capitalize(topicMethod.substring("get".length()))); } // no replacement - just pefix and capitalize // FIXME - subscribe to onMethod --- gets ---> onOnMethod :P return String.format("on%s", capitalize(topicMethod)); } // concentrator data coming from decoder static public Method getMethod(String serviceType, String methodName, Object[] params) { return getMethod("org.myrobotlab.service", serviceType, methodName, params); } // real encoded data ??? getMethodFromXML getMethodFromJson - all resolve to // this getMethod with class form // encoded data.. YA ! static public Method getMethod(String pkgName, String objectName, String methodName, Object[] params) { String fullObjectName = String.format("%s.%s", pkgName, objectName); log.debug("Full Object Name : {}", fullObjectName); return null; } static public ArrayList<Method> getMethodCandidates(String serviceType, String methodName, int paramCount) { if (!objectsCached.contains(serviceType)) { loadObjectCache(serviceType); } String ordinalKey = makeMethodOrdinalKey(serviceType, methodName, paramCount); if (!methodOrdinal.containsKey(ordinalKey)) { log.error(String.format("cant find matching method candidate for %s.%s %d params", serviceType, methodName, paramCount)); return null; } return methodOrdinal.get(ordinalKey); } // TODO // public static Object encode(Object, encoding) - dispatches appropriately static final public String getMsgKey(Message msg) { return String.format("msg %s.%s --> %s.%s(%s) - %d", msg.sender, msg.sendingMethod, msg.name, msg.method, CodecUtils.getParameterSignature(msg.data), msg.msgId); } static final public String getMsgTypeKey(Message msg) { return String.format("msg %s.%s --> %s.%s(%s)", msg.sender, msg.sendingMethod, msg.name, msg.method, CodecUtils.getParameterSignature(msg.data)); } static final public String getParameterSignature(final Object[] data) { if (data == null) { return ""; } StringBuffer ret = new StringBuffer(); for (int i = 0; i < data.length; ++i) { if (data[i] != null) { Class<?> c = data[i].getClass(); // not all data types are safe // toString() e.g. // SerializableImage if (c == String.class || c == Integer.class || c == Boolean.class || c == Float.class || c == MRLListener.class) { ret.append(data[i].toString()); } else { String type = data[i].getClass().getCanonicalName(); String shortTypeName = type.substring(type.lastIndexOf(".") + 1); ret.append(shortTypeName); } if (data.length != i + 1) { ret.append(","); } } else { ret.append("null"); } } return ret.toString(); } static public String getServiceType(String inType) { if (inType == null) { return null; } if (inType.contains(".")) { return inType; } return String.format("org.myrobotlab.service.%s", inType); } public static Message gsonToMsg(String gsonData) { return gson.fromJson(gsonData, Message.class); } /** * most lossy protocols need conversion of parameters into correctly typed * elements this method is used to query a candidate method to see if a simple * conversion is possible * * @return */ public static boolean isSimpleType(Class<?> clazz) { return WRAPPER_TYPES.contains(clazz) || clazz == String.class; } public static boolean isWrapper(Class<?> clazz) { return WRAPPER_TYPES.contains(clazz); } public static boolean isWrapper(String className) { return WRAPPER_TYPES_CANONICAL.contains(className); } // FIXME - axis's Method cache - loads only requested methods // this would probably be more gracefull than batch loading as I am doing.. // http://svn.apache.org/repos/asf/webservices/axis/tags/Version1_2RC2/java/src/org/apache/axis/utils/cache/MethodCache.java static public void loadObjectCache(String serviceType) { try { objectsCached.add(serviceType); Class<?> clazz = Class.forName(serviceType); Method[] methods = clazz.getMethods(); for (int i = 0; i < methods.length; ++i) { Method m = methods[i]; Class<?>[] types = m.getParameterTypes(); String ordinalKey = makeMethodOrdinalKey(serviceType, m.getName(), types.length); String methodKey = makeMethodKey(serviceType, m.getName(), types); if (!methodOrdinal.containsKey(ordinalKey)) { ArrayList<Method> keys = new ArrayList<Method>(); keys.add(m); methodOrdinal.put(ordinalKey, keys); } else { methodOrdinal.get(ordinalKey).add(m); } if (log.isDebugEnabled()) { log.debug(String.format("loading %s into method cache", methodKey)); } methodCache.put(methodKey, m); } } catch (Exception e) { Logging.logError(e); } } // FIXME !!! - encoding for Message ----> makeMethodKey(Message msg) static public String makeMethodKey(String fullObjectName, String methodName, Class<?>[] paramTypes) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < paramTypes.length; ++i) { sb.append("/"); sb.append(paramTypes[i].getCanonicalName()); } return String.format("%s/%s%s", fullObjectName, methodName, sb.toString()); } static public String makeMethodOrdinalKey(String fullObjectName, String methodName, int paramCount) { return String.format("%s/%s/%d", fullObjectName, methodName, paramCount); } // LOSSY Encoding (e.g. xml & gson - which do not encode type information) // can possibly // give us the parameter count - from the parameter count we can grab method // candidates // @return is a arraylist of keys !!! public static final String msgToBase64(Message msg) { final ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); try { final ObjectOutputStream objectStream = new ObjectOutputStream(dataStream); objectStream.writeObject(msg); objectStream.close(); dataStream.close(); String base64 = String.format("%s://%s", SCHEME_BASE64, new String(Base64.encodeBase64(dataStream.toByteArray()))); return base64; } catch (Exception e) { log.error(String.format("couldnt seralize %s", msg)); Logging.logError(e); return null; } } public static String msgToGson(Message msg) { return gson.toJson(msg, Message.class); } public static boolean setJSONPrettyPrinting(boolean b) { if (b) { gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss.SSS").setPrettyPrinting().disableHtmlEscaping().create(); } else { gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss.SSS").disableHtmlEscaping().create(); } return b; } // --- xml codec begin ------------------ // inbound parameters are probably strings or xml bits encoded in some way - // need to match // ordinal first static public String toCamelCase(String s) { String[] parts = s.split("_"); String camelCaseString = ""; for (String part : parts) { camelCaseString = camelCaseString + toCCase(part); } return String.format("%s%s", camelCaseString.substring(0, 1).toLowerCase(), camelCaseString.substring(1)); } static public String toCCase(String s) { return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); } public final static String toJson(Object o) { return gson.toJson(o); } public final static String toJson(Object o, Class<?> clazz) { return gson.toJson(o, clazz); } public static void toJsonFile(Object o, String filename) throws IOException { FileOutputStream fos = new FileOutputStream(new File(filename)); fos.write(gson.toJson(o).getBytes()); fos.close(); } // === method signatures begin === static public String toUnderScore(String camelCase) { return toUnderScore(camelCase, false); } static public String toUnderScore(String camelCase, Boolean toLowerCase) { byte[] a = camelCase.getBytes(); boolean lastLetterLower = false; StringBuilder sb = new StringBuilder(); for (int i = 0; i < a.length; ++i) { boolean currentCaseUpper = Character.isUpperCase(a[i]); Character newChar = null; if (toLowerCase != null) { if (toLowerCase) { newChar = (char) Character.toLowerCase(a[i]); } else { newChar = (char) Character.toUpperCase(a[i]); } } else { newChar = (char) a[i]; } sb.append(String.format("%s%c", (lastLetterLower && currentCaseUpper) ? "_" : "", newChar)); lastLetterLower = !currentCaseUpper; } return sb.toString(); } public static boolean tryParseInt(String string) { try { Integer.parseInt(string); return true; } catch (Exception e) { } return false; } public static String type(String type) { int pos0 = type.indexOf("."); if (pos0 > 0) { return type; } return String.format("org.myrobotlab.service.%s", type); } static final String JSON = "application/javascript"; // start fresh :P // FIXME should probably use a object factory and interface vs static methods static public void write(OutputStream out, Object toEncode) throws IOException { write(JSON, out, toEncode); } static public void write(String mimeType, OutputStream out, Object toEncode) throws IOException { if (JSON.equals(mimeType)) { out.write(gson.toJson(toEncode).getBytes()); // out.flush(); } else { log.error(String.format("write mimeType %s not supported", mimeType)); } } public static String getKeyToMimeType(String apiTypeKey) { if (!initialized) { init(); } String ret = MIME_TYPE_MESSAGES; if (keyToMimeType.containsKey(apiTypeKey)) { ret = keyToMimeType.get(apiTypeKey); } return ret; } // API KEY to MIME TYPES (request or response?) private static synchronized void init() { keyToMimeType.put("messages", MIME_TYPE_MESSAGES); keyToMimeType.put("services", MIME_TYPE_JSON); initialized = true; } // === method signatures end === }