org.jsonx.JxEncoder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of binding Show documentation
Show all versions of binding Show documentation
The JSON/Java Binding API is designed to bind JSON documents to Java objects. More specifically,
the JSON/Java Binding API provides a way for JSON objects whose structure is expressed in the
JSON Schema Definition Language to be parsed and marshaled, to and from Java objects of
strongly-typed classes. The JSON/Java Binding API can also be used to validate JSON documents as
they are parsed from text or marshaled from Java objects against a JSD. Thus, the JSON/Java
Binding API is a reference implementation of the validation and binding functionalities of the
JSON Schema Definition Language.
/* Copyright (c) 2018 Jsonx
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* You should have received a copy of The MIT License (MIT) along with this
* program. If not, see .
*/
package org.jsonx;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jsonx.ArrayValidator.Relation;
import org.jsonx.ArrayValidator.Relations;
import org.libj.util.Classes;
import org.libj.util.ArrayUtil;
import org.libj.util.function.TriObjBiIntConsumer;
/**
* Encoder that serializes Jx objects (that extend {@link JxObject}) and Jx
* arrays (with a provided annotation class that declares an {@link ArrayType}
* annotation) to JSON documents.
*/
public class JxEncoder {
private static final HashMap instances = new HashMap<>();
/** {@code JxEncoder} that does not indent JSON values */
public static final JxEncoder _0 = get(0);
/** {@code JxEncoder} that indents JSON values with 1 space */
public static final JxEncoder _1 = get(1);
/** {@code JxEncoder} that indents JSON values with 2 spaces */
public static final JxEncoder _2 = get(2);
/** {@code JxEncoder} that indents JSON values with 3 spaces */
public static final JxEncoder _3 = get(3);
/** {@code JxEncoder} that indents JSON values with 4 spaces */
public static final JxEncoder _4 = get(4);
/** {@code JxEncoder} that indents JSON values with 8 spaces */
public static final JxEncoder _8 = get(8);
private static JxEncoder global = _0;
/**
* Returns the {@code JxEncoder} for the specified number of spaces to be used
* when indenting values during serialization to JSON documents.
*
* @param indent The number of spaces to be used when indenting values during
* serialization to JSON documents.
* @return The {@code JxEncoder} for the specified number of spaces to be used
* when indenting values during serialization to JSON documents.
* @throws IllegalArgumentException If {@code indent < 0}.
*/
public static JxEncoder get(final int indent) {
if (indent < 0)
throw new IllegalArgumentException("Indent must be a non-negative: " + indent);
JxEncoder encoder = instances.get(indent);
if (encoder == null)
instances.put(indent, encoder = new JxEncoder(indent));
return encoder;
}
/**
* @return The global {@code JxEncoder}.
* @see #set(JxEncoder)
*/
public static JxEncoder get() {
return global;
}
/**
* Set the global {@code JxEncoder}.
*
* @param encoder The {@code JxEncoder}.
* @see #get()
*/
public static void set(final JxEncoder encoder) {
global = encoder;
}
final int indent;
private final String comma;
private final String colon;
private final boolean validate;
/**
* Creates a new {@code JxEncoder} for the specified number of spaces to be
* used when indenting values during serialization to JSON documents.
*
* @param indent The number of spaces to be used when indenting values during
* serialization to JSON documents.
* @throws IllegalArgumentException If {@code indent < 0}.
*/
protected JxEncoder(final int indent) {
this(indent, true);
}
/**
* Creates a new {@code JxEncoder} for the specified number of spaces to be
* used when indenting values during serialization to JSON documents.
*
* @param indent The number of spaces to be used when indenting values during
* serialization to JSON documents.
* @param validate If {@code true}, the produced JSON is validated; if
* {@code false}, the produced JSON is not validated.
* @throws IllegalArgumentException If {@code indent < 0}.
*/
JxEncoder(final int indent, final boolean validate) {
if (indent < 0)
throw new IllegalArgumentException("Indent must be a non-negative: " + indent);
this.indent = indent;
this.validate = validate;
if (indent == 0) {
this.comma = ",";
this.colon = ":";
}
else {
this.comma = ", ";
this.colon = ": ";
}
}
private static Object getValue(final Object object, final String propertyName, final Use use) {
final Method method = JsdUtil.getGetMethod(object.getClass(), propertyName);
try {
if (method != null)
return method.invoke(object);
Map,?> optionalMap = null;
Map,?> map = null;
for (final Field field : object.getClass().getFields()) {
if (!Map.class.isAssignableFrom(field.getType()))
continue;
final AnyProperty property = field.getAnnotation(AnyProperty.class);
if (property == null)
continue;
if (!propertyName.equals(property.name()))
continue;
map = (Map,?>)field.get(object);
for (final Map.Entry,?> entry : map.entrySet())
if (entry.getKey() instanceof String && ((String)entry.getKey()).matches(propertyName))
return map;
if (use == Use.OPTIONAL)
optionalMap = map;
}
return optionalMap;
}
catch (final IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
private Error encodeNonArray(final boolean isProperty, final Field field, final Annotation annotation, final Object object, final StringBuilder builder, final int depth) {
if (field == null && object == null) {
if (validate && !JsdUtil.isNullable(annotation))
return Error.MEMBER_NOT_NULLABLE(annotation);
builder.append("null");
}
else {
final Class> type;
final boolean isOptional;
if (field == null) {
isOptional = false;
type = object.getClass();
}
else {
type = (isOptional = Optional.class.isAssignableFrom(field.getType())) ? Classes.getGenericClasses(field)[0] : field.getType();
}
final Object value = object == null ? null : isOptional ? ((Optional>)object).orElse(null) : object;
if (String.class.isAssignableFrom(type)) {
final Object encoded = StringCodec.encodeObject(annotation, isProperty ? ((StringProperty)annotation).pattern() : ((StringElement)annotation).pattern(), (String)value, validate);
if (encoded instanceof Error)
return (Error)encoded;
builder.append(encoded);
}
else if (Boolean.class.isAssignableFrom(type) && (annotation instanceof BooleanProperty || annotation instanceof BooleanElement)) {
builder.append(BooleanCodec.encodeObject((Boolean)value));
}
else if (Number.class.isAssignableFrom(type)) {
final Form form;
final String range;
if (isProperty) {
final NumberProperty property = (NumberProperty)annotation;
form = property.form();
range = property.range();
}
else {
final NumberElement element = (NumberElement)annotation;
form = element.form();
range = element.range();
}
final Object encoded = NumberCodec.encodeObject(annotation, form, range, (Number)value, validate);
if (encoded instanceof Error)
return (Error)encoded;
builder.append(encoded);
}
else if (JxObject.class.isAssignableFrom(type)) {
final Error error = marshal((JxObject)value, null, builder, depth + 1);
if (error != null)
return error;
}
else {
throw new UnsupportedOperationException("Unsupported object type: " + type.getName());
}
}
return null;
}
@SuppressWarnings("unchecked")
private Error encodeProperty(final Field field, final Annotation annotation, final String name, final Object object, final TriObjBiIntConsumer onFieldEncode, final StringBuilder builder, final int depth) {
try {
if (annotation instanceof ArrayProperty) {
final Object encoded = ArrayCodec.encodeObject(field, object instanceof Optional ? ((Optional>)object).orElse(null) : (List