| /* Copyright 2016 Clifton Labs |
| * 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 com.sleepycat.json_simple; |
| |
| import java.io.IOException; |
| import java.io.Reader; |
| import java.io.StringReader; |
| import java.io.StringWriter; |
| import java.io.Writer; |
| import java.util.Collection; |
| import java.util.EnumSet; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** Jsoner provides JSON utilities for escaping strings to be JSON compatible, thread safe parsing (RFC 4627) JSON |
| * strings, and thread safe serializing data to strings in JSON format. |
| * @since 2.0.0 */ |
| public class Jsoner{ |
| /** Flags to tweak the behavior of the primary deserialization method. */ |
| private static enum DeserializationOptions{ |
| /** Whether multiple JSON values can be deserialized as a root element. */ |
| ALLOW_CONCATENATED_JSON_VALUES, |
| /** Whether a JsonArray can be deserialized as a root element. */ |
| ALLOW_JSON_ARRAYS, |
| /** Whether a boolean, null, Number, or String can be deserialized as a root element. */ |
| ALLOW_JSON_DATA, |
| /** Whether a JsonObject can be deserialized as a root element. */ |
| ALLOW_JSON_OBJECTS; |
| } |
| |
| /** Flags to tweak the behavior of the primary serialization method. */ |
| private static enum SerializationOptions{ |
| /** Instead of aborting serialization on non-JSON values it will continue serialization by serializing the |
| * non-JSON value directly into the now invalid JSON. Be mindful that invalid JSON will not successfully |
| * deserialize. */ |
| ALLOW_INVALIDS, |
| /** Instead of aborting serialization on non-JSON values that implement Jsonable it will continue serialization |
| * by deferring serialization to the Jsonable. |
| * @see Jsonable */ |
| ALLOW_JSONABLES; |
| } |
| |
| /** The possible States of a JSON deserializer. */ |
| private static enum States{ |
| /** Post-parsing state. */ |
| DONE, |
| /** Pre-parsing state. */ |
| INITIAL, |
| /** Parsing error, ParsingException should be thrown. */ |
| PARSED_ERROR, |
| @SuppressWarnings("javadoc") |
| PARSING_ARRAY, |
| /** Parsing a key-value pair inside of an object. */ |
| PARSING_ENTRY, |
| @SuppressWarnings("javadoc") |
| PARSING_OBJECT; |
| } |
| |
| @SuppressWarnings("javadoc") |
| private Jsoner(){ |
| /* Jsoner is purely static so instantiation is unnecessary. */ |
| } |
| |
| /** Deserializes a readable stream according to the RFC 4627 JSON specification. |
| * @param readableDeserializable representing content to be deserialized as JSON. |
| * @return either a boolean, null, Number, String, JsonObject, or JsonArray that best represents the deserializable. |
| * @throws JsonException if an unexpected token is encountered in the deserializable. To recover from a |
| * JsonException: fix the deserializable to no longer have an unexpected token and try again. */ |
| public static Object deserialize(final Reader readableDeserializable) throws JsonException{ |
| return Jsoner.deserialize(readableDeserializable, EnumSet.of(DeserializationOptions.ALLOW_JSON_ARRAYS, DeserializationOptions.ALLOW_JSON_OBJECTS, DeserializationOptions.ALLOW_JSON_DATA)).get(0); |
| } |
| |
| /** Deserialize a stream with all deserialized JSON values are wrapped in a JsonArray. |
| * @param deserializable representing content to be deserialized as JSON. |
| * @param flags representing the allowances and restrictions on deserialization. |
| * @return the allowable object best represented by the deserializable. |
| * @throws JsonException if a disallowed or unexpected token is encountered in the deserializable. To recover from a |
| * JsonException: fix the deserializable to no longer have a disallowed or unexpected token and try |
| * again. */ |
| private static JsonArray deserialize(final Reader deserializable, final Set<DeserializationOptions> flags) throws JsonException{ |
| final Yylex lexer = new Yylex(deserializable); |
| Yytoken token; |
| States currentState; |
| int returnCount = 1; |
| final LinkedList<States> stateStack = new LinkedList<>(); |
| final LinkedList<Object> valueStack = new LinkedList<>(); |
| stateStack.addLast(States.INITIAL); |
| do{ |
| /* Parse through the parsable string's tokens. */ |
| currentState = Jsoner.popNextState(stateStack); |
| token = Jsoner.lexNextToken(lexer); |
| switch(currentState){ |
| case DONE: |
| /* The parse has finished a JSON value. */ |
| if(!flags.contains(DeserializationOptions.ALLOW_CONCATENATED_JSON_VALUES) || Yytoken.Types.END.equals(token.getType())){ |
| /* Break if concatenated values are not allowed or if an END token is read. */ |
| break; |
| } |
| /* Increment the amount of returned JSON values and treat the token as if it were a fresh parse. */ |
| returnCount += 1; |
| /* Fall through to the case for the initial state. */ |
| //$FALL-THROUGH$ |
| case INITIAL: |
| /* The parse has just started. */ |
| switch(token.getType()){ |
| case DATUM: |
| /* A boolean, null, Number, or String could be detected. */ |
| if(flags.contains(DeserializationOptions.ALLOW_JSON_DATA)){ |
| valueStack.addLast(token.getValue()); |
| stateStack.addLast(States.DONE); |
| }else{ |
| throw new JsonException(lexer.getPosition(), JsonException.Problems.DISALLOWED_TOKEN, token); |
| } |
| break; |
| case LEFT_BRACE: |
| /* An object is detected. */ |
| if(flags.contains(DeserializationOptions.ALLOW_JSON_OBJECTS)){ |
| valueStack.addLast(new JsonObject()); |
| stateStack.addLast(States.PARSING_OBJECT); |
| }else{ |
| throw new JsonException(lexer.getPosition(), JsonException.Problems.DISALLOWED_TOKEN, token); |
| } |
| break; |
| case LEFT_SQUARE: |
| /* An array is detected. */ |
| if(flags.contains(DeserializationOptions.ALLOW_JSON_ARRAYS)){ |
| valueStack.addLast(new JsonArray()); |
| stateStack.addLast(States.PARSING_ARRAY); |
| }else{ |
| throw new JsonException(lexer.getPosition(), JsonException.Problems.DISALLOWED_TOKEN, token); |
| } |
| break; |
| default: |
| /* Neither a JSON array or object was detected. */ |
| throw new JsonException(lexer.getPosition(), JsonException.Problems.UNEXPECTED_TOKEN, token); |
| } |
| break; |
| case PARSED_ERROR: |
| /* The parse could be in this state due to the state stack not having a state to pop off. */ |
| throw new JsonException(lexer.getPosition(), JsonException.Problems.UNEXPECTED_TOKEN, token); |
| case PARSING_ARRAY: |
| switch(token.getType()){ |
| case COMMA: |
| /* The parse could detect a comma while parsing an array since it separates each element. */ |
| stateStack.addLast(currentState); |
| break; |
| case DATUM: |
| /* The parse found an element of the array. */ |
| JsonArray val = (JsonArray)valueStack.getLast(); |
| val.add(token.getValue()); |
| stateStack.addLast(currentState); |
| break; |
| case LEFT_BRACE: |
| /* The parse found an object in the array. */ |
| val = (JsonArray)valueStack.getLast(); |
| final JsonObject object = new JsonObject(); |
| val.add(object); |
| valueStack.addLast(object); |
| stateStack.addLast(currentState); |
| stateStack.addLast(States.PARSING_OBJECT); |
| break; |
| case LEFT_SQUARE: |
| /* The parse found another array in the array. */ |
| val = (JsonArray)valueStack.getLast(); |
| final JsonArray array = new JsonArray(); |
| val.add(array); |
| valueStack.addLast(array); |
| stateStack.addLast(currentState); |
| stateStack.addLast(States.PARSING_ARRAY); |
| break; |
| case RIGHT_SQUARE: |
| /* The parse found the end of the array. */ |
| if(valueStack.size() > returnCount){ |
| valueStack.removeLast(); |
| }else{ |
| /* The parse has been fully resolved. */ |
| stateStack.addLast(States.DONE); |
| } |
| break; |
| default: |
| /* Any other token is invalid in an array. */ |
| throw new JsonException(lexer.getPosition(), JsonException.Problems.UNEXPECTED_TOKEN, token); |
| } |
| break; |
| case PARSING_OBJECT: |
| /* The parse has detected the start of an object. */ |
| switch(token.getType()){ |
| case COMMA: |
| /* The parse could detect a comma while parsing an object since it separates each key value |
| * pair. Continue parsing the object. */ |
| stateStack.addLast(currentState); |
| break; |
| case DATUM: |
| /* The token ought to be a key. */ |
| if(token.getValue() instanceof String){ |
| /* JSON keys are always strings, strings are not always JSON keys but it is going to be |
| * treated as one. Continue parsing the object. */ |
| final String key = (String)token.getValue(); |
| valueStack.addLast(key); |
| stateStack.addLast(currentState); |
| stateStack.addLast(States.PARSING_ENTRY); |
| }else{ |
| /* Abort! JSON keys are always strings and it wasn't a string. */ |
| throw new JsonException(lexer.getPosition(), JsonException.Problems.UNEXPECTED_TOKEN, token); |
| } |
| break; |
| case RIGHT_BRACE: |
| /* The parse has found the end of the object. */ |
| if(valueStack.size() > returnCount){ |
| /* There are unresolved values remaining. */ |
| valueStack.removeLast(); |
| }else{ |
| /* The parse has been fully resolved. */ |
| stateStack.addLast(States.DONE); |
| } |
| break; |
| default: |
| /* The parse didn't detect the end of an object or a key. */ |
| throw new JsonException(lexer.getPosition(), JsonException.Problems.UNEXPECTED_TOKEN, token); |
| } |
| break; |
| case PARSING_ENTRY: |
| switch(token.getType()){ |
| /* Parsed pair keys can only happen while parsing objects. */ |
| case COLON: |
| /* The parse could detect a colon while parsing a key value pair since it separates the key |
| * and value from each other. Continue parsing the entry. */ |
| stateStack.addLast(currentState); |
| break; |
| case DATUM: |
| /* The parse has found a value for the parsed pair key. */ |
| String key = (String)valueStack.removeLast(); |
| JsonObject parent = (JsonObject)valueStack.getLast(); |
| parent.put(key, token.getValue()); |
| break; |
| case LEFT_BRACE: |
| /* The parse has found an object for the parsed pair key. */ |
| key = (String)valueStack.removeLast(); |
| parent = (JsonObject)valueStack.getLast(); |
| final JsonObject object = new JsonObject(); |
| parent.put(key, object); |
| valueStack.addLast(object); |
| stateStack.addLast(States.PARSING_OBJECT); |
| break; |
| case LEFT_SQUARE: |
| /* The parse has found an array for the parsed pair key. */ |
| key = (String)valueStack.removeLast(); |
| parent = (JsonObject)valueStack.getLast(); |
| final JsonArray array = new JsonArray(); |
| parent.put(key, array); |
| valueStack.addLast(array); |
| stateStack.addLast(States.PARSING_ARRAY); |
| break; |
| default: |
| /* The parse didn't find anything for the parsed pair key. */ |
| throw new JsonException(lexer.getPosition(), JsonException.Problems.UNEXPECTED_TOKEN, token); |
| } |
| break; |
| default: |
| break; |
| } |
| /* If we're not at the END and DONE then do the above again. */ |
| }while(!(States.DONE.equals(currentState) && Yytoken.Types.END.equals(token.getType()))); |
| return new JsonArray(valueStack); |
| } |
| |
| /** A convenience method that assumes a StringReader to deserialize a string. |
| * @param deserializable representing content to be deserialized as JSON. |
| * @return either a boolean, null, Number, String, JsonObject, or JsonArray that best represents the deserializable. |
| * @throws JsonException if an unexpected token is encountered in the deserializable. To recover from a |
| * JsonException: fix the deserializable to no longer have an unexpected token and try again. |
| * @see Jsoner#deserialize(Reader) |
| * @see StringReader */ |
| public static Object deserialize(final String deserializable) throws JsonException{ |
| Object returnable; |
| StringReader readableDeserializable = null; |
| try{ |
| readableDeserializable = new StringReader(deserializable); |
| returnable = Jsoner.deserialize(readableDeserializable); |
| }catch(final NullPointerException caught){ |
| /* They both have the same recovery scenario. |
| * See StringReader. |
| * If deserializable is null, it should be reasonable to expect null back. */ |
| returnable = null; |
| }finally{ |
| if(readableDeserializable != null){ |
| readableDeserializable.close(); |
| } |
| } |
| return returnable; |
| } |
| |
| /** A convenience method that assumes a JsonArray must be deserialized. |
| * @param deserializable representing content to be deserializable as a JsonArray. |
| * @param defaultValue representing what would be returned if deserializable isn't a JsonArray or an IOException, |
| * NullPointerException, or JsonException occurs during deserialization. |
| * @return a JsonArray that represents the deserializable, or the defaultValue if there isn't a JsonArray that |
| * represents deserializable. |
| * @see Jsoner#deserialize(Reader) */ |
| public static JsonArray deserialize(final String deserializable, final JsonArray defaultValue){ |
| StringReader readable = null; |
| JsonArray returnable; |
| try{ |
| readable = new StringReader(deserializable); |
| returnable = Jsoner.deserialize(readable, EnumSet.of(DeserializationOptions.ALLOW_JSON_ARRAYS)).<JsonArray> getCollection(0); |
| }catch(NullPointerException | JsonException caught){ |
| /* Don't care, just return the default value. */ |
| returnable = defaultValue; |
| }finally{ |
| if(readable != null){ |
| readable.close(); |
| } |
| } |
| return returnable; |
| } |
| |
| /** A convenience method that assumes a JsonObject must be deserialized. |
| * @param deserializable representing content to be deserializable as a JsonObject. |
| * @param defaultValue representing what would be returned if deserializable isn't a JsonObject or an IOException, |
| * NullPointerException, or JsonException occurs during deserialization. |
| * @return a JsonObject that represents the deserializable, or the defaultValue if there isn't a JsonObject that |
| * represents deserializable. |
| * @see Jsoner#deserialize(Reader) */ |
| public static JsonObject deserialize(final String deserializable, final JsonObject defaultValue){ |
| StringReader readable = null; |
| JsonObject returnable; |
| try{ |
| readable = new StringReader(deserializable); |
| returnable = Jsoner.deserialize(readable, EnumSet.of(DeserializationOptions.ALLOW_JSON_OBJECTS)).<JsonObject> getMap(0); |
| }catch(NullPointerException | JsonException caught){ |
| /* Don't care, just return the default value. */ |
| returnable = defaultValue; |
| }finally{ |
| if(readable != null){ |
| readable.close(); |
| } |
| } |
| return returnable; |
| } |
| |
| /** A convenience method that assumes multiple RFC 4627 JSON values (except numbers) have been concatenated together |
| * for deserilization which will be collectively returned in a JsonArray wrapper. |
| * There may be numbers included, they just must not be concatenated together as it is prone to |
| * NumberFormatExceptions (thus causing a JsonException) or the numbers no longer represent their |
| * respective values. |
| * Examples: |
| * "123null321" returns [123, null, 321] |
| * "nullnullnulltruefalse\"\"{}[]" returns [null, null, null, true, false, "", {}, []] |
| * "123" appended to "321" returns [123321] |
| * "12.3" appended to "3.21" throws JsonException(NumberFormatException) |
| * "123" appended to "-321" throws JsonException(NumberFormatException) |
| * "123e321" appended to "-1" throws JsonException(NumberFormatException) |
| * "null12.33.21null" throws JsonException(NumberFormatException) |
| * @param deserializable representing concatenated content to be deserialized as JSON in one reader. Its contents |
| * may not contain two numbers concatenated together. |
| * @return a JsonArray that contains each of the concatenated objects as its elements. Each concatenated element is |
| * either a boolean, null, Number, String, JsonArray, or JsonObject that best represents the concatenated |
| * content inside deserializable. |
| * @throws JsonException if an unexpected token is encountered in the deserializable. To recover from a |
| * JsonException: fix the deserializable to no longer have an unexpected token and try again. */ |
| public static JsonArray deserializeMany(final Reader deserializable) throws JsonException{ |
| return Jsoner.deserialize(deserializable, EnumSet.of(DeserializationOptions.ALLOW_JSON_ARRAYS, DeserializationOptions.ALLOW_JSON_OBJECTS, DeserializationOptions.ALLOW_JSON_DATA, DeserializationOptions.ALLOW_CONCATENATED_JSON_VALUES)); |
| } |
| |
| /** Escapes potentially confusing or important characters in the String provided. |
| * @param escapable an unescaped string. |
| * @return an escaped string for usage in JSON; An escaped string is one that has escaped all of the quotes ("), |
| * backslashes (\), return character (\r), new line character (\n), tab character (\t), |
| * backspace character (\b), form feed character (\f) and other control characters [u0000..u001F] or |
| * characters [u007F..u009F], [u2000..u20FF] with a |
| * backslash (\) which itself must be escaped by the backslash in a java string. */ |
| public static String escape(final String escapable){ |
| final StringBuilder builder = new StringBuilder(); |
| final int characters = escapable.length(); |
| for(int i = 0; i < characters; i++){ |
| final char character = escapable.charAt(i); |
| switch(character){ |
| case '"': |
| builder.append("\\\""); |
| break; |
| case '\\': |
| builder.append("\\\\"); |
| break; |
| case '\b': |
| builder.append("\\b"); |
| break; |
| case '\f': |
| builder.append("\\f"); |
| break; |
| case '\n': |
| builder.append("\\n"); |
| break; |
| case '\r': |
| builder.append("\\r"); |
| break; |
| case '\t': |
| builder.append("\\t"); |
| break; |
| case '/': |
| builder.append("\\/"); |
| break; |
| default: |
| /* The many characters that get replaced are benign to software but could be mistaken by people |
| * reading it for a JSON relevant character. */ |
| if(((character >= '\u0000') && (character <= '\u001F')) || ((character >= '\u007F') && (character <= '\u009F')) || ((character >= '\u2000') && (character <= '\u20FF'))){ |
| final String characterHexCode = Integer.toHexString(character); |
| builder.append("\\u"); |
| for(int k = 0; k < (4 - characterHexCode.length()); k++){ |
| builder.append("0"); |
| } |
| builder.append(characterHexCode.toUpperCase()); |
| }else{ |
| /* Character didn't need escaping. */ |
| builder.append(character); |
| } |
| } |
| } |
| return builder.toString(); |
| } |
| |
| /** Processes the lexer's reader for the next token. |
| * @param lexer represents a text processor being used in the deserialization process. |
| * @return a token representing a meaningful element encountered by the lexer. |
| * @throws JsonException if an unexpected character is encountered while processing the text. */ |
| private static Yytoken lexNextToken(final Yylex lexer) throws JsonException{ |
| Yytoken returnable; |
| /* Parse through the next token. */ |
| try{ |
| returnable = lexer.yylex(); |
| }catch(final IOException caught){ |
| throw new JsonException(-1, JsonException.Problems.UNEXPECTED_EXCEPTION, caught); |
| } |
| if(returnable == null){ |
| /* If there isn't another token, it must be the end. */ |
| returnable = new Yytoken(Yytoken.Types.END, null); |
| } |
| return returnable; |
| } |
| |
| /** Creates a new JsonKey that wraps the given string and value. This function should NOT be |
| * used in favor of existing constants and enumerations to make code easier to maintain. |
| * @param key represents the JsonKey as a String. |
| * @param value represents the value the JsonKey uses. |
| * @return a JsonKey that represents the provided key and value. */ |
| public static JsonKey mintJsonKey(final String key, final Object value){ |
| return new JsonKey(){ |
| @Override |
| public String getKey(){ |
| return key; |
| } |
| |
| @Override |
| public Object getValue(){ |
| return value; |
| } |
| }; |
| } |
| |
| /** Used for state transitions while deserializing. |
| * @param stateStack represents the deserialization states saved for future processing. |
| * @return a state for deserialization context so it knows how to consume the next token. */ |
| private static States popNextState(final LinkedList<States> stateStack){ |
| if(stateStack.size() > 0){ |
| return stateStack.removeLast(); |
| }else{ |
| return States.PARSED_ERROR; |
| } |
| } |
| |
| /** Formats the JSON string to be more easily human readable using tabs for indentation. |
| * @param printable representing a JSON formatted string with out extraneous characters, like one returned from |
| * Jsoner#serialize(Object). |
| * @return printable except it will have '\n' then '\t' characters inserted after '[', '{', ',' and before ']' '}' |
| * tokens in the JSON. It will return null if printable isn't a JSON string. */ |
| public static String prettyPrint(final String printable){ |
| return Jsoner.prettyPrint(printable, "\t"); |
| } |
| |
| /** Formats the JSON string to be more easily human readable using an arbitrary amount of spaces for indentation. |
| * @param printable representing a JSON formatted string with out extraneous characters, like one returned from |
| * Jsoner#serialize(Object). |
| * @param spaces representing the amount of spaces to use for indentation. Must be between 2 and 10. |
| * @return printable except it will have '\n' then space characters inserted after '[', '{', ',' and before ']' '}' |
| * tokens in the JSON. It will return null if printable isn't a JSON string. |
| * @throws IllegalArgumentException if spaces isn't between [2..10]. |
| * @see Jsoner#prettyPrint(String) |
| * @since 2.2.0 to allow pretty printing with spaces instead of tabs. */ |
| public static String prettyPrint(final String printable, final int spaces){ |
| if((spaces > 10) || (spaces < 2)){ |
| throw new IllegalArgumentException("Indentation with spaces must be between 2 and 10."); |
| } |
| final StringBuilder indentation = new StringBuilder(""); |
| for(int i = 0; i < spaces; i++){ |
| indentation.append(" "); |
| } |
| return Jsoner.prettyPrint(printable, indentation.toString()); |
| } |
| |
| /** Makes the JSON string more easily human readable using indentation of the caller's choice. |
| * @param printable representing a JSON formatted string with out extraneous characters, like one returned from |
| * Jsoner#serialize(Object). |
| * @param indentation representing the indentation used to format the JSON string. |
| * @return printable except it will have '\n' then indentation characters inserted after '[', '{', ',' and before |
| * ']' '}' tokens in the JSON. It will return null if printable isn't a JSON string. */ |
| private static String prettyPrint(final String printable, final String indentation){ |
| final Yylex lexer = new Yylex(new StringReader(printable)); |
| Yytoken lexed; |
| final StringBuilder returnable = new StringBuilder(); |
| int level = 0; |
| try{ |
| do{ |
| lexed = Jsoner.lexNextToken(lexer); |
| switch(lexed.getType()){ |
| case COLON: |
| returnable.append(":"); |
| break; |
| case COMMA: |
| returnable.append(lexed.getValue()); |
| returnable.append("\n"); |
| for(int i = 0; i < level; i++){ |
| returnable.append(indentation); |
| } |
| break; |
| case END: |
| break; |
| case LEFT_BRACE: |
| case LEFT_SQUARE: |
| returnable.append(lexed.getValue()); |
| returnable.append("\n"); |
| level++; |
| for(int i = 0; i < level; i++){ |
| returnable.append(indentation); |
| } |
| break; |
| case RIGHT_BRACE: |
| case RIGHT_SQUARE: |
| returnable.append("\n"); |
| level--; |
| for(int i = 0; i < level; i++){ |
| returnable.append(indentation); |
| } |
| returnable.append(lexed.getValue()); |
| break; |
| default: |
| if(lexed.getValue() instanceof String){ |
| returnable.append("\""); |
| returnable.append(Jsoner.escape((String)lexed.getValue())); |
| returnable.append("\""); |
| }else{ |
| returnable.append(lexed.getValue()); |
| } |
| break; |
| } |
| }while(!lexed.getType().equals(Yytoken.Types.END)); |
| }catch(final JsonException caught){ |
| /* This is according to the method's contract. */ |
| return null; |
| } |
| return returnable.toString(); |
| } |
| |
| /** A convenience method that assumes a StringWriter. |
| * @param jsonSerializable represents the object that should be serialized as a string in JSON format. |
| * @return a string, in JSON format, that represents the object provided. |
| * @throws IllegalArgumentException if the jsonSerializable isn't serializable in JSON. |
| * @see Jsoner#serialize(Object, Writer) |
| * @see StringWriter */ |
| public static String serialize(final Object jsonSerializable){ |
| final StringWriter writableDestination = new StringWriter(); |
| try{ |
| Jsoner.serialize(jsonSerializable, writableDestination); |
| }catch(final IOException caught){ |
| /* See StringWriter. */ |
| } |
| return writableDestination.toString(); |
| } |
| |
| /** Serializes values according to the RFC 4627 JSON specification. It will also trust the serialization provided by |
| * any Jsonables it serializes. |
| * @param jsonSerializable represents the object that should be serialized in JSON format. |
| * @param writableDestination represents where the resulting JSON text is written to. |
| * @throws IOException if the writableDestination encounters an I/O problem, like being closed while in use. |
| * @throws IllegalArgumentException if the jsonSerializable isn't serializable in JSON. */ |
| public static void serialize(final Object jsonSerializable, final Writer writableDestination) throws IOException{ |
| Jsoner.serialize(jsonSerializable, writableDestination, EnumSet.of(SerializationOptions.ALLOW_JSONABLES)); |
| } |
| |
| /** Serialize values to JSON and write them to the provided writer based on behavior flags. |
| * @param jsonSerializable represents the object that should be serialized to a string in JSON format. |
| * @param writableDestination represents where the resulting JSON text is written to. |
| * @param flags represents the allowances and restrictions on serialization. |
| * @throws IOException if the writableDestination encounters an I/O problem. |
| * @throws IllegalArgumentException if the jsonSerializable isn't serializable in JSON. |
| * @see SerializationOptions */ |
| private static void serialize(final Object jsonSerializable, final Writer writableDestination, final Set<SerializationOptions> flags) throws IOException{ |
| if(jsonSerializable == null){ |
| /* When a null is passed in the word null is supported in JSON. */ |
| writableDestination.write("null"); |
| }else if(((jsonSerializable instanceof Jsonable) && flags.contains(SerializationOptions.ALLOW_JSONABLES))){ |
| /* Writes the writable as defined by the writable. */ |
| writableDestination.write(((Jsonable)jsonSerializable).toJson()); |
| }else if(jsonSerializable instanceof String){ |
| /* Make sure the string is properly escaped. */ |
| writableDestination.write('"'); |
| writableDestination.write(Jsoner.escape((String)jsonSerializable)); |
| writableDestination.write('"'); |
| }else if(jsonSerializable instanceof Character){ |
| /* Make sure the string is properly escaped. |
| * Quotes for some reason are necessary for String, but not Character. */ |
| writableDestination.write(Jsoner.escape(jsonSerializable.toString())); |
| }else if(jsonSerializable instanceof Double){ |
| if(((Double)jsonSerializable).isInfinite() || ((Double)jsonSerializable).isNaN()){ |
| /* Infinite and not a number are not supported by the JSON specification, so null is used instead. */ |
| writableDestination.write("null"); |
| }else{ |
| writableDestination.write(jsonSerializable.toString()); |
| } |
| }else if(jsonSerializable instanceof Float){ |
| if(((Float)jsonSerializable).isInfinite() || ((Float)jsonSerializable).isNaN()){ |
| /* Infinite and not a number are not supported by the JSON specification, so null is used instead. */ |
| writableDestination.write("null"); |
| }else{ |
| writableDestination.write(jsonSerializable.toString()); |
| } |
| }else if(jsonSerializable instanceof Number){ |
| writableDestination.write(jsonSerializable.toString()); |
| }else if(jsonSerializable instanceof Boolean){ |
| writableDestination.write(jsonSerializable.toString()); |
| }else if(jsonSerializable instanceof Map){ |
| /* Writes the map in JSON object format. */ |
| boolean isFirstEntry = true; |
| @SuppressWarnings("rawtypes") |
| final Iterator entries = ((Map)jsonSerializable).entrySet().iterator(); |
| writableDestination.write('{'); |
| while(entries.hasNext()){ |
| if(isFirstEntry){ |
| isFirstEntry = false; |
| }else{ |
| writableDestination.write(','); |
| } |
| @SuppressWarnings("rawtypes") |
| final Map.Entry entry = (Map.Entry)entries.next(); |
| Jsoner.serialize(entry.getKey(), writableDestination, flags); |
| writableDestination.write(':'); |
| Jsoner.serialize(entry.getValue(), writableDestination, flags); |
| } |
| writableDestination.write('}'); |
| }else if(jsonSerializable instanceof Collection){ |
| /* Writes the collection in JSON array format. */ |
| boolean isFirstElement = true; |
| @SuppressWarnings("rawtypes") |
| final Iterator elements = ((Collection)jsonSerializable).iterator(); |
| writableDestination.write('['); |
| while(elements.hasNext()){ |
| if(isFirstElement){ |
| isFirstElement = false; |
| }else{ |
| writableDestination.write(','); |
| } |
| Jsoner.serialize(elements.next(), writableDestination, flags); |
| } |
| writableDestination.write(']'); |
| }else if(jsonSerializable instanceof byte[]){ |
| /* Writes the array in JSON array format. */ |
| final byte[] writableArray = (byte[])jsonSerializable; |
| final int numberOfElements = writableArray.length; |
| writableDestination.write('['); |
| for(int i = 0; i < numberOfElements; i++){ |
| if(i == (numberOfElements - 1)){ |
| Jsoner.serialize(writableArray[i], writableDestination, flags); |
| }else{ |
| Jsoner.serialize(writableArray[i], writableDestination, flags); |
| writableDestination.write(','); |
| } |
| } |
| writableDestination.write(']'); |
| }else if(jsonSerializable instanceof short[]){ |
| /* Writes the array in JSON array format. */ |
| final short[] writableArray = (short[])jsonSerializable; |
| final int numberOfElements = writableArray.length; |
| writableDestination.write('['); |
| for(int i = 0; i < numberOfElements; i++){ |
| if(i == (numberOfElements - 1)){ |
| Jsoner.serialize(writableArray[i], writableDestination, flags); |
| }else{ |
| Jsoner.serialize(writableArray[i], writableDestination, flags); |
| writableDestination.write(','); |
| } |
| } |
| writableDestination.write(']'); |
| }else if(jsonSerializable instanceof int[]){ |
| /* Writes the array in JSON array format. */ |
| final int[] writableArray = (int[])jsonSerializable; |
| final int numberOfElements = writableArray.length; |
| writableDestination.write('['); |
| for(int i = 0; i < numberOfElements; i++){ |
| if(i == (numberOfElements - 1)){ |
| Jsoner.serialize(writableArray[i], writableDestination, flags); |
| }else{ |
| Jsoner.serialize(writableArray[i], writableDestination, flags); |
| writableDestination.write(','); |
| } |
| } |
| writableDestination.write(']'); |
| }else if(jsonSerializable instanceof long[]){ |
| /* Writes the array in JSON array format. */ |
| final long[] writableArray = (long[])jsonSerializable; |
| final int numberOfElements = writableArray.length; |
| writableDestination.write('['); |
| for(int i = 0; i < numberOfElements; i++){ |
| if(i == (numberOfElements - 1)){ |
| Jsoner.serialize(writableArray[i], writableDestination, flags); |
| }else{ |
| Jsoner.serialize(writableArray[i], writableDestination, flags); |
| writableDestination.write(','); |
| } |
| } |
| writableDestination.write(']'); |
| }else if(jsonSerializable instanceof float[]){ |
| /* Writes the array in JSON array format. */ |
| final float[] writableArray = (float[])jsonSerializable; |
| final int numberOfElements = writableArray.length; |
| writableDestination.write('['); |
| for(int i = 0; i < numberOfElements; i++){ |
| if(i == (numberOfElements - 1)){ |
| Jsoner.serialize(writableArray[i], writableDestination, flags); |
| }else{ |
| Jsoner.serialize(writableArray[i], writableDestination, flags); |
| writableDestination.write(','); |
| } |
| } |
| writableDestination.write(']'); |
| }else if(jsonSerializable instanceof double[]){ |
| /* Writes the array in JSON array format. */ |
| final double[] writableArray = (double[])jsonSerializable; |
| final int numberOfElements = writableArray.length; |
| writableDestination.write('['); |
| for(int i = 0; i < numberOfElements; i++){ |
| if(i == (numberOfElements - 1)){ |
| Jsoner.serialize(writableArray[i], writableDestination, flags); |
| }else{ |
| Jsoner.serialize(writableArray[i], writableDestination, flags); |
| writableDestination.write(','); |
| } |
| } |
| writableDestination.write(']'); |
| }else if(jsonSerializable instanceof boolean[]){ |
| /* Writes the array in JSON array format. */ |
| final boolean[] writableArray = (boolean[])jsonSerializable; |
| final int numberOfElements = writableArray.length; |
| writableDestination.write('['); |
| for(int i = 0; i < numberOfElements; i++){ |
| if(i == (numberOfElements - 1)){ |
| Jsoner.serialize(writableArray[i], writableDestination, flags); |
| }else{ |
| Jsoner.serialize(writableArray[i], writableDestination, flags); |
| writableDestination.write(','); |
| } |
| } |
| writableDestination.write(']'); |
| }else if(jsonSerializable instanceof char[]){ |
| /* Writes the array in JSON array format. */ |
| final char[] writableArray = (char[])jsonSerializable; |
| final int numberOfElements = writableArray.length; |
| writableDestination.write("[\""); |
| for(int i = 0; i < numberOfElements; i++){ |
| if(i == (numberOfElements - 1)){ |
| Jsoner.serialize(writableArray[i], writableDestination, flags); |
| }else{ |
| Jsoner.serialize(writableArray[i], writableDestination, flags); |
| writableDestination.write("\",\""); |
| } |
| } |
| writableDestination.write("\"]"); |
| }else if(jsonSerializable instanceof Object[]){ |
| /* Writes the array in JSON array format. */ |
| final Object[] writableArray = (Object[])jsonSerializable; |
| final int numberOfElements = writableArray.length; |
| writableDestination.write('['); |
| for(int i = 0; i < numberOfElements; i++){ |
| if(i == (numberOfElements - 1)){ |
| Jsoner.serialize(writableArray[i], writableDestination, flags); |
| }else{ |
| Jsoner.serialize(writableArray[i], writableDestination, flags); |
| writableDestination.write(","); |
| } |
| } |
| writableDestination.write(']'); |
| }else{ |
| /* It cannot by any measure be safely serialized according to specification. */ |
| if(flags.contains(SerializationOptions.ALLOW_INVALIDS)){ |
| /* Can be helpful for debugging how it isn't valid. */ |
| writableDestination.write(jsonSerializable.toString()); |
| }else{ |
| /* Notify the caller the cause of failure for the serialization. */ |
| throw new IllegalArgumentException("Encountered a: " + jsonSerializable.getClass().getName() + " as: " + jsonSerializable.toString() + " that isn't JSON serializable.\n Try:\n 1) Implementing the Jsonable interface for the object to return valid JSON. If it already does it probably has a bug.\n 2) If you cannot edit the source of the object or couple it with this library consider wrapping it in a class that does implement the Jsonable interface.\n 3) Otherwise convert it to a boolean, null, number, JsonArray, JsonObject, or String value before serializing it.\n 4) If you feel it should have serialized you could use a more tolerant serialization for debugging purposes."); |
| } |
| } |
| } |
| |
| /** Serializes like the first version of this library. |
| * It has been adapted to use Jsonable for serializing custom objects, but otherwise works like the old JSON string |
| * serializer. It will allow non-JSON values in its output like the old one. It can be helpful for last resort log |
| * statements and debugging errors in self generated JSON. Anything serialized using this method isn't guaranteed to |
| * be deserializable. |
| * @param jsonSerializable represents the object that should be serialized in JSON format. |
| * @param writableDestination represents where the resulting JSON text is written to. |
| * @throws IOException if the writableDestination encounters an I/O problem, like being closed while in use. */ |
| public static void serializeCarelessly(final Object jsonSerializable, final Writer writableDestination) throws IOException{ |
| Jsoner.serialize(jsonSerializable, writableDestination, EnumSet.of(SerializationOptions.ALLOW_JSONABLES, SerializationOptions.ALLOW_INVALIDS)); |
| } |
| |
| /** Serializes JSON values and only JSON values according to the RFC 4627 JSON specification. |
| * @param jsonSerializable represents the object that should be serialized in JSON format. |
| * @param writableDestination represents where the resulting JSON text is written to. |
| * @throws IOException if the writableDestination encounters an I/O problem, like being closed while in use. |
| * @throws IllegalArgumentException if the jsonSerializable isn't serializable in raw JSON. */ |
| public static void serializeStrictly(final Object jsonSerializable, final Writer writableDestination) throws IOException{ |
| Jsoner.serialize(jsonSerializable, writableDestination, EnumSet.noneOf(SerializationOptions.class)); |
| } |
| } |