Java tutorial
/** * !(#) J.java * Copyright (c) 2014 DNW Technologies and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * DNW Technologies - initial API and implementation * * Create by manbaum since Oct 11, 2014. */ package com.dnw.json; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.dnw.plugin.util.WeakCache; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; /** * An utility class to make and parse JSON format string. * * @author manbaum * @since Oct 11, 2014 */ public final class J { // using a cache to improve the performance. private final static WeakCache<Class<?>, K<?>> cache = new WeakCache<Class<?>, K<?>>(); private final static Map<Class<?>, K<?>> map = new HashMap<Class<?>, K<?>>(); private static K<Object> defaultConverter; private final static ObjectMapper mapper = new ObjectMapper(); private final static Pattern NAMEPATTERN = Pattern.compile("^[\\x21-\\x7e]+$"); /** * Registers a default type converter. Pass in a <code>null</code> to unregister. * * @author manbaum * @since Oct 14, 2014 * @param converter the default type converter, <code>null</code> for unregister. */ public final static void registerDefaultConverter(K<Object> converter) { defaultConverter = converter; } /** * Returns the current default type converter. * * @author manbaum * @since Oct 14, 2014 * @return current default type converter. */ public final static K<Object> getDefaultConverter() { return defaultConverter; } /** * Registers a type converter. * * @author manbaum * @since Oct 13, 2014 * @param type the type. * @param converter the type converter. * @throws IllegalArgumentException if the type has been registered ever. */ public final static <T> void register(Class<T> type, K<T> converter) { if (type == null) throw new NullPointerException("type.is.null"); if (converter == null) throw new NullPointerException("converter.is.null"); if (map.containsKey(type)) throw new IllegalArgumentException("duplicate.converter: " + type.getName()); map.put(type, converter); } /** * Unregisters a type converter. * * @author manbaum * @since Oct 13, 2014 * @param type the type. */ public final static <T> void unregister(Class<T> type) { if (type == null) throw new NullPointerException("type.is.null"); if (map.containsKey(type)) { map.remove(type); } } /** * Returns the current registered converter for the given type. * * @author manbaum * @since Oct 14, 2014 * @param type the given type. * @return the current registered converter. */ @SuppressWarnings("unchecked") public final static <T> K<T> getConverter(Class<T> type) { return (K<T>) map.get(type); } /** * Checks if a type converter has been registered. * * @author manbaum * @since Oct 13, 2014 * @param type the type. * @return <code>true</code> if it has been registered, else <code>false</code>. */ public final static <T> boolean isRegistered(Class<T> type) { return map.containsKey(type); } /** * Clears type converter registry. * * @author manbaum * @since Oct 13, 2014 */ public final static void clearRegistry() { map.clear(); } /** * Finds the corresponding type converter. * * @author manbaum * @since Oct 14, 2014 * @param type the given type. * @return the corresponding type converter. */ private final static K<?> findConverter(Class<?> type) { K<?> k = cache.get(type); if (k != null) return k; k = map.get(type); if (k != null) { cache.put(type, k); return k; } Class<?> s = type.getSuperclass(); if (s != null) { k = findConverter(s); if (k != null) return k; } for (Class<?> i : type.getInterfaces()) { k = findConverter(i); if (k != null) return k; } return null; } /** * Tries to convert the given value using the registered converter. * * @author manbaum * @since Oct 13, 2014 * @param value the value. * @return the converted value. * @throws IllegalArgumentException if no corresponding converter registered. */ @SuppressWarnings("unchecked") private final static <T> Object tryConverter(Object value) { K<T> k = (K<T>) findConverter(value.getClass()); if (k != null) return k.convert((T) value); if (defaultConverter != null) return defaultConverter.convert(value); throw new IllegalArgumentException("unsupported.type: " + value.getClass()); } /** * Tries to convert the given value to a JSON compatible value. A global internal cache will be * used to improve the performance. * * @author manbaum * @since Oct 11, 2014 * @param value the value to convert. * @return the result value. */ public final static Object convert(Object value) { if (value == null) return value; else if (value instanceof CharSequence) return ((CharSequence) value).toString(); else if (value instanceof Character) return value.toString(); else if (value instanceof Number) return value; else if (value instanceof Boolean) return value; else if (value instanceof M) return ((M) value).map; else if (value instanceof Map) { Map<?, ?> src = (Map<?, ?>) value; Map<String, Object> map = new HashMap<String, Object>(); for (Map.Entry<?, ?> e : src.entrySet()) { String key = String.valueOf(e.getKey()); map.put(key, convert(e.getValue())); } return map; } else if (value instanceof L) return ((L) value).list; else if (value instanceof Iterable) { Iterable<?> src = (List<?>) value; List<Object> list = new ArrayList<Object>(); for (Object v : src) { list.add(convert(v)); } return list; } else if (value.getClass().isArray()) { List<Object> list = new ArrayList<Object>(); int length = Array.getLength(value); for (int i = 0; i < length; i++) { list.add(convert(Array.get(value, i))); } return list; } else return tryConverter(value); } /** * Clears the global internal converter cache, i.e. after modified the converter registry. * * @author manbaum * @since Oct 24, 2014 */ public final static void clearConverterCache() { cache.clear(); } /** * Appends the given name to the string buffer, a name usually used as an object key. * * @author manbaum * @since Oct 11, 2014 * @param sb the string buffer. * @param name the name to append. * @throws IllegalArgumentException if it's not a valid name. */ private final static void emitName(final StringBuffer sb, final String name) { Matcher m = NAMEPATTERN.matcher(name); if (!m.matches()) throw new IllegalArgumentException("not.a.valid.name"); sb.append(name); } /** * Escapes each character to make the string can be denoted as a JSON string. * * @author manbaum * @since Oct 11, 2014 * @param text a string to have all characters to be escaped. * @return the escaped string. */ private final static String escape(final CharSequence text) { final StringBuffer sb = new StringBuffer(); for (int i = 0; i < text.length(); i++) { char ch = text.charAt(i); if (ch == 0) { sb.append("\\0"); } else if (ch == '\n') { sb.append("\\n"); } else if (ch == '\r') { sb.append("\\r"); } else if (ch < 32) { sb.append("\\x"); if (ch < 16) { sb.append('0'); } sb.append(Integer.toHexString(ch)); } else if (ch == '\\' || ch == '\'' || ch == '\"') { sb.append("\\"); sb.append(ch); } else if (ch <= 126) { sb.append(ch); } else { int n = Character.codePointAt(text, i); sb.append("\\u"); if (n < 16) { sb.append("000"); } else if (n < 256) { sb.append("00"); } else if (n < 4096) { sb.append("0"); } sb.append(Integer.toHexString(n)); } } return sb.toString(); } /** * Appends the given string to the string buffer. * * @author manbaum * @since Oct 11, 2014 * @param sb the string buffer. * @param text the string to append. */ private final static void emitString(final StringBuffer sb, final CharSequence text) { sb.append('\"'); sb.append(J.escape(text)); sb.append('\"'); } /** * Appends the given <code>java.util.Map</code> to the string buffer. * * @author manbaum * @since Oct 11, 2014 * @param sb the string buffer. * @param map the map to append. */ private final static void emitMap(final StringBuffer sb, final Map<?, ?> map) { sb.append('{'); boolean first = true; for (Map.Entry<?, ?> e : map.entrySet()) { if (first) { first = false; } else { sb.append(','); } sb.append('\"'); J.emitName(sb, String.valueOf(e.getKey())); sb.append("\":"); J.emit(sb, e.getValue()); } sb.append('}'); } /** * Appends the given <code>java.lang.Iterable</code> to the string buffer. * * @author manbaum * @since Oct 13, 2014 * @param sb the string buffer. * @param collection the iterable collection to append. */ private final static void emitIterable(final StringBuffer sb, final Iterable<?> collection) { sb.append('['); boolean first = true; for (Object v : collection) { if (first) { first = false; } else { sb.append(','); } J.emit(sb, v); } sb.append(']'); } /** * Appends the given array to the string buffer. * * @author manbaum * @since Oct 11, 2014 * @param sb the string buffer. * @param array the array to append. */ private final static void emitArray(final StringBuffer sb, final Object array) { sb.append('['); int length = Array.getLength(array); for (int i = 0; i < length; i++) { if (i > 0) { sb.append(','); } J.emit(sb, Array.get(array, i)); } sb.append(']'); } /** * <p> * Appends the given object to the string buffer. * </p> * <p> * This method can correctly recognize the following values: * <ul> * <li>Value of <code>null</code>, denoted as JavaScript literal <code>null</code>.</li> * <li>Values in class <code>boolean</code>, <code>java.lang.Boolean</code>, denoted as * JavaScrpt boolean literal <code>true</code> or <code>false</code>.</li> * <li>Values in class <code>byte</code>, <code>short</code>, <code>int</code>, * <code>long</code>, <code>float</code>, <code>double</code>, or in their box types, or in * class derived from <code>java.lang.Number</code>, like <code>java.math.BigInteger</code> or * <code>java.math.BigDecimal</code>, denoted as JavaScript numeric literal.</li> * <li>Values in class <code>char</code>, <code>java.lang.Character</code>, * <code>java.lang.String</code>, or in class derived from <code>java.lang.CharSequence</code> , * denoted as JavaScript string literal, make character escape if need.</li> * <li>Values in class <code>com.dnw.json.M</code>, or in class derived from * <code>java.util.Map</code>, denoted as JavaScript object literal.</li> * <li>Values in class <code>com.dnw.json.L</code>, or in class derived from * <code>java.lang.Iterable</code>, denoted as JavaScript array literal.</li> * <li>Values of array with elements in any class, denoted as JavaScript array literal.</li> * </ul> * </p> * <p> * All values in other types, which are not listed above, are denoted as JavaScript string * literal <code>'[Object <qualified type name>]'</code>, e.g. * <code>'[Object java.lang.Object]'</code>. * </p> * * @author manbaum * @since Oct 11, 2014 * @param sb the string buffer. * @param value the object to be append. */ public final static void emit(final StringBuffer sb, final Object value) { if (value == null) { sb.append("null"); } else if (value instanceof CharSequence) { J.emitString(sb, (CharSequence) value); } else if (value instanceof Character) { J.emitString(sb, String.valueOf(value)); } else if (value instanceof Number) { sb.append(String.valueOf(value)); } else if (value instanceof Boolean) { sb.append(String.valueOf(value)); } else if (value instanceof M) { J.emitMap(sb, ((M) value).map); } else if (value instanceof Map) { J.emitMap(sb, (Map<?, ?>) value); } else if (value instanceof L) { J.emitIterable(sb, ((L) value).list); } else if (value instanceof Iterable) { J.emitIterable(sb, (Iterable<?>) value); } else if (value.getClass().isArray()) { J.emitArray(sb, value); } else { sb.append("\"[Object "); sb.append(value.getClass().getName()); sb.append("]\""); } } /** * Returns a corresponding JSON string of the given Java object. * * @author manbaum * @since Oct 24, 2014 * @param value the given Java object. * @return a JSON string. */ public final static String make(Object value) { StringBuffer sb = new StringBuffer(); emit(sb, value); return sb.toString(); } /** * Resolves an object <code>JsonNode</code>, returns a <code>com.dnw.json.M</code> object. For * each value of key-value pair, recursively calls <code>parse(JsonNode)</code> to resolve it. * * @author manbaum * @since Oct 22, 2014 * @param node the given <code>JsonNode</code>. * @return a <code>com.dnw.json.M</code> object. */ private final static M resolveObject(JsonNode node) { M m = M.m(); Iterator<String> i = node.fieldNames(); while (i.hasNext()) { String key = i.next(); m.a(key, resolve(node.get(key))); } return m; } /** * Resolves an array <code>JsonNode</code>, returns a <code>com.dnw.json.L</code> object. For * each element in this array, recursively calls <code>parse(JsonNode)</code> to resolve it. * * @author manbaum * @since Oct 22, 2014 * @param node the given <code>JsonNode</code>. * @return a <code>com.dnw.json.L</code> object. */ private final static L resolveArray(JsonNode node) { L l = L.l(); Iterator<JsonNode> i = node.elements(); while (i.hasNext()) { l.a(resolve(i.next())); } return l; } /** * Resolves a JsonNode, returns the corresponding Java object. * * @author manbaum * @since Oct 22, 2014 * @param node the given <code>JsonNode</code>. * @return the corresponding Java object. */ public final static Object resolve(JsonNode node) { if (node.isNull()) { return null; } else if (node.isTextual()) { return node.asText(); } else if (node.isIntegralNumber()) { return node.asLong(); } else if (node.isDouble()) { return node.asDouble(); } else if (node.isBoolean()) { return node.asBoolean(); } else if (node.isObject()) { return resolveObject(node); } else if (node.isArray()) { return resolveArray(node); } return null; } /** * <p> * Parses a JSON string, returns a Java object to represent the data. * </p> * <p> * <ul> * <li>For JavaScript literal <code>null</code>, returns a <code>null</code>.</li> * <li>For JavaScript literal string, returns a corresponding <code>java.lang.String</code> * object.</li> * <li>For JavaScript literal integral number, returns a corresponding * <code>java.lang.Long</code> object.</li> * <li>For JavaScript literal decimal number, returns a corresponding * <code>java.lang.Double</code> object.</li> * <li>For JavaScript literal boolean value, returns a corresponding * <code>java.lang.Boolean</code> object.</li> * <li>For JavaScript literal object, returns a corresponding <code>com.dnw.json.M</code> * object.</li> * <li>For JavaScript literal array, returns a corresponding <code>com.dnw.json.L</code> object. * </li> * </ul> * </p> * <p> * N.B. The method catches all checked exceptions happened during parsing, and returns a * <code>null</code>. * </p> * * @author manbaum * @since Oct 22, 2014 * @param jsonData a string represented JSON data. * @return an object represents the JSON data. */ public final static Object parse(String jsonData) { try { JsonNode node = mapper.readTree(jsonData); return resolve(node); } catch (JsonProcessingException e) { return null; } catch (IOException e) { return null; } } /** * Method parse. * * @author manbaum * @since Oct 24, 2014 * @param stream * @return */ public final static Object parse(InputStream stream) { try { JsonNode node = mapper.readTree(stream); return resolve(node); } catch (JsonProcessingException e) { return null; } catch (IOException e) { return null; } } }