All Downloads are FREE. Search and download functionalities are using the official Maven repository.

aQute.lib.json.JSONCodec Maven / Gradle / Ivy

Go to download

A main program (executable JAR) that will listen to port 29998. At first, it can only answer that it is an Envoy (a limited agent). The only function it supports is installing a -runpath. It will then create a framework + agent and transfer the connection to the just installed agent who will then install the bundles. This JAR is a main command for JPM called bndremote. In JPM, it will start up with debug enabled. This JAR does some highly complicated class loading wizardy to ensure that it does not enforce any constraints on the -runpath.

The newest version!
package aQute.lib.json;

import java.io.EOFException;
import java.io.File;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;

/**
 * This is a simple JSON Coder and Encoder that uses the Java type system to
 * convert data objects to JSON and JSON to (type safe) Java objects. The
 * conversion is very much driven by classes and their public fields. Generic
 * information, when present is taken into account.
 * 

* Usage patterns to encode: * *
 *  JSONCoder codec = new JSONCodec(); // assert "1".equals(
 * codec.enc().to().put(1).toString()); assert "[1,2,3]".equals(
 * codec.enc().to().put(Arrays.asList(1,2,3).toString()); Map m = new HashMap();
 * m.put("a", "A"); assert "{\"a\":\"A\"}".equals(
 * codec.enc().to().put(m).toString()); static class D { public int a; } D d =
 * new D(); d.a = 41; assert "{\"a\":41}".equals(
 * codec.enc().to().put(d).toString());
 * 
* * It is possible to redirect the encoder to another output (default is a * string). See {@link Encoder#to()},{@link Encoder#to(File)}, * {@link Encoder#to(OutputStream)}, {@link Encoder#to(Appendable)}. To reset * the string output call {@link Encoder#to()}. *

* This Codec class can be used in a concurrent environment. The Decoders and * Encoders, however, must only be used in a single thread. *

* Will now use hex for encoding byte arrays */ public class JSONCodec { final static Set keywords = Set.of("abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else", "enum", "exports", "extends", "final", "finally", "float", "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", "module", "native", "new", "package", "private", "protected", "public", "requires", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "try", "var", "void", "volatile", "while", "true", "false", "null", "_", "record", "sealed", "non-sealed", "permits"); public static final String KEYWORD_SUFFIX = "__"; final static String START_CHARACTERSX = "[{\"-0123456789tfn"; final static String START_CHARACTERS_BAD = START_CHARACTERSX + "'TF"; // Handlers private final static WeakHashMap handlers = new WeakHashMap<>(); private static StringHandler sh = new StringHandler(); private static BooleanHandler bh = new BooleanHandler(); private static CharacterHandler ch = new CharacterHandler(); private static CollectionHandler dch = new CollectionHandler(ArrayList.class, Object.class); private static SpecialHandler sph = new SpecialHandler(Pattern.class, null, null); private static DateHandler sdh = new DateHandler(); private static FileHandler fh = new FileHandler(); private static ByteArrayHandler byteh = new ByteArrayHandler(); private static UUIDHandler uuidh = new UUIDHandler(); final AtomicInteger fishy = new AtomicInteger(); boolean ignorenull; Map localHandlers = new ConcurrentHashMap<>(); boolean promiscuous; String startCharacters = START_CHARACTERSX; /** * Create a new Encoder with the state and appropriate API. * * @return an Encoder */ public Encoder enc() { return new Encoder(this); } /** * Create a new Decoder with the state and appropriate API. * * @return a Decoder */ public Decoder dec() { return new Decoder(this); } /* * Work horse encode methods, all encoding ends up here. */ void encode(Encoder app, Object object, Type type, Map visited) throws Exception { // Get the null out of the way if (object == null) { app.append("null"); return; } // If we have no type or the type is Object.class // we take the type of the object itself. Normally types // come from declaration sites (returns, fields, methods, etc) // and contain generic info. if (type == null || type == Object.class) type = object.getClass(); // Dispatch to the handler who knows how to handle the given type. Handler h = getHandler(type, object.getClass()); h.encode(app, object, visited); } /** * This method figures out which handler should handle the type specific * stuff. It returns a handler for each type. If no appropriate handler * exists, it will create one for the given type. There are actually quite a * lot of handlers since Java is not very object oriented. * * @param type * @return a {@code Handler} appropriate for {@code type} * @throws Exception */ Handler getHandler(Type type, Class actual) throws Exception { // Use the local handlers for the common types if exist. Handler h = localHandlers.get(type); if (h != null) return h; // Use the static hard coded handlers for the common types. if (type == String.class) return sh; if (type == Boolean.class || type == boolean.class) return bh; if (type == byte[].class) return byteh; if (Character.class == type || char.class == type) return ch; if (Pattern.class == type) return sph; if (Date.class == type) return sdh; if (File.class == type) return fh; if (UUID.class == type) return uuidh; if (type instanceof GenericArrayType gat) { Type sub = gat.getGenericComponentType(); if (sub == byte.class) return byteh; } synchronized (handlers) { h = handlers.get(type); } if (h != null) return h; if (type instanceof Class clazz) { if (Enum.class.isAssignableFrom(clazz)) h = new EnumHandler(clazz); else if (Iterable.class.isAssignableFrom(clazz)) // A Non Generic // collection h = dch; else if (clazz.isArray()) // Non generic array h = new ArrayHandler(clazz, clazz.getComponentType()); else if (Map.class.isAssignableFrom(clazz)) // A Non Generic map h = new MapHandler(clazz, Object.class, Object.class); else if (Number.class.isAssignableFrom(clazz) || clazz.isPrimitive()) h = new NumberHandler(clazz); else if (Record.class.isAssignableFrom(clazz)) h = new RecordHandler(this, clazz); else { Method valueOf = null; Constructor constructor = null; try { constructor = clazz.getConstructor(String.class); } catch (Exception e) { // Ignore } try { valueOf = clazz.getMethod("valueOf", String.class); } catch (Exception e) { // Ignore } if (constructor != null || valueOf != null) h = new SpecialHandler(clazz, constructor, valueOf); else h = new ObjectHandler(this, clazz); // Hmm, might not be a // data class ... } } else { // We have generic information available // We only support generics on Collection, Map, and arrays if (type instanceof ParameterizedType pt) { Type rawType = pt.getRawType(); if (rawType instanceof Class rawClass) { if (Iterable.class.isAssignableFrom(rawClass)) h = new CollectionHandler(rawClass, pt.getActualTypeArguments()[0]); else if (Map.class.isAssignableFrom(rawClass)) h = new MapHandler(rawClass, pt.getActualTypeArguments()[0], pt.getActualTypeArguments()[1]); else if (Dictionary.class.isAssignableFrom(rawClass)) h = new MapHandler(Hashtable.class, pt.getActualTypeArguments()[0], pt.getActualTypeArguments()[1]); else // // We try to use the rawtype instead. // return getHandler(rawType, null); } } else if (type instanceof GenericArrayType gat) { if (gat.getGenericComponentType() == byte[].class) h = byteh; else h = new ArrayHandler(getRawClass(type), gat.getGenericComponentType()); } else if (type instanceof TypeVariable tv) { if (actual != null) // // We can save ourselves a lot of work if we have // an actual type (the type of the object to encode) // h = getHandler(actual, null); else { Type[] bounds = tv.getBounds(); if (bounds == null || bounds.length == 0) { h = new ObjectHandler(this, Object.class); } else { h = getHandler(bounds[bounds.length - 1], null); } } } else throw new IllegalArgumentException("Found a parameterized type that is not a map or collection"); } synchronized (handlers) { // We might actually have duplicates // but who cares? They should be identical handlers.put(type, h); } return h; } Object decode(Type type, Decoder isr) throws Exception { int c = isr.skipWs(); Handler h; if (type == null || type == Object.class) { // Establish default behavior when we run without // type information switch (c) { case '{' : type = LinkedHashMap.class; break; case '[' : type = ArrayList.class; break; case '\'' : isr.badJSON("Got a single quote ' when a double quote \" should be used"); return parseString(isr); case '"' : return parseString(isr); case 'N' : isr.badJSON("null must not use upper case, got a N"); case 'n' : isr.expect("ull"); return null; case 'T' : isr.badJSON("booleans must not use upper case, got a T"); case 't' : isr.expect("rue"); return true; case 'F' : isr.badJSON("booleans must not use upper case, got a F"); case 'f' : isr.expect("alse"); return false; case '0' : case '1' : case '2' : case '3' : case '4' : case '5' : case '6' : case '7' : case '8' : case '9' : case '-' : return parseNumber(isr); default : throw new IllegalArgumentException("Invalid character at begin of token: " + (char) c); } } h = getHandler(type, null); switch (c) { case '{' : return h.decodeObject(isr); case '[' : return h.decodeArray(isr); case '\'' : isr.badJSON("single quote is not allowed"); case '"' : String string = parseString(isr); return h.decode(isr, string); case 'N' : isr.badJSON("do not use upper case for null"); case 'n' : isr.expect("ull"); return h.decode(isr); case 'T' : isr.badJSON("do not use upper case for booleans"); // fall through case 't' : isr.expect("rue"); return h.decode(isr, Boolean.TRUE); case 'F' : isr.badJSON("do not use upper case for booleans"); case 'f' : isr.expect("alse"); return h.decode(isr, Boolean.FALSE); case '0' : case '1' : case '2' : case '3' : case '4' : case '5' : case '6' : case '7' : case '8' : case '9' : case '-' : return h.decode(isr, parseNumber(isr)); default : throw new IllegalArgumentException("Unexpected character in input stream: " + (char) c); } } protected String parseString(Decoder r) throws Exception { char quote = (char) r.current(); assert r.current() == '"' || (promiscuous && r.current == '\''); int c = r.next(); // skip first " StringBuilder sb = new StringBuilder(); while (c != quote) { if (c < 0 || Character.isISOControl(c)) throw new IllegalArgumentException("JSON strings may not contain control characters: " + r.current()); if (c == '\\') { c = r.read(); switch (c) { case '\'' : r.badJSON("Do not escape single quotes"); // fall through case '"' : case '\\' : case '/' : sb.append((char) c); break; case 'b' : sb.append('\b'); break; case 'f' : sb.append('\f'); break; case 'n' : sb.append('\n'); break; case 'r' : sb.append('\r'); break; case 't' : sb.append('\t'); break; case 'u' : int a3 = hexDigit(r.read()) << 12; int a2 = hexDigit(r.read()) << 8; int a1 = hexDigit(r.read()) << 4; int a0 = hexDigit(r.read()) << 0; c = a3 + a2 + a1 + a0; sb.append((char) c); break; case '\n' : r.badJSON("Do not escape a new line"); break; default : throw new IllegalArgumentException( "The only characters after a backslash are \", \\, b, f, n, r, t, and u but got " + c); } } else sb.append((char) c); c = r.read(); } assert c == quote; r.read(); // skip quote return sb.toString(); } private int hexDigit(int c) throws EOFException { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'A' && c <= 'F') return c - 'A' + 10; if (c >= 'a' && c <= 'f') return c - 'a' + 10; throw new IllegalArgumentException("Invalid hex character: " + c); } private Number parseNumber(Decoder r) throws Exception { StringBuilder sb = new StringBuilder(); boolean d = false; if (r.current() == '-') { sb.append('-'); r.read(); } int c = r.current(); if (c == '0') { sb.append('0'); c = r.read(); } else if (c >= '1' && c <= '9') { sb.append((char) c); c = r.read(); while (c >= '0' && c <= '9') { sb.append((char) c); c = r.read(); } } else throw new IllegalArgumentException("Expected digit"); if (c == '.') { d = true; sb.append('.'); c = r.read(); while (c >= '0' && c <= '9') { sb.append((char) c); c = r.read(); } } if (c == 'e' || c == 'E') { d = true; sb.append('e'); c = r.read(); if (c == '+') { sb.append('+'); c = r.read(); } else if (c == '-') { sb.append('-'); c = r.read(); } while (c >= '0' && c <= '9') { sb.append((char) c); c = r.read(); } } if (d) return Double.parseDouble(sb.toString()); long l = Long.parseLong(sb.toString()); if (l > Integer.MAX_VALUE || l < Integer.MIN_VALUE) return l; return (int) l; } void parseArray(Collection list, Type componentType, Decoder r) throws Exception { assert r.current() == '['; int c = r.next(); while (isStartCharacter(c)) { Object o = decode(componentType, r); list.add(o); c = r.skipWs(); if (c == ']') break; if (c == ',') { c = r.next(); continue; } throw new IllegalArgumentException( "Invalid character in parsing list, expected ] or , but found " + (char) c); } assert r.current() == ']'; r.read(); // skip closing } Class getRawClass(Type type) { if (type instanceof Class ctype) return ctype; if (type instanceof ParameterizedType ptype) return getRawClass(ptype.getRawType()); if (type instanceof GenericArrayType gat) { Type subType = gat.getGenericComponentType(); Class c = getRawClass(subType); return Array.newInstance(c, 0) .getClass(); } throw new IllegalArgumentException( "Does not support generics beyond Parameterized Type and GenericArrayType, got " + type); } /** * Ignore null values in output and input * * @param ignorenull * @return this */ public JSONCodec setIgnorenull(boolean ignorenull) { this.ignorenull = ignorenull; return this; } public boolean isIgnorenull() { return ignorenull; } /** * Add a new local handler */ public JSONCodec addHandler(Type type, Handler handler) { localHandlers.put(type, handler); return this; } /** * This maps a name of a Java construct, which cannot contain Java keywords, * to a keyword if it ends with a {@link #KEYWORD_SUFFIX} and the name * without the suffix maps to a Java keyword. * * @param name the name * @return either the name when it wasn't a keyword or a keyword */ public static String keyword(String name) { if (name.endsWith(KEYWORD_SUFFIX)) { String keyword = name.substring(0, name.length() - KEYWORD_SUFFIX.length()); if (keywords.contains(keyword)) return keyword; } return name; } public static String name(String key) { if (keywords.contains(key)) { return key + KEYWORD_SUFFIX; } return key; } public JSONCodec promiscuous() { this.startCharacters = START_CHARACTERS_BAD; this.promiscuous = true; return this; } public boolean isStartCharacter(int c) { return startCharacters.indexOf(c) >= 0; } }