package org.basex.io.serial; import static org.basex.data.DataText.*; import static org.basex.query.util.Err.*; import static org.basex.util.Token.*; import java.io.IOException; import java.io.OutputStream; import org.basex.query.item.Item; import org.basex.query.util.json.JSONConverter; import org.basex.util.TokenBuilder; import org.basex.util.Util; import org.basex.util.hash.TokenMap; import org.basex.util.hash.TokenSet; import org.basex.util.list.BoolList; import org.basex.util.list.TokenList; /** * This class serializes data as JSON. The input must conform to the rules * defined in the {@link JSONConverter} class. * * @author BaseX Team 2005-12, BSD License * @author Christian Gruen */ public final class JSONSerializer extends OutputSerializer { /** Plural. */ private static final byte[] S = { 's' }; /** Global data type attributes. */ private static final byte[][] ATTRS = { concat(T_BOOLEAN, S), concat(T_NUMBER, S), concat(NULL, S), concat(T_ARRAY, S), concat(T_OBJECT, S), concat(T_STRING, S) }; /** Supported data types. */ private static final byte[][] TYPES = { T_BOOLEAN, T_NUMBER, NULL, T_ARRAY, T_OBJECT, T_STRING }; /** Cached data types. */ private final TokenSet[] typeCache = new TokenSet[TYPES.length]; /** Comma flag. */ private final BoolList comma = new BoolList(); /** Types. */ private final TokenList types = new TokenList(); /** * Constructor. * @param os output stream reference * @param props serialization properties * @throws IOException I/O exception */ public JSONSerializer(final OutputStream os, final SerializerProp props) throws IOException { super(os, props); for(int t = 0; t < typeCache.length; t++) typeCache[t] = new TokenMap(); } @Override protected void startOpen(final byte[] name) throws IOException { if(level == 0 && !eq(name, T_JSON)) error("<%> expected as root node", T_JSON); types.set(level, null); comma.set(level + 1, false); } @Override public void attribute(final byte[] name, final byte[] value) throws IOException { if(level == 0) { final int tl = typeCache.length; for(int t = 0; t < tl; t++) { if(eq(name, ATTRS[t])) { for(final byte[] b : split(value, ' ')) typeCache[t].add(b); return; } } } if(eq(name, T_TYPE)) { types.set(level, value); if(!eq(value, TYPES)) error("Element <%> has invalid type \"%\"", tag, value); } else { error("Element <%> has invalid attribute \"%\"", tag, name); } } @Override protected void finishOpen() throws IOException { if(comma.get(level)) print(','); else comma.set(level, true); if(level > 0) { indent(level); final byte[] par = types.get(level - 1); if(eq(par, T_OBJECT)) { print('"'); print(name(tag)); print("\": "); } else if(!eq(par, T_ARRAY)) { error("Element <%> is typed as \"%\" and cannot be nested", tags.get(level - 1), par); } } byte[] type = types.get(level); if(type == null) { int t = -1; final int tl = typeCache.length; while(++t < tl && typeCache[t].id(tag) == 0); if(t != tl) type = TYPES[t]; else type = T_STRING; types.set(level, type); } if(eq(type, T_OBJECT)) { print('{'); } else if(eq(type, T_ARRAY)) { print('['); } else if(level == 0) { error("Element <%> must be typed as \"%\" or \"%\"", T_JSON, T_OBJECT, T_ARRAY); } } @Override public void finishText(final byte[] text) throws IOException { final byte[] type = types.get(level - 1); if(eq(type, T_STRING)) { print('"'); for(final byte ch : text) code(ch); print('"'); } else if(eq(type, T_BOOLEAN, T_NUMBER)) { print(text); } else if(trim(text).length != 0) { error("Element <%> is typed as \"%\" and cannot have a value", tags.get(level - 1), type); } } @Override protected void finishEmpty() throws IOException { finishOpen(); final byte[] type = types.get(level); if(eq(type, T_STRING)) { print("\"\""); } else if(eq(type, NULL)) { print(NULL); } else if(!eq(type, T_OBJECT, T_ARRAY)) { error("Value expected for type \"%\"", type); } finishClose(); } @Override protected void finishClose() throws IOException { final byte[] type = types.get(level); if(eq(type, T_ARRAY)) { indent(level); print(']'); } else if(eq(type, T_OBJECT)) { indent(level); print('}'); } } @Override protected void code(final int ch) throws IOException { switch(ch) { case '\b': print("\\b"); break; case '\f': print("\\f"); break; case '\n': print("\\n"); break; case '\r': print("\\r"); break; case '\t': print("\\t"); break; case '"': print("\\\""); break; case '/': print("\\/"); break; case '\\': print("\\\\"); break; default: print(ch); break; } } @Override public void finishComment(final byte[] value) throws IOException { error("Comments cannot be serialized"); } @Override public void finishPi(final byte[] name, final byte[] value) throws IOException { error("Processing instructions cannot be serialized"); } @Override public void finishItem(final Item value) throws IOException { error("Items cannot be serialized"); } /** * Prints some indentation. * @param lvl level * @throws IOException I/O exception */ void indent(final int lvl) throws IOException { print(nl); final int ls = lvl * indents; for(int l = 0; l < ls; ++l) print(tab); } /** * Converts an XML element name to a JSON name. * @param name name * @return cached QName */ private static byte[] name(final byte[] name) { // convert name to valid XML representation final TokenBuilder tb = new TokenBuilder(); int uc = 0; // mode: 0=normal, 1=unicode, 2=underscore, 3=building unicode int mode = 0; for(int n = 0; n < name.length;) { final int cp = cp(name, n); if(mode >= 3) { uc = (uc << 4) + cp - (cp >= '0' && cp <= '9' ? '0' : 0x37); if(++mode == 7) { tb.add(uc); mode = 0; } } else if(cp == '_') { // limit underscore counter if(++mode == 3) { tb.add('_'); mode = 0; continue; } } else if(mode == 1) { // unicode mode = 3; continue; } else if(mode == 2) { // underscore tb.add('_'); mode = 0; continue; } else { // normal character tb.add(cp); mode = 0; } n += cl(name, n); } if(mode == 2) { tb.add('_'); } else if(mode > 0 && tb.size() != 0) { tb.add('?'); } return tb.finish(); } /** * Raises an error with the specified message. * @param msg error message * @param ext error details * @throws IOException I/O exception */ private static void error(final String msg, final Object... ext) throws IOException { throw JSONSER.thrwSerial(Util.inf(msg, ext)); } }