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

com.backblaze.b2.json.B2Json Maven / Gradle / Ivy

Go to download

The core logic for B2 SDK for Java. Does not include any implementations of B2WebApiClient.

There is a newer version: 6.3.0
Show newest version
/**
 * JSON (de)serialization of Java objects.
 *
 * Copyright 2017, Backblaze Inc. All Rights Reserved.
 * License https://www.backblaze.com/using_b2_code.html
 */

package com.backblaze.b2.json;

import com.backblaze.b2.util.B2StringUtil;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.StringReader;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;
import java.util.Map;

/**
 * 

JSON (de)serialization of Java objects.

* *

This class knows how to take a Java object and turn it * into JSON, and then reverse the process and take JSON and create * a Java object.

* *

A number of classes are built in to B2Json. In addition to all * of the primitive types (byte, char, int, etc.) these classes are * all handled: Byte, Character, Integer, Long, Float, Double, Boolean, * String, LocalDate, LocalDateTime, and BigDecimal.

* *

For other classes to be used, you can either add a static getJsonTypeHandler() * method to it or add annotations to the class to say how it should go to JSON. * For classes without a getJsonTypeHandler() method, there must be a "required", * "optional", or "ignored" annotation on every field. And there must be exactly * one constructor with the "constructor" annotation.

* *

The selected constructor must take as arguments all of the non-ignored * fields in the object. If any validation of values needs to happen * during deserialization, it should happen in the constructor.

* *

During deserialization, an exception will be thrown if any required * fields are missing or null. Optional fields are set to 0/false/null * if they are not present in the JSON. If unexpected fields are present * in the JSON, they will cause an exception unless ALLOW_EXTRA_FIELDS * is selected.

* *

java.util.Map objects turn into JSON objects when serialized, and * java.util.List and java.util.Set objects turn into JSON arrays. On * deserialization, the values for Map fields are created as TreeMaps, * the values for List fields are created as ArrayLists, and the values for * Set fields are created as HashSets.

* *

The JSON produced is always "pretty", with newlines and indentation. * Field names are always sorted alphabetically.

* *

B2Json objects are THREAD SAFE.

*/ public class B2Json { /* Design note. If/when we need to handle types that we can't * annotate or add a getJsonTypeHandler() method to, we can * have some kind of plugin mechanism to register handlers with * this class. And, before that, i might ask what you're doing * relying on the shape of a class for which you don't own the * source code? */ private static String UTF8 = "UTF-8"; /** * A simple instance that can be shared. */ private static final B2Json instance = new B2Json(); /** * Bit map values for the options parameter to the constructor. */ public static final int ALLOW_EXTRA_FIELDS = 1; /** * The holder for all of the different handlers. */ private final B2JsonHandlerMap handlerMap; /** * @return A shared instance. */ public static B2Json get() { return instance; } /** * Initializes a new B2Json object with handlers for all of the * classes that are handled specially. */ private B2Json() { this.handlerMap = new B2JsonHandlerMap(); } /** * Turn an object into JSON, returning the result as an array of UTF-8 * bytes. */ public byte[] toJsonUtf8Bytes(Object obj) throws B2JsonException { try { return toJson(obj).getBytes(UTF8); } catch (IOException e) { throw new RuntimeException("error writing to byte array: " + e.getMessage()); } } /** * Turn an object into JSON, returning the result as an array of UTF-8 * bytes. */ public byte[] toJsonUtf8BytesWithNewline(Object obj) throws B2JsonException { try { final ByteArrayOutputStream out = new ByteArrayOutputStream(); toJson(obj, out); out.write('\n'); return out.toByteArray(); } catch (IOException e) { throw new RuntimeException("error writing to byte array: " + e.getMessage()); } } /** * Turn an object into JSON, writing the results to given * output stream. * * Note that the output stream is NOT closed as a side-effect of calling this. * It was a bug that it was being closed in version 1.1.1 and earlier. */ public void toJson(Object obj, OutputStream out) throws IOException, B2JsonException { if (obj == null) { throw new B2JsonException("top level object must not be null"); } final Class clazz = obj.getClass(); final B2JsonTypeHandler handler = handlerMap.getHandler(clazz); B2JsonWriter jsonWriter = new B2JsonWriter(out); //noinspection unchecked handler.serialize(obj, jsonWriter); } /** * Turn an object into JSON, returning the result as a string. */ public String toJson(Object obj) throws B2JsonException { if (obj == null) { throw new B2JsonException("top level object must not be null"); } Class clazz = obj.getClass(); final B2JsonTypeHandler handler = handlerMap.getHandler(clazz); try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) { B2JsonWriter jsonWriter = new B2JsonWriter(out); //noinspection unchecked handler.serialize(obj, jsonWriter); return out.toString(); } catch (IOException e) { throw new RuntimeException("IO exception writing to string"); } } /** * Turn an object into JSON, returning the result as a string. * This throws a RuntimeException instead of a B2JsonException, * so use it carefully. */ public static String toJsonOrThrowRuntime(Object obj) { try { return get().toJson(obj); } catch (B2JsonException e) { throw new IllegalArgumentException("failed to convert to json: " + e.getMessage(), e); } } /** * Turn a map into JSON, returning the result as a string. */ public String mapToJson(Map map, Class keyClass, Class valueClass) throws B2JsonException { if (map == null) { throw new B2JsonException("map must not be null"); } final B2JsonTypeHandler keyHandler = handlerMap.getHandler(keyClass); final B2JsonTypeHandler valueHandler = handlerMap.getHandler(valueClass); final B2JsonTypeHandler handler = new B2JsonMapHandler(keyHandler, valueHandler); try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) { B2JsonWriter jsonWriter = new B2JsonWriter(out); //noinspection unchecked handler.serialize(map, jsonWriter); return out.toString(B2StringUtil.UTF8); } catch (IOException e) { throw new RuntimeException("IO exception writing to string"); } } /** * Parses a JSON object into a map. */ public Map mapFromJson(String json, Class keyClass, Class valueClass) throws B2JsonException { final B2JsonTypeHandler keyHandler = handlerMap.getHandler(keyClass); final B2JsonTypeHandler valueHandler = handlerMap.getHandler(valueClass); final B2JsonTypeHandler handler = new B2JsonMapHandler(keyHandler, valueHandler); return fromJsonWithHandler(json, handler, 0); } /** * Turn a map into JSON, returning the result as a string. */ public String listToJson(List list, Class valueClass) throws B2JsonException { if (list == null) { throw new B2JsonException("list must not be null"); } final B2JsonTypeHandler valueHandler = handlerMap.getHandler(valueClass); final B2JsonTypeHandler handler = new B2JsonListHandler(valueHandler); try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) { B2JsonWriter jsonWriter = new B2JsonWriter(out); //noinspection unchecked handler.serialize(list, jsonWriter); return out.toString(); } catch (IOException e) { throw new RuntimeException("IO exception writing to string"); } } /** * Parses a JSON object into a map. */ public List listFromJson(String json, Class valueClass) throws B2JsonException { final B2JsonTypeHandler valueHandler = handlerMap.getHandler(valueClass); final B2JsonTypeHandler handler = new B2JsonListHandler(valueHandler); return fromJsonWithHandler(json, handler, 0); } /** * Parse JSON as an object of the given class with the given options, * reading from the input stream until reaching EOF. Throws an error * if there is anything but whitespace after the JSON value. */ public T fromJsonUntilEof(InputStream in, Class clazz) throws IOException, B2JsonException { return fromJsonUntilEof(in, clazz, 0); } public T fromJsonUntilEof(InputStream in, Class clazz, int options) throws IOException, B2JsonException { B2JsonReader reader = new B2JsonReader(new InputStreamReader(in, "UTF-8")); final B2JsonTypeHandler handler = handlerMap.getHandler(clazz); //noinspection unchecked T result = (T) handler.deserialize(reader, options); if (!reader.nextNonWhitespaceIsEof()) { throw new B2JsonException("non-whitespace characters after JSON value"); } return result; } /** * Parse JSON as an object of the given class with the given options. */ public T fromJson(InputStream in, Class clazz) throws IOException, B2JsonException { return fromJson(in, clazz, 0); } public T fromJson(InputStream in, Class clazz, int options) throws IOException, B2JsonException { B2JsonReader reader = new B2JsonReader(new InputStreamReader(in, "UTF-8")); final B2JsonTypeHandler handler = handlerMap.getHandler(clazz); if (handler == null) { throw new B2JsonException("B2Json.fromJson called with handler not in handlerMap"); } //noinspection unchecked return (T) handler.deserialize(reader, options); } /** * Parse JSON as an object of the given class. */ public T fromJson(String json, Class clazz) throws B2JsonException { return fromJson(json, clazz, 0); } public T fromJson(String json, Class clazz, int options) throws B2JsonException { final B2JsonTypeHandler handler = handlerMap.getHandler(clazz); return fromJsonWithHandler(json, handler, options); } private T fromJsonWithHandler(String json, B2JsonTypeHandler handler, int options) throws B2JsonException { try { B2JsonReader reader = new B2JsonReader(new StringReader(json)); //noinspection unchecked return (T) handler.deserialize(reader, options); } catch (IOException e) { throw new RuntimeException("error reading string", e); } } /** * Parse JSON as an object of the given class. */ public T fromJson(byte[] jsonUtf8Bytes, Class clazz) throws IOException, B2JsonException { return fromJson(jsonUtf8Bytes, clazz, 0); } public T fromJson(byte[] jsonUtf8Bytes, Class clazz, int options) throws IOException, B2JsonException { B2JsonReader reader = new B2JsonReader(new InputStreamReader(new ByteArrayInputStream(jsonUtf8Bytes), "UTF-8")); final B2JsonTypeHandler handler = handlerMap.getHandler(clazz); //noinspection unchecked return (T) handler.deserialize(reader, options); } /** * Parse a URL parameter map as an object of the given class. * * The values in the map are the values that will be used in the * object. The caller is responsible for URL-decoding them * before passing them to this method. */ public T fromUrlParameterMap(Map parameterMap, Class clazz) throws IOException, B2JsonException { return fromUrlParameterMap(parameterMap, clazz, 0); } public T fromUrlParameterMap(Map parameterMap, Class clazz, int options) throws IOException, B2JsonException { final B2JsonTypeHandler handler = handlerMap.getHandler(clazz); if (!(handler instanceof B2JsonObjectHandler)) { throw new B2JsonException("only objects can be deserialized from parameter maps"); } @SuppressWarnings("unchecked") final B2JsonObjectHandler objectHandler = (B2JsonObjectHandler) handler; //noinspection unchecked return objectHandler.deserializeFromUrlParameterMap(parameterMap, options); } /** *

Class annotation that says a class is the base class for a union type.

* *

The class must not extend any other classes, but may implement interfaces. * It must have no B2Json field or constructor annotations

* *

Direct instances of this class cannot be (de)serialized. Instances of * subclasses can be.

* *

For now, the implementation of deserialization is INEFFICIENT, so union * types should be used only for small objects.

*/ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface union { String typeField(); } /** * Field annotation that says a field is required to be present. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface required {} /** * Field annotation that says a field is optional. The value will * always be included, even if it is null. When deserializing, * null/false/0 will be passed to the constructor if the value is * not present in the JSON. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface optional {} /** * Field annotation that says a field is optional. The value will * always be included when serializing, even if it is null. When * deserializing, the provided default value will be used. The default * provided should be the JSON form of the value, as a string. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface optionalWithDefault { String defaultValue(); } /** * Field annotation that says the field is not included in JSON. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface ignored {} /** * Constructor annotation saying that this is the constructor B2Json * should use. This constructor must take ALL of the serializable * fields as parameters. * * You must provide an "params" parameter that lists the order of * the parameters to the constructor. * * If present, the "discards" parameter is a comma-separated list of * field names which are allowed to be present in the parsed json, * but whose values will be discarded. The names may be for fields * that don't exist or for fields marked @ignored. This is useful * for accepting deprecated fields without having to use * ALLOW_EXTRA_FIELDS, which would accept ALL unknown fields. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.CONSTRUCTOR) public @interface constructor { String params(); String discards() default ""; } /** * Field annotation that designates the enum value to use when the * value in a field isn't one of the known values. Use this at most * once in an enum. If no values have this annotation, we will throw * a B2JsonException at runtime when we hit an invalid value. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface defaultForInvalidEnumValue {} /** * All of the B2Json annotations. */ @SuppressWarnings("unchecked") /*package*/ static final Class[] ALL_ANNOTATIONS = new Class[] { union.class, required.class, optional.class, optionalWithDefault.class, ignored.class, constructor.class, defaultForInvalidEnumValue.class }; }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy