feat(serialize): implement data binding

This commit is contained in:
Johannes Frohnmeyer 2024-04-12 19:35:00 +02:00
parent 1f51e9025d
commit e9dffa3a33
Signed by: Johannes
GPG Key ID: E76429612C2929F4
38 changed files with 3424 additions and 9 deletions

View File

@ -0,0 +1,31 @@
import io.gitlab.jfronny.scripts.*
plugins {
commons.library
}
dependencies {
implementation(projects.commons)
implementation(projects.commonsSerializeDatabind)
testImplementation(libs.junit.jupiter.api)
testRuntimeOnly(libs.junit.jupiter.engine)
}
publishing {
publications {
create<MavenPublication>("maven") {
groupId = "io.gitlab.jfronny"
artifactId = "commons-serialize-databind-sql"
from(components["java"])
}
}
}
tasks.javadoc {
linksOffline("https://maven.frohnmeyer-wds.de/javadoc/artifacts/io/gitlab/jfronny/commons/$version/raw", projects.commons)
linksOffline("https://maven.frohnmeyer-wds.de/javadoc/artifacts/io/gitlab/jfronny/commons-serialize/$version/raw", projects.commonsSerialize)
linksOffline("https://maven.frohnmeyer-wds.de/javadoc/artifacts/io/gitlab/jfronny/commons-serialize-databind/$version/raw", projects.commonsSerializeDatabind)
enabled = false
}

View File

@ -0,0 +1,71 @@
/*
* Copyright (C) 2011 Google Inc.
*
* 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 io.gitlab.jfronny.commons.serialize.databind.sql;
import io.gitlab.jfronny.commons.serialize.databind.*;
import io.gitlab.jfronny.commons.serialize.stream.MalformedDataException;
import io.gitlab.jfronny.commons.serialize.stream.SerializeReader;
import io.gitlab.jfronny.commons.serialize.stream.SerializeWriter;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
/**
* Adapter for java.sql.Date. Although this class appears stateless, it is not. DateFormat captures
* its time zone and locale when it is created, which gives this class state. DateFormat isn't
* thread safe either, so this class has to synchronize its read and write methods.
*/
@SuppressWarnings("JavaUtilDate")
@SerializerFor(targets = java.sql.Date.class)
final class SqlDateTypeAdapter extends TypeAdapter<java.sql.Date> {
private final DateFormat format = new SimpleDateFormat("MMM d, yyyy");
private SqlDateTypeAdapter() {}
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(java.sql.Date value, Writer writer) throws TEx, MalformedDataException {
if (value == null) {
writer.nullValue();
return;
}
String dateString;
synchronized (this) {
dateString = format.format(value);
}
writer.value(dateString);
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> java.sql.Date deserialize(Reader reader) throws TEx, MalformedDataException {
String s = reader.nextString();
synchronized (this) {
TimeZone originalTimeZone = format.getTimeZone(); // Save the original time zone
try {
Date utilDate = format.parse(s);
return new java.sql.Date(utilDate.getTime());
} catch (ParseException e) {
throw new MalformedDataException(
"Failed parsing '" + s + "' as SQL Date; at path " + reader.getPreviousPath(), e);
} finally {
format.setTimeZone(originalTimeZone); // Restore the original time zone after parsing
}
}
}
}

View File

@ -0,0 +1,79 @@
/*
* Copyright (C) 2011 Google Inc.
*
* 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 io.gitlab.jfronny.commons.serialize.databind.sql;
import io.gitlab.jfronny.commons.serialize.databind.SerializerFor;
import io.gitlab.jfronny.commons.serialize.databind.TypeAdapter;
import io.gitlab.jfronny.commons.serialize.stream.MalformedDataException;
import io.gitlab.jfronny.commons.serialize.stream.SerializeReader;
import io.gitlab.jfronny.commons.serialize.stream.SerializeWriter;
import io.gitlab.jfronny.commons.serialize.stream.Token;
import java.sql.Time;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
/**
* Adapter for java.sql.Time. Although this class appears stateless, it is not. DateFormat captures
* its time zone and locale when it is created, which gives this class state. DateFormat isn't
* thread safe either, so this class has to synchronize its read and write methods.
*/
@SuppressWarnings("JavaUtilDate")
@SerializerFor(targets = Time.class)
final class SqlTimeTypeAdapter extends TypeAdapter<Time> {
private final DateFormat format = new SimpleDateFormat("hh:mm:ss a");
private SqlTimeTypeAdapter() {}
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(Time value, Writer writer) throws TEx, MalformedDataException {
if (value == null) {
writer.nullValue();
return;
}
String timeString;
synchronized (this) {
timeString = format.format(value);
}
writer.value(timeString);
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> Time deserialize(Reader reader) throws TEx, MalformedDataException {
if (reader.peek() == Token.NULL) {
reader.nextNull();
return null;
}
String s = reader.nextString();
synchronized (this) {
TimeZone originalTimeZone = format.getTimeZone(); // Save the original time zone
try {
Date date = format.parse(s);
return new Time(date.getTime());
} catch (ParseException e) {
throw new MalformedDataException(
"Failed parsing '" + s + "' as SQL Time; at path " + reader.getPreviousPath(), e);
} finally {
format.setTimeZone(originalTimeZone); // Restore the original time zone
}
}
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright (C) 2020 Google Inc.
*
* 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 io.gitlab.jfronny.commons.serialize.databind.sql;
import io.gitlab.jfronny.commons.serialize.databind.ObjectMapper;
import io.gitlab.jfronny.commons.serialize.databind.TypeAdapter;
import io.gitlab.jfronny.commons.serialize.databind.TypeAdapterFactory;
import io.gitlab.jfronny.commons.serialize.databind.TypeToken;
import io.gitlab.jfronny.commons.serialize.stream.MalformedDataException;
import io.gitlab.jfronny.commons.serialize.stream.SerializeReader;
import io.gitlab.jfronny.commons.serialize.stream.SerializeWriter;
import java.sql.Timestamp;
import java.util.Date;
@SuppressWarnings("JavaUtilDate")
class SqlTimestampTypeAdapterFactory implements TypeAdapterFactory {
@Override
public <T> TypeAdapter<T> create(ObjectMapper mapper, TypeToken<T> type) {
if (type.getRawType() != Timestamp.class) return null;
TypeAdapter<Date> dateTypeAdapter = mapper.getAdapter(Date.class);
return (TypeAdapter) new TimestampTypeAdapter(dateTypeAdapter);
}
private static class TimestampTypeAdapter extends TypeAdapter<Timestamp> {
private final TypeAdapter<Date> dateTypeAdapter;
public TimestampTypeAdapter(TypeAdapter<Date> dateTypeAdapter) {
this.dateTypeAdapter = dateTypeAdapter;
}
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(Timestamp value, Writer writer) throws TEx, MalformedDataException {
dateTypeAdapter.serialize(value, writer);
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> Timestamp deserialize(Reader reader) throws TEx, MalformedDataException {
Date date = dateTypeAdapter.deserialize(reader);
return date != null ? new Timestamp(date.getTime()) : null;
}
}
}

View File

@ -0,0 +1,7 @@
module io.gitlab.jfronny.commons.serialize.databind.sql {
requires io.gitlab.jfronny.commons;
requires io.gitlab.jfronny.commons.serialize;
requires io.gitlab.jfronny.commons.serialize.databind;
requires java.sql;
requires static org.jetbrains.annotations;
}

View File

@ -0,0 +1,2 @@
io.gitlab.jfronny.commons.serialize.databind.sql.SqlDateTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.sql.SqlTimeTypeAdapter

View File

@ -0,0 +1 @@
io.gitlab.jfronny.commons.serialize.databind.sql.SqlTimestampTypeAdapterFactory

View File

@ -0,0 +1,29 @@
import io.gitlab.jfronny.scripts.*
plugins {
commons.library
}
dependencies {
implementation(projects.commons)
api(projects.commonsSerialize)
testImplementation(libs.junit.jupiter.api)
testRuntimeOnly(libs.junit.jupiter.engine)
}
publishing {
publications {
create<MavenPublication>("maven") {
groupId = "io.gitlab.jfronny"
artifactId = "commons-serialize-databind"
from(components["java"])
}
}
}
tasks.javadoc {
linksOffline("https://maven.frohnmeyer-wds.de/javadoc/artifacts/io/gitlab/jfronny/commons/$version/raw", projects.commons)
linksOffline("https://maven.frohnmeyer-wds.de/javadoc/artifacts/io/gitlab/jfronny/commons-serialize/$version/raw", projects.commonsSerialize)
}

View File

@ -0,0 +1,154 @@
package io.gitlab.jfronny.commons.serialize.databind;
import io.gitlab.jfronny.commons.serialize.databind.impl.adapter.SerializationDelegatingTypeAdapter;
import io.gitlab.jfronny.commons.serialize.stream.MalformedDataException;
import io.gitlab.jfronny.commons.serialize.stream.SerializeReader;
import io.gitlab.jfronny.commons.serialize.stream.SerializeWriter;
import java.lang.reflect.Type;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class ObjectMapper {
private final ThreadLocal<Map<TypeToken<?>, TypeAdapter<?>>> threadLocalAdapterResults = new ThreadLocal<>();
private final ConcurrentMap<TypeToken<?>, TypeAdapter<?>> typeTokenCache = new ConcurrentHashMap<>();
private final List<TypeAdapterFactory> factories = new ArrayList<>();
private final Map<TypeToken<?>, TypeAdapter<?>> explicitAdapters = new ConcurrentHashMap<>();
public ObjectMapper() {
ServiceLoader.load(TypeAdapterFactory.class).forEach(this::registerTypeAdapterFactory);
ServiceLoader.load(TypeAdapter.class).forEach(this::registerTypeAdapter);
}
public ObjectMapper registerTypeAdapter(Type type, TypeAdapter<?> adapter) {
explicitAdapters.put(TypeToken.get(type), Objects.requireNonNull(adapter));
return this;
}
public ObjectMapper registerTypeAdapter(TypeAdapter<?> adapter) {
SerializerFor annotation = adapter.getClass().getAnnotation(SerializerFor.class);
if (annotation == null) {
throw new IllegalArgumentException("TypeAdapter must be annotated with @SerializerFor to register it without specifying the target type");
}
adapter = annotation.nullSafe() ? adapter.nullSafe() : adapter;
for (Class<?> target : annotation.targets()) {
registerTypeAdapter(target, adapter);
}
return this;
}
public ObjectMapper registerTypeAdapterFactory(TypeAdapterFactory factory) {
factories.add(Objects.requireNonNull(factory));
return this;
}
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>, T> T deserialize(Class<T> type, Reader reader) throws TEx, MalformedDataException {
return getAdapter(type).deserialize(reader);
}
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>, T> void serialize(T value, Writer writer) throws TEx, MalformedDataException {
getAdapter((Class<T>) value.getClass()).serialize(value, writer);
}
public <T> TypeAdapter<T> getAdapter(Class<T> type) {
return getAdapter(TypeToken.get(type));
}
public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) {
Objects.requireNonNull(type, "type must not be null");
if (explicitAdapters.containsKey(type)) {
return (TypeAdapter<T>) explicitAdapters.get(type);
}
TypeAdapter<?> cached = typeTokenCache.get(type);
if (cached != null) {
return (TypeAdapter<T>) cached;
}
Map<TypeToken<?>, TypeAdapter<?>> threadCalls = threadLocalAdapterResults.get();
boolean isInitialAdapterRequest = false;
if (threadCalls == null) {
threadCalls = new HashMap<>();
threadLocalAdapterResults.set(threadCalls);
isInitialAdapterRequest = true;
} else {
TypeAdapter<T> ongoingCall = (TypeAdapter<T>) threadCalls.get(type);
if (ongoingCall != null) {
return ongoingCall;
}
}
TypeAdapter<T> candidate = null;
try {
FutureTypeAdapter<T> call = new FutureTypeAdapter<>();
threadCalls.put(type, call);
for (TypeAdapterFactory factory : factories) {
candidate = factory.create(this, type);
if (candidate != null) {
call.setDelegate(candidate);
// Replace future adapter with actual adapter
threadCalls.put(type, candidate);
break;
}
}
} finally {
if (isInitialAdapterRequest) {
threadLocalAdapterResults.remove();
}
}
if (candidate == null) {
throw new IllegalArgumentException("JfCommons ObjectMapper cannot handle " + type);
}
if (isInitialAdapterRequest) {
/*
* Publish resolved adapters to all threads
* Can only do this for the initial request because cyclic dependency TypeA -> TypeB -> TypeA
* would otherwise publish adapter for TypeB which uses not yet resolved adapter for TypeA
* See https://github.com/google/gson/issues/625
*/
typeTokenCache.putAll(threadCalls);
}
return candidate;
}
static class FutureTypeAdapter<T> extends SerializationDelegatingTypeAdapter<T> {
private TypeAdapter<T> delegate = null;
public void setDelegate(TypeAdapter<T> typeAdapter) {
if (delegate != null) {
throw new AssertionError("Delegate is already set");
}
delegate = typeAdapter;
}
private TypeAdapter<T> delegate() {
TypeAdapter<T> delegate = this.delegate;
if (delegate == null) {
// Can occur when adapter is leaked to other thread or when adapter is used for
// (de-)serialization
// directly within the TypeAdapterFactory which requested it
throw new IllegalStateException(
"Adapter for type with cyclic dependency has been used"
+ " before dependency has been resolved");
}
return delegate;
}
@Override
public TypeAdapter<T> getSerializationDelegate() {
return delegate();
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> T deserialize(Reader reader) throws TEx, MalformedDataException {
return delegate().deserialize(reader);
}
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(T value, Writer writer) throws TEx, MalformedDataException {
delegate().serialize(value, writer);
}
}
}

View File

@ -0,0 +1,28 @@
package io.gitlab.jfronny.commons.serialize.databind;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotate a class with this to specify a custom adapter for serialization and deserialization of its instances.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SerializeWithAdapter {
/**
* Either a {@link TypeAdapter} or {@link TypeAdapterFactory}.
*/
Class<?> adapter();
/**
* Whether the adapter referenced by {@link #adapter()} should be made {@linkplain
* TypeAdapter#nullSafe() null-safe}.
*
* <p>If {@code true} (the default), it will be made null-safe and Gson will handle {@code null}
* Java objects on serialization and JSON {@code null} on deserialization without calling the
* adapter. If {@code false}, the adapter will have to handle the {@code null} values.
*/
boolean nullSafe() default true;
}

View File

@ -0,0 +1,24 @@
package io.gitlab.jfronny.commons.serialize.databind;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SerializerFor {
/**
* The type this {@link TypeAdapter} handles.
*/
Class<?>[] targets();
/**
* Whether this adapter should be made {@linkplain TypeAdapter#nullSafe() null-safe}.
*
* <p>If {@code true} (the default), it will be made null-safe and Gson will handle {@code null}
* Java objects on serialization and JSON {@code null} on deserialization without calling the
* adapter. If {@code false}, the adapter will have to handle the {@code null} values.
*/
boolean nullSafe() default true;
}

View File

@ -0,0 +1,33 @@
package io.gitlab.jfronny.commons.serialize.databind;
import io.gitlab.jfronny.commons.serialize.stream.MalformedDataException;
import io.gitlab.jfronny.commons.serialize.stream.SerializeReader;
import io.gitlab.jfronny.commons.serialize.stream.SerializeWriter;
public abstract class TypeAdapter<T> {
public abstract <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(T value, Writer writer) throws TEx, MalformedDataException;
public abstract <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> T deserialize(Reader reader) throws TEx, MalformedDataException;
public final TypeAdapter<T> nullSafe() {
return new TypeAdapter<>() {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(T value, Writer writer) throws TEx, MalformedDataException {
if (value == null) {
writer.nullValue();
} else {
TypeAdapter.this.serialize(value, writer);
}
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> T deserialize(Reader reader) throws TEx, MalformedDataException {
if (reader.peek() == null) {
reader.nextNull();
return null;
} else {
return TypeAdapter.this.deserialize(reader);
}
}
};
}
}

View File

@ -0,0 +1,5 @@
package io.gitlab.jfronny.commons.serialize.databind;
public interface TypeAdapterFactory {
<T> TypeAdapter<T> create(ObjectMapper mapper, TypeToken<T> type);
}

View File

@ -0,0 +1,401 @@
package io.gitlab.jfronny.commons.serialize.databind;
import io.gitlab.jfronny.commons.serialize.databind.impl.TypeUtils;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class TypeToken<T> {
private final Class<? super T> rawType;
private final Type type;
private final int hashCode;
/**
* Constructs a new type literal. Derives represented class from type parameter.
*
* <p>Clients create an empty anonymous subclass. Doing so embeds the type parameter in the
* anonymous class's type hierarchy so we can reconstitute it at runtime despite erasure, for
* example:
*
* <p>{@code new TypeToken<List<String>>() {}}
*
* @throws IllegalArgumentException If the anonymous {@code TypeToken} subclass captures a type
* variable, for example {@code TypeToken<List<T>>}. See the {@code TypeToken} class
* documentation for more details.
*/
@SuppressWarnings("unchecked")
protected TypeToken() {
this.type = getTypeTokenTypeArgument();
this.rawType = (Class<? super T>) TypeUtils.getRawType(type);
this.hashCode = type.hashCode();
}
/** Unsafe. Constructs a type literal manually. */
@SuppressWarnings("unchecked")
private TypeToken(Type type) {
this.type = TypeUtils.canonicalize(Objects.requireNonNull(type));
this.rawType = (Class<? super T>) TypeUtils.getRawType(this.type);
this.hashCode = this.type.hashCode();
}
private static boolean isCapturingTypeVariablesForbidden() {
return !Objects.equals(System.getProperty("gson.allowCapturingTypeVariables"), "true");
}
/**
* Verifies that {@code this} is an instance of a direct subclass of TypeToken and returns the
* type argument for {@code T} in {@link TypeUtils#canonicalize canonical form}.
*/
private Type getTypeTokenTypeArgument() {
Type superclass = getClass().getGenericSuperclass();
if (superclass instanceof ParameterizedType) {
ParameterizedType parameterized = (ParameterizedType) superclass;
if (parameterized.getRawType() == TypeToken.class) {
Type typeArgument = TypeUtils.canonicalize(parameterized.getActualTypeArguments()[0]);
if (isCapturingTypeVariablesForbidden()) {
verifyNoTypeVariable(typeArgument);
}
return typeArgument;
}
}
// Check for raw TypeToken as superclass
else if (superclass == TypeToken.class) {
throw new IllegalStateException(
"TypeToken must be created with a type argument: new TypeToken<...>() {}; When using code"
+ " shrinkers (ProGuard, R8, ...) make sure that generic signatures are preserved.");
}
// User created subclass of subclass of TypeToken
throw new IllegalStateException("Must only create direct subclasses of TypeToken");
}
private static void verifyNoTypeVariable(Type type) {
if (type instanceof TypeVariable) {
TypeVariable<?> typeVariable = (TypeVariable<?>) type;
throw new IllegalArgumentException(
"TypeToken type argument must not contain a type variable; captured type variable "
+ typeVariable.getName()
+ " declared by "
+ typeVariable.getGenericDeclaration());
} else if (type instanceof GenericArrayType) {
verifyNoTypeVariable(((GenericArrayType) type).getGenericComponentType());
} else if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
Type ownerType = parameterizedType.getOwnerType();
if (ownerType != null) {
verifyNoTypeVariable(ownerType);
}
for (Type typeArgument : parameterizedType.getActualTypeArguments()) {
verifyNoTypeVariable(typeArgument);
}
} else if (type instanceof WildcardType) {
WildcardType wildcardType = (WildcardType) type;
for (Type bound : wildcardType.getLowerBounds()) {
verifyNoTypeVariable(bound);
}
for (Type bound : wildcardType.getUpperBounds()) {
verifyNoTypeVariable(bound);
}
} else if (type == null) {
// Occurs in Eclipse IDE and certain Java versions (e.g. Java 11.0.18) when capturing type
// variable declared by method of local class, see
// https://github.com/eclipse-jdt/eclipse.jdt.core/issues/975
throw new IllegalArgumentException(
"TypeToken captured `null` as type argument; probably a compiler / runtime bug");
}
}
/** Returns the raw (non-generic) type for this type. */
public final Class<? super T> getRawType() {
return rawType;
}
/** Gets underlying {@code Type} instance. */
public final Type getType() {
return type;
}
/**
* Check if this type is assignable from the given class object.
*
* @deprecated this implementation may be inconsistent with javac for types with wildcards.
*/
@Deprecated
public boolean isAssignableFrom(Class<?> cls) {
return isAssignableFrom((Type) cls);
}
/**
* Check if this type is assignable from the given Type.
*
* @deprecated this implementation may be inconsistent with javac for types with wildcards.
*/
@Deprecated
public boolean isAssignableFrom(Type from) {
if (from == null) {
return false;
}
if (type.equals(from)) {
return true;
}
return switch (type) {
case Class<?> klazz -> rawType.isAssignableFrom(TypeUtils.getRawType(from));
case ParameterizedType parameterizedType -> isAssignableFrom(from, parameterizedType, new HashMap<>());
case GenericArrayType genericArrayType -> rawType.isAssignableFrom(TypeUtils.getRawType(from))
&& isAssignableFrom(from, genericArrayType);
default -> throw buildUnsupportedTypeException(
type, Class.class, ParameterizedType.class, GenericArrayType.class);
};
}
/**
* Check if this type is assignable from the given type token.
*
* @deprecated this implementation may be inconsistent with javac for types with wildcards.
*/
@Deprecated
public boolean isAssignableFrom(TypeToken<?> token) {
return isAssignableFrom(token.getType());
}
/**
* Private helper function that performs some assignability checks for the provided
* GenericArrayType.
*/
private static boolean isAssignableFrom(Type from, GenericArrayType to) {
Type toGenericComponentType = to.getGenericComponentType();
if (toGenericComponentType instanceof ParameterizedType) {
Type t = from;
if (from instanceof GenericArrayType) {
t = ((GenericArrayType) from).getGenericComponentType();
} else if (from instanceof Class<?>) {
Class<?> classType = (Class<?>) from;
while (classType.isArray()) {
classType = classType.getComponentType();
}
t = classType;
}
return isAssignableFrom(
t, (ParameterizedType) toGenericComponentType, new HashMap<>());
}
// No generic defined on "to"; therefore, return true and let other
// checks determine assignability
return true;
}
/** Private recursive helper function to actually do the type-safe checking of assignability. */
private static boolean isAssignableFrom(
Type from, ParameterizedType to, Map<String, Type> typeVarMap) {
if (from == null) {
return false;
}
if (to.equals(from)) {
return true;
}
// First figure out the class and any type information.
Class<?> clazz = TypeUtils.getRawType(from);
ParameterizedType ptype = null;
if (from instanceof ParameterizedType) {
ptype = (ParameterizedType) from;
}
// Load up parameterized variable info if it was parameterized.
if (ptype != null) {
Type[] tArgs = ptype.getActualTypeArguments();
TypeVariable<?>[] tParams = clazz.getTypeParameters();
for (int i = 0; i < tArgs.length; i++) {
Type arg = tArgs[i];
TypeVariable<?> var = tParams[i];
while (arg instanceof TypeVariable<?>) {
TypeVariable<?> v = (TypeVariable<?>) arg;
arg = typeVarMap.get(v.getName());
}
typeVarMap.put(var.getName(), arg);
}
// check if they are equivalent under our current mapping.
if (typeEquals(ptype, to, typeVarMap)) {
return true;
}
}
for (Type itype : clazz.getGenericInterfaces()) {
if (isAssignableFrom(itype, to, new HashMap<>(typeVarMap))) {
return true;
}
}
// Interfaces didn't work, try the superclass.
Type sType = clazz.getGenericSuperclass();
return isAssignableFrom(sType, to, new HashMap<>(typeVarMap));
}
/**
* Checks if two parameterized types are exactly equal, under the variable replacement described
* in the typeVarMap.
*/
private static boolean typeEquals(
ParameterizedType from, ParameterizedType to, Map<String, Type> typeVarMap) {
if (from.getRawType().equals(to.getRawType())) {
Type[] fromArgs = from.getActualTypeArguments();
Type[] toArgs = to.getActualTypeArguments();
for (int i = 0; i < fromArgs.length; i++) {
if (!matches(fromArgs[i], toArgs[i], typeVarMap)) {
return false;
}
}
return true;
}
return false;
}
private static IllegalArgumentException buildUnsupportedTypeException(
Type token, Class<?>... expected) {
// Build exception message
StringBuilder exceptionMessage = new StringBuilder("Unsupported type, expected one of: ");
for (Class<?> clazz : expected) {
exceptionMessage.append(clazz.getName()).append(", ");
}
exceptionMessage
.append("but got: ")
.append(token.getClass().getName())
.append(", for type token: ")
.append(token.toString());
return new IllegalArgumentException(exceptionMessage.toString());
}
/**
* Checks if two types are the same or are equivalent under a variable mapping given in the type
* map that was provided.
*/
private static boolean matches(Type from, Type to, Map<String, Type> typeMap) {
return to.equals(from)
|| (from instanceof TypeVariable
&& to.equals(typeMap.get(((TypeVariable<?>) from).getName())));
}
@Override
public final int hashCode() {
return this.hashCode;
}
@Override
public final boolean equals(Object o) {
return o instanceof TypeToken<?> && TypeUtils.equals(type, ((TypeToken<?>) o).type);
}
@Override
public final String toString() {
return TypeUtils.typeToString(type);
}
/** Gets type literal for the given {@code Type} instance. */
public static TypeToken<?> get(Type type) {
return new TypeToken<>(type);
}
/** Gets type literal for the given {@code Class} instance. */
public static <T> TypeToken<T> get(Class<T> type) {
return new TypeToken<>(type);
}
/**
* Gets a type literal for the parameterized type represented by applying {@code typeArguments} to
* {@code rawType}. This is mainly intended for situations where the type arguments are not
* available at compile time. The following example shows how a type token for {@code Map<K, V>}
* can be created:
*
* <pre>{@code
* Class<K> keyClass = ...;
* Class<V> valueClass = ...;
* TypeToken<?> mapTypeToken = TypeToken.getParameterized(Map.class, keyClass, valueClass);
* }</pre>
*
* As seen here the result is a {@code TypeToken<?>}; this method cannot provide any type-safety,
* and care must be taken to pass in the correct number of type arguments.
*
* <p>If {@code rawType} is a non-generic class and no type arguments are provided, this method
* simply delegates to {@link #get(Class)} and creates a {@code TypeToken(Class)}.
*
* @throws IllegalArgumentException If {@code rawType} is not of type {@code Class}, or if the
* type arguments are invalid for the raw type
*/
public static TypeToken<?> getParameterized(Type rawType, Type... typeArguments) {
Objects.requireNonNull(rawType);
Objects.requireNonNull(typeArguments);
// Perform basic validation here because this is the only public API where users
// can create malformed parameterized types
if (!(rawType instanceof Class)) {
// See also https://bugs.openjdk.org/browse/JDK-8250659
throw new IllegalArgumentException("rawType must be of type Class, but was " + rawType);
}
Class<?> rawClass = (Class<?>) rawType;
TypeVariable<?>[] typeVariables = rawClass.getTypeParameters();
int expectedArgsCount = typeVariables.length;
int actualArgsCount = typeArguments.length;
if (actualArgsCount != expectedArgsCount) {
throw new IllegalArgumentException(
rawClass.getName()
+ " requires "
+ expectedArgsCount
+ " type arguments, but got "
+ actualArgsCount);
}
// For legacy reasons create a TypeToken(Class) if the type is not generic
if (typeArguments.length == 0) {
return get(rawClass);
}
// Check for this here to avoid misleading exception thrown by ParameterizedTypeImpl
if (TypeUtils.requiresOwnerType(rawType)) {
throw new IllegalArgumentException(
"Raw type "
+ rawClass.getName()
+ " is not supported because it requires specifying an owner type");
}
for (int i = 0; i < expectedArgsCount; i++) {
Type typeArgument =
Objects.requireNonNull(typeArguments[i], "Type argument must not be null");
Class<?> rawTypeArgument = TypeUtils.getRawType(typeArgument);
TypeVariable<?> typeVariable = typeVariables[i];
for (Type bound : typeVariable.getBounds()) {
Class<?> rawBound = TypeUtils.getRawType(bound);
if (!rawBound.isAssignableFrom(rawTypeArgument)) {
throw new IllegalArgumentException(
"Type argument "
+ typeArgument
+ " does not satisfy bounds for type variable "
+ typeVariable
+ " declared by "
+ rawType);
}
}
}
return new TypeToken<>(TypeUtils.newParameterizedTypeWithOwner(null, rawType, typeArguments));
}
/**
* Gets type literal for the array type whose elements are all instances of {@code componentType}.
*/
public static TypeToken<?> getArray(Type componentType) {
return new TypeToken<>(TypeUtils.arrayOf(componentType));
}
}

View File

@ -0,0 +1,381 @@
/*
* Copyright (C) 2015 Google Inc.
*
* 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 io.gitlab.jfronny.commons.serialize.databind.impl;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.*;
/**
* Utilities methods for manipulating dates in iso8601 format. This is much faster and GC friendly
* than using SimpleDateFormat so highly suitable if you (un)serialize lots of date objects.
*
* <p>Supported parse format:
* [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh[:]mm]]
*
* @see <a href="http://www.w3.org/TR/NOTE-datetime">this specification</a>
*/
// Date parsing code from Jackson databind ISO8601Utils.java
// https://github.com/FasterXML/jackson-databind/blob/2.8/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java
public class ISO8601Utils {
private ISO8601Utils() {}
/**
* ID to represent the 'UTC' string, default timezone since Jackson 2.7
*
* @since 2.7
*/
private static final String UTC_ID = "UTC";
/**
* The UTC timezone, prefetched to avoid more lookups.
*
* @since 2.7
*/
private static final TimeZone TIMEZONE_UTC = TimeZone.getTimeZone(UTC_ID);
/*
/**********************************************************
/* Formatting
/**********************************************************
*/
/**
* Format a date into 'yyyy-MM-ddThh:mm:ssZ' (default timezone, no milliseconds precision)
*
* @param date the date to format
* @return the date formatted as 'yyyy-MM-ddThh:mm:ssZ'
*/
public static String format(Date date) {
return format(date, false, TIMEZONE_UTC);
}
/**
* Format a date into 'yyyy-MM-ddThh:mm:ss[.sss]Z' (GMT timezone)
*
* @param date the date to format
* @param millis true to include millis precision otherwise false
* @return the date formatted as 'yyyy-MM-ddThh:mm:ss[.sss]Z'
*/
public static String format(Date date, boolean millis) {
return format(date, millis, TIMEZONE_UTC);
}
/**
* Format date into yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
*
* @param date the date to format
* @param millis true to include millis precision otherwise false
* @param tz timezone to use for the formatting (UTC will produce 'Z')
* @return the date formatted as yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
*/
public static String format(Date date, boolean millis, TimeZone tz) {
Calendar calendar = new GregorianCalendar(tz, Locale.US);
calendar.setTime(date);
// estimate capacity of buffer as close as we can (yeah, that's pedantic ;)
int capacity = "yyyy-MM-ddThh:mm:ss".length();
capacity += millis ? ".sss".length() : 0;
capacity += tz.getRawOffset() == 0 ? "Z".length() : "+hh:mm".length();
StringBuilder formatted = new StringBuilder(capacity);
padInt(formatted, calendar.get(Calendar.YEAR), "yyyy".length());
formatted.append('-');
padInt(formatted, calendar.get(Calendar.MONTH) + 1, "MM".length());
formatted.append('-');
padInt(formatted, calendar.get(Calendar.DAY_OF_MONTH), "dd".length());
formatted.append('T');
padInt(formatted, calendar.get(Calendar.HOUR_OF_DAY), "hh".length());
formatted.append(':');
padInt(formatted, calendar.get(Calendar.MINUTE), "mm".length());
formatted.append(':');
padInt(formatted, calendar.get(Calendar.SECOND), "ss".length());
if (millis) {
formatted.append('.');
padInt(formatted, calendar.get(Calendar.MILLISECOND), "sss".length());
}
int offset = tz.getOffset(calendar.getTimeInMillis());
if (offset != 0) {
int hours = Math.abs((offset / (60 * 1000)) / 60);
int minutes = Math.abs((offset / (60 * 1000)) % 60);
formatted.append(offset < 0 ? '-' : '+');
padInt(formatted, hours, "hh".length());
formatted.append(':');
padInt(formatted, minutes, "mm".length());
} else {
formatted.append('Z');
}
return formatted.toString();
}
/*
/**********************************************************
/* Parsing
/**********************************************************
*/
/**
* Parse a date from ISO-8601 formatted string. It expects a format
* [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh[:mm]]]
*
* @param date ISO string to parse in the appropriate format.
* @param pos The position to start parsing from, updated to where parsing stopped.
* @return the parsed date
* @throws ParseException if the date is not in the appropriate format
*/
public static Date parse(String date, ParsePosition pos) throws ParseException {
Exception fail = null;
try {
int offset = pos.getIndex();
// extract year
int year = parseInt(date, offset, offset += 4);
if (checkOffset(date, offset, '-')) {
offset += 1;
}
// extract month
int month = parseInt(date, offset, offset += 2);
if (checkOffset(date, offset, '-')) {
offset += 1;
}
// extract day
int day = parseInt(date, offset, offset += 2);
// default time value
int hour = 0;
int minutes = 0;
int seconds = 0;
// always use 0 otherwise returned date will include millis of current time
int milliseconds = 0;
// if the value has no time component (and no time zone), we are done
boolean hasT = checkOffset(date, offset, 'T');
if (!hasT && (date.length() <= offset)) {
Calendar calendar = new GregorianCalendar(year, month - 1, day);
calendar.setLenient(false);
pos.setIndex(offset);
return calendar.getTime();
}
if (hasT) {
// extract hours, minutes, seconds and milliseconds
hour = parseInt(date, offset += 1, offset += 2);
if (checkOffset(date, offset, ':')) {
offset += 1;
}
minutes = parseInt(date, offset, offset += 2);
if (checkOffset(date, offset, ':')) {
offset += 1;
}
// second and milliseconds can be optional
if (date.length() > offset) {
char c = date.charAt(offset);
if (c != 'Z' && c != '+' && c != '-') {
seconds = parseInt(date, offset, offset += 2);
if (seconds > 59 && seconds < 63) {
seconds = 59; // truncate up to 3 leap seconds
}
// milliseconds can be optional in the format
if (checkOffset(date, offset, '.')) {
offset += 1;
int endOffset = indexOfNonDigit(date, offset + 1); // assume at least one digit
int parseEndOffset = Math.min(endOffset, offset + 3); // parse up to 3 digits
int fraction = parseInt(date, offset, parseEndOffset);
// compensate for "missing" digits
switch (parseEndOffset - offset) { // number of digits parsed
case 2:
milliseconds = fraction * 10;
break;
case 1:
milliseconds = fraction * 100;
break;
default:
milliseconds = fraction;
}
offset = endOffset;
}
}
}
}
// extract timezone
if (date.length() <= offset) {
throw new IllegalArgumentException("No time zone indicator");
}
TimeZone timezone = null;
char timezoneIndicator = date.charAt(offset);
if (timezoneIndicator == 'Z') {
timezone = TIMEZONE_UTC;
offset += 1;
} else if (timezoneIndicator == '+' || timezoneIndicator == '-') {
String timezoneOffset = date.substring(offset);
// When timezone has no minutes, we should append it, valid timezones are, for example:
// +00:00, +0000 and +00
timezoneOffset = timezoneOffset.length() >= 5 ? timezoneOffset : timezoneOffset + "00";
offset += timezoneOffset.length();
// 18-Jun-2015, tatu: Minor simplification, skip offset of "+0000"/"+00:00"
if (timezoneOffset.equals("+0000") || timezoneOffset.equals("+00:00")) {
timezone = TIMEZONE_UTC;
} else {
// 18-Jun-2015, tatu: Looks like offsets only work from GMT, not UTC...
// not sure why, but that's the way it looks. Further, Javadocs for
// `java.util.TimeZone` specifically instruct use of GMT as base for
// custom timezones... odd.
String timezoneId = "GMT" + timezoneOffset;
// String timezoneId = "UTC" + timezoneOffset;
timezone = TimeZone.getTimeZone(timezoneId);
String act = timezone.getID();
if (!act.equals(timezoneId)) {
/* 22-Jan-2015, tatu: Looks like canonical version has colons, but we may be given
* one without. If so, don't sweat.
* Yes, very inefficient. Hopefully not hit often.
* If it becomes a perf problem, add 'loose' comparison instead.
*/
String cleaned = act.replace(":", "");
if (!cleaned.equals(timezoneId)) {
throw new IndexOutOfBoundsException(
"Mismatching time zone indicator: "
+ timezoneId
+ " given, resolves to "
+ timezone.getID());
}
}
}
} else {
throw new IndexOutOfBoundsException(
"Invalid time zone indicator '" + timezoneIndicator + "'");
}
Calendar calendar = new GregorianCalendar(timezone);
calendar.setLenient(false);
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, month - 1);
calendar.set(Calendar.DAY_OF_MONTH, day);
calendar.set(Calendar.HOUR_OF_DAY, hour);
calendar.set(Calendar.MINUTE, minutes);
calendar.set(Calendar.SECOND, seconds);
calendar.set(Calendar.MILLISECOND, milliseconds);
pos.setIndex(offset);
return calendar.getTime();
// If we get a ParseException it'll already have the right message/offset.
// Other exception types can convert here.
} catch (IndexOutOfBoundsException | IllegalArgumentException e) {
fail = e;
}
String input = (date == null) ? null : ('"' + date + '"');
String msg = fail.getMessage();
if (msg == null || msg.isEmpty()) {
msg = "(" + fail.getClass().getName() + ")";
}
ParseException ex =
new ParseException("Failed to parse date [" + input + "]: " + msg, pos.getIndex());
ex.initCause(fail);
throw ex;
}
/**
* Check if the expected character exist at the given offset in the value.
*
* @param value the string to check at the specified offset
* @param offset the offset to look for the expected character
* @param expected the expected character
* @return true if the expected character exist at the given offset
*/
private static boolean checkOffset(String value, int offset, char expected) {
return (offset < value.length()) && (value.charAt(offset) == expected);
}
/**
* Parse an integer located between 2 given offsets in a string
*
* @param value the string to parse
* @param beginIndex the start index for the integer in the string
* @param endIndex the end index for the integer in the string
* @return the int
* @throws NumberFormatException if the value is not a number
*/
private static int parseInt(String value, int beginIndex, int endIndex)
throws NumberFormatException {
if (beginIndex < 0 || endIndex > value.length() || beginIndex > endIndex) {
throw new NumberFormatException(value);
}
// use same logic as in Integer.parseInt() but less generic we're not supporting negative values
int i = beginIndex;
int result = 0;
int digit;
if (i < endIndex) {
digit = Character.digit(value.charAt(i++), 10);
if (digit < 0) {
throw new NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex));
}
result = -digit;
}
while (i < endIndex) {
digit = Character.digit(value.charAt(i++), 10);
if (digit < 0) {
throw new NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex));
}
result *= 10;
result -= digit;
}
return -result;
}
/**
* Zero pad a number to a specified length
*
* @param buffer buffer to use for padding
* @param value the integer value to pad if necessary.
* @param length the length of the string we should zero pad
*/
private static void padInt(StringBuilder buffer, int value, int length) {
String strValue = Integer.toString(value);
for (int i = length - strValue.length(); i > 0; i--) {
buffer.append('0');
}
buffer.append(strValue);
}
/**
* Returns the index of the first character in the string that is not a digit, starting at offset.
*/
private static int indexOfNonDigit(String string, int offset) {
for (int i = offset; i < string.length(); i++) {
char c = string.charAt(i);
if (c < '0' || c > '9') {
return i;
}
}
return string.length();
}
}

View File

@ -0,0 +1,122 @@
package io.gitlab.jfronny.commons.serialize.databind.impl;
import io.gitlab.jfronny.commons.data.LazilyParsedNumber;
import io.gitlab.jfronny.commons.serialize.stream.MalformedDataException;
import io.gitlab.jfronny.commons.serialize.stream.SerializeReader;
import io.gitlab.jfronny.commons.serialize.stream.Token;
import java.util.Locale;
public class MapKeyReader extends SerializeReader<MalformedDataException, MapKeyReader> {
private final String path;
private final String previousPath;
private String content;
public MapKeyReader(String path, String previousPath, String content) {
this.path = path;
this.previousPath = previousPath;
this.content = content;
}
@Override
public MapKeyReader beginArray() throws MalformedDataException {
throw new MalformedDataException("Cannot read arrays in map keys");
}
@Override
public MapKeyReader endArray() throws MalformedDataException {
throw new MalformedDataException("Cannot end arrays in map keys");
}
@Override
public MapKeyReader beginObject() throws MalformedDataException {
throw new MalformedDataException("Cannot read objects in map keys");
}
@Override
public MapKeyReader endObject() throws MalformedDataException {
throw new MalformedDataException("Cannot end objects in map keys");
}
@Override
public boolean hasNext() throws MalformedDataException {
return content != null;
}
@Override
public Token peek() throws MalformedDataException {
return Token.STRING;
}
@Override
public String nextName() throws MalformedDataException {
throw new MalformedDataException("Cannot read names in map keys");
}
@Override
public String nextString() throws MalformedDataException {
if (content == null) {
throw new MalformedDataException("Map key already exhausted");
}
String result = content;
content = null;
return result;
}
@Override
public boolean nextBoolean() throws MalformedDataException {
if (content == null) {
throw new MalformedDataException("Map key already exhausted");
}
String result = content.toLowerCase(Locale.ROOT);
if (result.equals("true")) {
content = null;
return true;
} else if (result.equals("false")) {
content = null;
return false;
} else {
throw new MalformedDataException("Expected boolean but was " + result);
}
}
@Override
public void nextNull() throws MalformedDataException {
if (content == null) {
throw new MalformedDataException("Map key already exhausted");
}
content = null;
}
@Override
public Number nextNumber() throws MalformedDataException {
if (content == null) {
throw new MalformedDataException("Map key already exhausted");
}
LazilyParsedNumber result = new LazilyParsedNumber(content);
content = null;
return result;
}
@Override
public void skipValue() throws MalformedDataException {
if (content == null) {
throw new MalformedDataException("Map key already exhausted");
}
content = null;
}
@Override
public String getPath() {
return path;
}
@Override
public String getPreviousPath() {
return previousPath;
}
@Override
public void close() {
}
}

View File

@ -0,0 +1,93 @@
package io.gitlab.jfronny.commons.serialize.databind.impl;
import io.gitlab.jfronny.commons.serialize.stream.MalformedDataException;
import io.gitlab.jfronny.commons.serialize.stream.SerializeWriter;
import java.util.ArrayList;
import java.util.List;
public class MapKeyWriter extends SerializeWriter<MalformedDataException, MapKeyWriter> {
private String result = null;
private List<String> comments = new ArrayList<>();
private boolean isFailureOrigin = false;
public String getResult() {
return result;
}
public List<String> getComments() {
return comments;
}
public boolean isFailureOrigin() {
return isFailureOrigin;
}
@Override
public MapKeyWriter beginArray() throws MalformedDataException {
isFailureOrigin = true;
throw new MalformedDataException("Cannot write arrays in map keys");
}
@Override
public MapKeyWriter endArray() throws MalformedDataException {
isFailureOrigin = true;
throw new MalformedDataException("Cannot end arrays in map keys");
}
@Override
public MapKeyWriter beginObject() throws MalformedDataException {
isFailureOrigin = true;
throw new MalformedDataException("Cannot write objects in map keys");
}
@Override
public MapKeyWriter endObject() throws MalformedDataException {
isFailureOrigin = true;
throw new MalformedDataException("Cannot end objects in map keys");
}
@Override
public MapKeyWriter comment(String comment) throws MalformedDataException {
comments.add(comment);
return this;
}
@Override
public MapKeyWriter name(String name) throws MalformedDataException {
if (result != null) {
isFailureOrigin = true;
throw new MalformedDataException("Cannot write multiple names in map keys");
}
result = name;
return this;
}
@Override
public MapKeyWriter value(String value) throws MalformedDataException {
if (result != null) {
isFailureOrigin = true;
throw new MalformedDataException("Cannot write multiple values in map keys");
}
result = value;
return this;
}
@Override
public MapKeyWriter literalValue(String value) throws MalformedDataException {
if (result != null) {
isFailureOrigin = true;
throw new MalformedDataException("Cannot write multiple values in map keys");
}
result = value;
return this;
}
@Override
public void flush() {
}
@Override
public void close() {
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright (C) 2017 The Gson authors
*
* 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 io.gitlab.jfronny.commons.serialize.databind.impl;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Locale;
/** Provides DateFormats for US locale with patterns which were the default ones before Java 9. */
public class PreJava9DateFormatProvider {
private PreJava9DateFormatProvider() {}
/**
* Returns the same DateFormat as {@code DateFormat.getDateTimeInstance(dateStyle, timeStyle,
* Locale.US)} in Java 8 or below.
*/
public static DateFormat getUsDateTimeFormat(int dateStyle, int timeStyle) {
String pattern =
getDatePartOfDateTimePattern(dateStyle) + " " + getTimePartOfDateTimePattern(timeStyle);
return new SimpleDateFormat(pattern, Locale.US);
}
private static String getDatePartOfDateTimePattern(int dateStyle) {
switch (dateStyle) {
case DateFormat.SHORT:
return "M/d/yy";
case DateFormat.MEDIUM:
return "MMM d, yyyy";
case DateFormat.LONG:
return "MMMM d, yyyy";
case DateFormat.FULL:
return "EEEE, MMMM d, yyyy";
default:
throw new IllegalArgumentException("Unknown DateFormat style: " + dateStyle);
}
}
private static String getTimePartOfDateTimePattern(int timeStyle) {
switch (timeStyle) {
case DateFormat.SHORT:
return "h:mm a";
case DateFormat.MEDIUM:
return "h:mm:ss a";
case DateFormat.FULL:
case DateFormat.LONG:
return "h:mm:ss a z";
default:
throw new IllegalArgumentException("Unknown DateFormat style: " + timeStyle);
}
}
}

View File

@ -0,0 +1,665 @@
package io.gitlab.jfronny.commons.serialize.databind.impl;
import java.io.Serializable;
import java.lang.reflect.*;
import java.util.*;
import static java.util.Objects.requireNonNull;
public class TypeUtils {
static final Type[] EMPTY_TYPE_ARRAY = new Type[] {};
private TypeUtils() {
throw new UnsupportedOperationException();
}
/**
* Returns a new parameterized type, applying {@code typeArguments} to {@code rawType} and
* enclosed by {@code ownerType}.
*
* @return a {@link java.io.Serializable serializable} parameterized type.
*/
public static ParameterizedType newParameterizedTypeWithOwner(
Type ownerType, Type rawType, Type... typeArguments) {
return new ParameterizedTypeImpl(ownerType, rawType, typeArguments);
}
/**
* Returns an array type whose elements are all instances of {@code componentType}.
*
* @return a {@link java.io.Serializable serializable} generic array type.
*/
public static GenericArrayType arrayOf(Type componentType) {
return new GenericArrayTypeImpl(componentType);
}
/**
* Returns a type that represents an unknown type that extends {@code bound}. For example, if
* {@code bound} is {@code CharSequence.class}, this returns {@code ? extends CharSequence}. If
* {@code bound} is {@code Object.class}, this returns {@code ?}, which is shorthand for {@code ?
* extends Object}.
*/
public static WildcardType subtypeOf(Type bound) {
Type[] upperBounds;
if (bound instanceof WildcardType) {
upperBounds = ((WildcardType) bound).getUpperBounds();
} else {
upperBounds = new Type[] {bound};
}
return new WildcardTypeImpl(upperBounds, EMPTY_TYPE_ARRAY);
}
/**
* Returns a type that represents an unknown supertype of {@code bound}. For example, if {@code
* bound} is {@code String.class}, this returns {@code ? super String}.
*/
public static WildcardType supertypeOf(Type bound) {
Type[] lowerBounds;
if (bound instanceof WildcardType) {
lowerBounds = ((WildcardType) bound).getLowerBounds();
} else {
lowerBounds = new Type[] {bound};
}
return new WildcardTypeImpl(new Type[] {Object.class}, lowerBounds);
}
/**
* Returns a type that is functionally equal but not necessarily equal according to {@link
* Object#equals(Object) Object.equals()}. The returned type is {@link java.io.Serializable}.
*/
public static Type canonicalize(Type type) {
if (type instanceof Class) {
Class<?> c = (Class<?>) type;
return c.isArray() ? new GenericArrayTypeImpl(canonicalize(c.getComponentType())) : c;
} else if (type instanceof ParameterizedType) {
ParameterizedType p = (ParameterizedType) type;
return new ParameterizedTypeImpl(
p.getOwnerType(), p.getRawType(), p.getActualTypeArguments());
} else if (type instanceof GenericArrayType) {
GenericArrayType g = (GenericArrayType) type;
return new GenericArrayTypeImpl(g.getGenericComponentType());
} else if (type instanceof WildcardType) {
WildcardType w = (WildcardType) type;
return new WildcardTypeImpl(w.getUpperBounds(), w.getLowerBounds());
} else {
// type is either serializable as-is or unsupported
return type;
}
}
public static Class<?> getRawType(Type type) {
if (type instanceof Class<?>) {
// type is a normal class.
return (Class<?>) type;
} else if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
// getRawType() returns Type instead of Class; that seems to be an API mistake,
// see https://bugs.openjdk.org/browse/JDK-8250659
Type rawType = parameterizedType.getRawType();
checkArgument(rawType instanceof Class);
return (Class<?>) rawType;
} else if (type instanceof GenericArrayType) {
Type componentType = ((GenericArrayType) type).getGenericComponentType();
return Array.newInstance(getRawType(componentType), 0).getClass();
} else if (type instanceof TypeVariable) {
// we could use the variable's bounds, but that won't work if there are multiple.
// having a raw type that's more general than necessary is okay
return Object.class;
} else if (type instanceof WildcardType) {
Type[] bounds = ((WildcardType) type).getUpperBounds();
// Currently the JLS only permits one bound for wildcards so using first bound is safe
assert bounds.length == 1;
return getRawType(bounds[0]);
} else {
String className = type == null ? "null" : type.getClass().getName();
throw new IllegalArgumentException(
"Expected a Class, ParameterizedType, or GenericArrayType, but <"
+ type
+ "> is of type "
+ className);
}
}
private static boolean equal(Object a, Object b) {
return Objects.equals(a, b);
}
/** Returns true if {@code a} and {@code b} are equal. */
public static boolean equals(Type a, Type b) {
if (a == b) {
// also handles (a == null && b == null)
return true;
} else if (a instanceof Class) {
// Class already specifies equals().
return a.equals(b);
} else if (a instanceof ParameterizedType) {
if (!(b instanceof ParameterizedType)) {
return false;
}
// TODO: save a .clone() call
ParameterizedType pa = (ParameterizedType) a;
ParameterizedType pb = (ParameterizedType) b;
return equal(pa.getOwnerType(), pb.getOwnerType())
&& pa.getRawType().equals(pb.getRawType())
&& Arrays.equals(pa.getActualTypeArguments(), pb.getActualTypeArguments());
} else if (a instanceof GenericArrayType) {
if (!(b instanceof GenericArrayType)) {
return false;
}
GenericArrayType ga = (GenericArrayType) a;
GenericArrayType gb = (GenericArrayType) b;
return equals(ga.getGenericComponentType(), gb.getGenericComponentType());
} else if (a instanceof WildcardType) {
if (!(b instanceof WildcardType)) {
return false;
}
WildcardType wa = (WildcardType) a;
WildcardType wb = (WildcardType) b;
return Arrays.equals(wa.getUpperBounds(), wb.getUpperBounds())
&& Arrays.equals(wa.getLowerBounds(), wb.getLowerBounds());
} else if (a instanceof TypeVariable) {
if (!(b instanceof TypeVariable)) {
return false;
}
TypeVariable<?> va = (TypeVariable<?>) a;
TypeVariable<?> vb = (TypeVariable<?>) b;
return Objects.equals(va.getGenericDeclaration(), vb.getGenericDeclaration())
&& va.getName().equals(vb.getName());
} else {
// This isn't a type we support. Could be a generic array type, wildcard type, etc.
return false;
}
}
public static String typeToString(Type type) {
return type instanceof Class ? ((Class<?>) type).getName() : type.toString();
}
/**
* Returns the generic supertype for {@code supertype}. For example, given a class {@code
* IntegerSet}, the result for when supertype is {@code Set.class} is {@code Set<Integer>} and the
* result when the supertype is {@code Collection.class} is {@code Collection<Integer>}.
*/
private static Type getGenericSupertype(Type context, Class<?> rawType, Class<?> supertype) {
if (supertype == rawType) {
return context;
}
// we skip searching through interfaces if unknown is an interface
if (supertype.isInterface()) {
Class<?>[] interfaces = rawType.getInterfaces();
for (int i = 0, length = interfaces.length; i < length; i++) {
if (interfaces[i] == supertype) {
return rawType.getGenericInterfaces()[i];
} else if (supertype.isAssignableFrom(interfaces[i])) {
return getGenericSupertype(rawType.getGenericInterfaces()[i], interfaces[i], supertype);
}
}
}
// check our supertypes
if (!rawType.isInterface()) {
while (rawType != Object.class) {
Class<?> rawSupertype = rawType.getSuperclass();
if (rawSupertype == supertype) {
return rawType.getGenericSuperclass();
} else if (supertype.isAssignableFrom(rawSupertype)) {
return getGenericSupertype(rawType.getGenericSuperclass(), rawSupertype, supertype);
}
rawType = rawSupertype;
}
}
// we can't resolve this further
return supertype;
}
/**
* Returns the generic form of {@code supertype}. For example, if this is {@code
* ArrayList<String>}, this returns {@code Iterable<String>} given the input {@code
* Iterable.class}.
*
* @param supertype a superclass of, or interface implemented by, this.
*/
private static Type getSupertype(Type context, Class<?> contextRawType, Class<?> supertype) {
if (context instanceof WildcardType) {
// Wildcards are useless for resolving supertypes. As the upper bound has the same raw type,
// use it instead
Type[] bounds = ((WildcardType) context).getUpperBounds();
// Currently the JLS only permits one bound for wildcards so using first bound is safe
assert bounds.length == 1;
context = bounds[0];
}
checkArgument(supertype.isAssignableFrom(contextRawType));
return resolve(
context,
contextRawType,
TypeUtils.getGenericSupertype(context, contextRawType, supertype));
}
/**
* Returns the component type of this array type.
*
* @throws ClassCastException if this type is not an array.
*/
public static Type getArrayComponentType(Type array) {
return array instanceof GenericArrayType
? ((GenericArrayType) array).getGenericComponentType()
: ((Class<?>) array).getComponentType();
}
/**
* Returns the element type of this collection type.
*
* @throws IllegalArgumentException if this type is not a collection.
*/
public static Type getCollectionElementType(Type context, Class<?> contextRawType) {
Type collectionType = getSupertype(context, contextRawType, Collection.class);
if (collectionType instanceof ParameterizedType) {
return ((ParameterizedType) collectionType).getActualTypeArguments()[0];
}
return Object.class;
}
/**
* Returns a two element array containing this map's key and value types in positions 0 and 1
* respectively.
*/
public static Type[] getMapKeyAndValueTypes(Type context, Class<?> contextRawType) {
/*
* Work around a problem with the declaration of java.util.Properties. That
* class should extend Hashtable<String, String>, but it's declared to
* extend Hashtable<Object, Object>.
*/
if (context == Properties.class) {
return new Type[] {String.class, String.class}; // TODO: test subclasses of Properties!
}
Type mapType = getSupertype(context, contextRawType, Map.class);
// TODO: strip wildcards?
if (mapType instanceof ParameterizedType) {
ParameterizedType mapParameterizedType = (ParameterizedType) mapType;
return mapParameterizedType.getActualTypeArguments();
}
return new Type[] {Object.class, Object.class};
}
public static Type resolve(Type context, Class<?> contextRawType, Type toResolve) {
return resolve(context, contextRawType, toResolve, new HashMap<TypeVariable<?>, Type>());
}
private static Type resolve(
Type context,
Class<?> contextRawType,
Type toResolve,
Map<TypeVariable<?>, Type> visitedTypeVariables) {
// this implementation is made a little more complicated in an attempt to avoid object-creation
TypeVariable<?> resolving = null;
while (true) {
if (toResolve instanceof TypeVariable) {
TypeVariable<?> typeVariable = (TypeVariable<?>) toResolve;
Type previouslyResolved = visitedTypeVariables.get(typeVariable);
if (previouslyResolved != null) {
// cannot reduce due to infinite recursion
return (previouslyResolved == Void.TYPE) ? toResolve : previouslyResolved;
}
// Insert a placeholder to mark the fact that we are in the process of resolving this type
visitedTypeVariables.put(typeVariable, Void.TYPE);
if (resolving == null) {
resolving = typeVariable;
}
toResolve = resolveTypeVariable(context, contextRawType, typeVariable);
if (toResolve == typeVariable) {
break;
}
} else if (toResolve instanceof Class && ((Class<?>) toResolve).isArray()) {
Class<?> original = (Class<?>) toResolve;
Type componentType = original.getComponentType();
Type newComponentType =
resolve(context, contextRawType, componentType, visitedTypeVariables);
toResolve = equal(componentType, newComponentType) ? original : arrayOf(newComponentType);
break;
} else if (toResolve instanceof GenericArrayType) {
GenericArrayType original = (GenericArrayType) toResolve;
Type componentType = original.getGenericComponentType();
Type newComponentType =
resolve(context, contextRawType, componentType, visitedTypeVariables);
toResolve = equal(componentType, newComponentType) ? original : arrayOf(newComponentType);
break;
} else if (toResolve instanceof ParameterizedType) {
ParameterizedType original = (ParameterizedType) toResolve;
Type ownerType = original.getOwnerType();
Type newOwnerType = resolve(context, contextRawType, ownerType, visitedTypeVariables);
boolean changed = !equal(newOwnerType, ownerType);
Type[] args = original.getActualTypeArguments();
for (int t = 0, length = args.length; t < length; t++) {
Type resolvedTypeArgument =
resolve(context, contextRawType, args[t], visitedTypeVariables);
if (!equal(resolvedTypeArgument, args[t])) {
if (!changed) {
args = args.clone();
changed = true;
}
args[t] = resolvedTypeArgument;
}
}
toResolve =
changed
? newParameterizedTypeWithOwner(newOwnerType, original.getRawType(), args)
: original;
break;
} else if (toResolve instanceof WildcardType) {
WildcardType original = (WildcardType) toResolve;
Type[] originalLowerBound = original.getLowerBounds();
Type[] originalUpperBound = original.getUpperBounds();
if (originalLowerBound.length == 1) {
Type lowerBound =
resolve(context, contextRawType, originalLowerBound[0], visitedTypeVariables);
if (lowerBound != originalLowerBound[0]) {
toResolve = supertypeOf(lowerBound);
break;
}
} else if (originalUpperBound.length == 1) {
Type upperBound =
resolve(context, contextRawType, originalUpperBound[0], visitedTypeVariables);
if (upperBound != originalUpperBound[0]) {
toResolve = subtypeOf(upperBound);
break;
}
}
toResolve = original;
break;
} else {
break;
}
}
// ensure that any in-process resolution gets updated with the final result
if (resolving != null) {
visitedTypeVariables.put(resolving, toResolve);
}
return toResolve;
}
private static Type resolveTypeVariable(
Type context, Class<?> contextRawType, TypeVariable<?> unknown) {
Class<?> declaredByRaw = declaringClassOf(unknown);
// we can't reduce this further
if (declaredByRaw == null) {
return unknown;
}
Type declaredBy = getGenericSupertype(context, contextRawType, declaredByRaw);
if (declaredBy instanceof ParameterizedType) {
int index = indexOf(declaredByRaw.getTypeParameters(), unknown);
return ((ParameterizedType) declaredBy).getActualTypeArguments()[index];
}
return unknown;
}
private static int indexOf(Object[] array, Object toFind) {
for (int i = 0, length = array.length; i < length; i++) {
if (toFind.equals(array[i])) {
return i;
}
}
throw new NoSuchElementException();
}
/**
* Returns the declaring class of {@code typeVariable}, or {@code null} if it was not declared by
* a class.
*/
private static Class<?> declaringClassOf(TypeVariable<?> typeVariable) {
GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration();
return genericDeclaration instanceof Class ? (Class<?>) genericDeclaration : null;
}
static void checkNotPrimitive(Type type) {
checkArgument(!(type instanceof Class<?>) || !((Class<?>) type).isPrimitive());
}
/**
* Whether an {@linkplain ParameterizedType#getOwnerType() owner type} must be specified when
* constructing a {@link ParameterizedType} for {@code rawType}.
*
* <p>Note that this method might not require an owner type for all cases where Java reflection
* would create parameterized types with owner type.
*/
public static boolean requiresOwnerType(Type rawType) {
if (rawType instanceof Class<?>) {
Class<?> rawTypeAsClass = (Class<?>) rawType;
return !Modifier.isStatic(rawTypeAsClass.getModifiers())
&& rawTypeAsClass.getDeclaringClass() != null;
}
return false;
}
public static <T> Optional<T> instantiate(Class<?> klazz) {
try {
return Optional.of((T) klazz.getConstructor().newInstance());
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
return Optional.empty();
}
}
// Here and below we put @SuppressWarnings("serial") on fields of type `Type`. Recent Java
// compilers complain that the declared type is not Serializable. But in this context we go out of
// our way to ensure that the Type in question is either Class (which is serializable) or one of
// the nested Type implementations here (which are also serializable).
private static final class ParameterizedTypeImpl implements ParameterizedType, Serializable {
@SuppressWarnings("serial")
private final Type ownerType;
@SuppressWarnings("serial")
private final Type rawType;
@SuppressWarnings("serial")
private final Type[] typeArguments;
public ParameterizedTypeImpl(Type ownerType, Type rawType, Type... typeArguments) {
// TODO: Should this enforce that rawType is a Class? See JDK implementation of
// the ParameterizedType interface and https://bugs.openjdk.org/browse/JDK-8250659
requireNonNull(rawType);
if (ownerType == null && requiresOwnerType(rawType)) {
throw new IllegalArgumentException("Must specify owner type for " + rawType);
}
this.ownerType = ownerType == null ? null : canonicalize(ownerType);
this.rawType = canonicalize(rawType);
this.typeArguments = typeArguments.clone();
for (int t = 0, length = this.typeArguments.length; t < length; t++) {
requireNonNull(this.typeArguments[t]);
checkNotPrimitive(this.typeArguments[t]);
this.typeArguments[t] = canonicalize(this.typeArguments[t]);
}
}
@Override
public Type[] getActualTypeArguments() {
return typeArguments.clone();
}
@Override
public Type getRawType() {
return rawType;
}
@Override
public Type getOwnerType() {
return ownerType;
}
@Override
public boolean equals(Object other) {
return other instanceof ParameterizedType
&& TypeUtils.equals(this, (ParameterizedType) other);
}
private static int hashCodeOrZero(Object o) {
return o != null ? o.hashCode() : 0;
}
@Override
public int hashCode() {
return Arrays.hashCode(typeArguments) ^ rawType.hashCode() ^ hashCodeOrZero(ownerType);
}
@Override
public String toString() {
int length = typeArguments.length;
if (length == 0) {
return typeToString(rawType);
}
StringBuilder stringBuilder = new StringBuilder(30 * (length + 1));
stringBuilder
.append(typeToString(rawType))
.append("<")
.append(typeToString(typeArguments[0]));
for (int i = 1; i < length; i++) {
stringBuilder.append(", ").append(typeToString(typeArguments[i]));
}
return stringBuilder.append(">").toString();
}
private static final long serialVersionUID = 0;
}
private static final class GenericArrayTypeImpl implements GenericArrayType, Serializable {
@SuppressWarnings("serial")
private final Type componentType;
public GenericArrayTypeImpl(Type componentType) {
requireNonNull(componentType);
this.componentType = canonicalize(componentType);
}
@Override
public Type getGenericComponentType() {
return componentType;
}
@Override
public boolean equals(Object o) {
return o instanceof GenericArrayType && TypeUtils.equals(this, (GenericArrayType) o);
}
@Override
public int hashCode() {
return componentType.hashCode();
}
@Override
public String toString() {
return typeToString(componentType) + "[]";
}
private static final long serialVersionUID = 0;
}
/**
* The WildcardType interface supports multiple upper bounds and multiple lower bounds. We only
* support what the target Java version supports - at most one bound, see also
* https://bugs.openjdk.java.net/browse/JDK-8250660. If a lower bound is set, the upper bound must
* be Object.class.
*/
private static final class WildcardTypeImpl implements WildcardType, Serializable {
@SuppressWarnings("serial")
private final Type upperBound;
@SuppressWarnings("serial")
private final Type lowerBound;
public WildcardTypeImpl(Type[] upperBounds, Type[] lowerBounds) {
checkArgument(lowerBounds.length <= 1);
checkArgument(upperBounds.length == 1);
if (lowerBounds.length == 1) {
requireNonNull(lowerBounds[0]);
checkNotPrimitive(lowerBounds[0]);
checkArgument(upperBounds[0] == Object.class);
this.lowerBound = canonicalize(lowerBounds[0]);
this.upperBound = Object.class;
} else {
requireNonNull(upperBounds[0]);
checkNotPrimitive(upperBounds[0]);
this.lowerBound = null;
this.upperBound = canonicalize(upperBounds[0]);
}
}
@Override
public Type[] getUpperBounds() {
return new Type[] {upperBound};
}
@Override
public Type[] getLowerBounds() {
return lowerBound != null ? new Type[] {lowerBound} : EMPTY_TYPE_ARRAY;
}
@Override
public boolean equals(Object other) {
return other instanceof WildcardType && TypeUtils.equals(this, (WildcardType) other);
}
@Override
public int hashCode() {
// this equals Arrays.hashCode(getLowerBounds()) ^ Arrays.hashCode(getUpperBounds());
return (lowerBound != null ? 31 + lowerBound.hashCode() : 1) ^ (31 + upperBound.hashCode());
}
@Override
public String toString() {
if (lowerBound != null) {
return "? super " + typeToString(lowerBound);
} else if (upperBound == Object.class) {
return "?";
} else {
return "? extends " + typeToString(upperBound);
}
}
private static final long serialVersionUID = 0;
}
public static void checkArgument(boolean condition) {
if (!condition) {
throw new IllegalArgumentException();
}
}
}

View File

@ -0,0 +1,93 @@
package io.gitlab.jfronny.commons.serialize.databind.impl.adapter;
import io.gitlab.jfronny.commons.serialize.databind.ObjectMapper;
import io.gitlab.jfronny.commons.serialize.databind.TypeAdapter;
import io.gitlab.jfronny.commons.serialize.databind.TypeAdapterFactory;
import io.gitlab.jfronny.commons.serialize.databind.TypeToken;
import io.gitlab.jfronny.commons.serialize.databind.impl.TypeUtils;
import io.gitlab.jfronny.commons.serialize.stream.MalformedDataException;
import io.gitlab.jfronny.commons.serialize.stream.SerializeReader;
import io.gitlab.jfronny.commons.serialize.stream.SerializeWriter;
import io.gitlab.jfronny.commons.serialize.stream.Token;
import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Type;
import java.util.ArrayList;
public class ArrayTypeAdapterFactory implements TypeAdapterFactory {
@Override
public <T> TypeAdapter<T> create(ObjectMapper mapper, TypeToken<T> typeToken) {
Type type = typeToken.getType();
if (!(type instanceof GenericArrayType || (type instanceof Class<?> cl && cl.isArray()))) {
return null;
}
Type componentType = TypeUtils.getArrayComponentType(type);
TypeAdapter<?> componentTypeAdapter = mapper.getAdapter(TypeToken.get(componentType));
//noinspection rawtypes,unchecked
return new ArrayTypeAdapter(mapper, componentTypeAdapter, TypeUtils.getRawType(componentType));
}
private static class ArrayTypeAdapter<E> extends TypeAdapter<Object> {
private final Class<E> componentType;
private final TypeAdapter<E> componentTypeAdapter;
public ArrayTypeAdapter(ObjectMapper context, TypeAdapter<E> componentTypeAdapter, Class<E> componentType) {
this.componentTypeAdapter = new TypeAdapterRuntimeTypeWrapper<>(context, componentTypeAdapter, componentType);
this.componentType = componentType;
}
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(Object value, Writer writer) throws TEx, MalformedDataException {
if (value == null) {
writer.nullValue();
return;
}
writer.beginArray();
int length = Array.getLength(value);
for (int i = 0; i < length; i++) {
E element = (E) Array.get(value, i);
componentTypeAdapter.serialize(element, writer);
}
writer.endArray();
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> Object deserialize(Reader reader) throws TEx, MalformedDataException {
if (reader.peek() == Token.NULL) {
reader.nextNull();
return null;
}
if (reader.isLenient() && reader.peek() != Token.BEGIN_ARRAY) {
// Coerce
Object array = Array.newInstance(componentType, 1);
Array.set(array, 0, componentTypeAdapter.deserialize(reader));
return array;
}
ArrayList<E> list = new ArrayList<>();
reader.beginArray();
while (reader.hasNext()) {
E instance = componentTypeAdapter.deserialize(reader);
list.add(instance);
}
reader.endArray();
int size = list.size();
// Have to copy primitives one by one to primitive array
if (componentType.isPrimitive()) {
Object array = Array.newInstance(componentType, size);
for (int i = 0; i < size; i++) {
Array.set(array, i, list.get(i));
}
return array;
}
// But for Object[] can use ArrayList.toArray
else {
@SuppressWarnings("unchecked")
E[] array = (E[]) Array.newInstance(componentType, size);
return list.toArray(array);
}
}
}
}

View File

@ -0,0 +1,81 @@
package io.gitlab.jfronny.commons.serialize.databind.impl.adapter;
import io.gitlab.jfronny.commons.serialize.databind.SerializerFor;
import io.gitlab.jfronny.commons.serialize.databind.TypeAdapter;
import io.gitlab.jfronny.commons.serialize.databind.impl.ISO8601Utils;
import io.gitlab.jfronny.commons.serialize.databind.impl.PreJava9DateFormatProvider;
import io.gitlab.jfronny.commons.serialize.stream.MalformedDataException;
import io.gitlab.jfronny.commons.serialize.stream.SerializeReader;
import io.gitlab.jfronny.commons.serialize.stream.SerializeWriter;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.*;
@SerializerFor(targets = Date.class)
public class DefaultDateTypeAdapter extends TypeAdapter<Date> {
private static final String SIMPLE_NAME = "DefaultDateTypeAdapter";
/**
* List of 1 or more different date formats used for de-serialization attempts. The first of them
* is used for serialization as well.
*/
private final List<DateFormat> dateFormats = new ArrayList<>();
DefaultDateTypeAdapter() {
dateFormats.add(DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.US));
if (!Locale.getDefault().equals(Locale.US)) {
dateFormats.add(DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT));
}
dateFormats.add(PreJava9DateFormatProvider.getUsDateTimeFormat(DateFormat.DEFAULT, DateFormat.DEFAULT));
}
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(Date value, Writer writer) throws TEx, MalformedDataException {
DateFormat dateFormat = dateFormats.getFirst();
String dateFormatAsString;
// Needs to be synchronized since JDK DateFormat classes are not thread-safe
synchronized (dateFormats) {
dateFormatAsString = dateFormat.format(value);
}
writer.value(dateFormatAsString);
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> Date deserialize(Reader reader) throws TEx, MalformedDataException {
String s = reader.nextString();
// Needs to be synchronized since JDK DateFormat classes are not thread-safe
synchronized (dateFormats) {
for (DateFormat dateFormat : dateFormats) {
TimeZone originalTimeZone = dateFormat.getTimeZone();
try {
return dateFormat.parse(s);
} catch (ParseException ignored) {
// OK: try the next format
} finally {
dateFormat.setTimeZone(originalTimeZone);
}
}
}
try {
return ISO8601Utils.parse(s, new ParsePosition(0));
} catch (ParseException e) {
throw new MalformedDataException(
"Failed parsing '" + s + "' as Date; at path " + reader.getPreviousPath(), e);
}
}
@Override
public String toString() {
DateFormat defaultFormat = dateFormats.getFirst();
if (defaultFormat instanceof SimpleDateFormat) {
return SIMPLE_NAME + '(' + ((SimpleDateFormat) defaultFormat).toPattern() + ')';
} else {
return SIMPLE_NAME + '(' + defaultFormat.getClass().getSimpleName() + ')';
}
}
}

View File

@ -0,0 +1,90 @@
package io.gitlab.jfronny.commons.serialize.databind.impl.adapter;
import io.gitlab.jfronny.commons.serialize.annotations.SerializedName;
import io.gitlab.jfronny.commons.serialize.databind.ObjectMapper;
import io.gitlab.jfronny.commons.serialize.databind.TypeAdapter;
import io.gitlab.jfronny.commons.serialize.databind.TypeAdapterFactory;
import io.gitlab.jfronny.commons.serialize.databind.TypeToken;
import io.gitlab.jfronny.commons.serialize.stream.MalformedDataException;
import io.gitlab.jfronny.commons.serialize.stream.SerializeReader;
import io.gitlab.jfronny.commons.serialize.stream.SerializeWriter;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class EnumTypeAdapterFactory implements TypeAdapterFactory {
@Override
public <T> TypeAdapter<T> create(ObjectMapper mapper, TypeToken<T> type) {
Class<? super T> rawRype = type.getRawType();
if (!Enum.class.isAssignableFrom(rawRype) || rawRype == Enum.class) {
return null;
}
if (!rawRype.isEnum()) {
rawRype = rawRype.getSuperclass();
}
return new EnumTypeAdapter(rawRype);
}
private static class EnumTypeAdapter<T extends Enum<T>> extends TypeAdapter<T> {
private final Map<String, T> nameToConstant = new HashMap<>();
private final Map<String, T> stringToConstant = new HashMap<>();
private final Map<T, String> constantToName = new HashMap<>();
public EnumTypeAdapter(final Class<T> classOfT) {
try {
// Uses reflection to find enum constants to work around name mismatches for obfuscated
// classes
// Reflection access might throw SecurityException, therefore run this in privileged
// context; should be acceptable because this only retrieves enum constants, but does not
// expose anything else
Field[] constantFields;
{
Field[] fields = classOfT.getDeclaredFields();
ArrayList<Field> constantFieldsList = new ArrayList<>(fields.length);
for (Field f : fields) {
if (f.isEnumConstant()) {
constantFieldsList.add(f);
}
}
constantFields = constantFieldsList.toArray(new Field[0]);
AccessibleObject.setAccessible(constantFields, true);
}
for (Field constantField : constantFields) {
@SuppressWarnings("unchecked")
T constant = (T) constantField.get(null);
String name = constant.name();
String toStringVal = constant.toString();
SerializedName annotation = constantField.getAnnotation(SerializedName.class);
if (annotation != null) {
name = annotation.value();
for (String alternate : annotation.alternate()) {
nameToConstant.put(alternate, constant);
}
}
nameToConstant.put(name, constant);
stringToConstant.put(toStringVal, constant);
constantToName.put(constant, name);
}
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
}
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(T value, Writer writer) throws TEx, MalformedDataException {
writer.value(value == null ? null : constantToName.get(value));
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> T deserialize(Reader reader) throws TEx, MalformedDataException {
String key = reader.nextString();
T constant = nameToConstant.get(key);
return (constant == null) ? stringToConstant.get(key) : constant;
}
}
}

View File

@ -0,0 +1,138 @@
package io.gitlab.jfronny.commons.serialize.databind.impl.adapter;
import io.gitlab.jfronny.commons.serialize.databind.ObjectMapper;
import io.gitlab.jfronny.commons.serialize.databind.TypeAdapter;
import io.gitlab.jfronny.commons.serialize.databind.TypeAdapterFactory;
import io.gitlab.jfronny.commons.serialize.databind.TypeToken;
import io.gitlab.jfronny.commons.serialize.databind.impl.MapKeyReader;
import io.gitlab.jfronny.commons.serialize.databind.impl.MapKeyWriter;
import io.gitlab.jfronny.commons.serialize.databind.impl.TypeUtils;
import io.gitlab.jfronny.commons.serialize.stream.MalformedDataException;
import io.gitlab.jfronny.commons.serialize.stream.SerializeReader;
import io.gitlab.jfronny.commons.serialize.stream.SerializeWriter;
import io.gitlab.jfronny.commons.serialize.stream.Token;
import io.gitlab.jfronny.commons.tuple.Tuple;
import java.lang.reflect.Type;
import java.util.*;
public class MapTypeAdapterFactory implements TypeAdapterFactory {
@Override
public <T> TypeAdapter<T> create(ObjectMapper mapper, TypeToken<T> typeToken) {
Type type = typeToken.getType();
Class<? super T> rawType = typeToken.getRawType();
if (!Map.class.isAssignableFrom(rawType)) {
return null;
}
Type[] keyAndValueTypes = TypeUtils.getMapKeyAndValueTypes(type, rawType);
TypeAdapter<?> keyAdapter = mapper.getAdapter(TypeToken.get(keyAndValueTypes[0]));
TypeAdapter<?> valueAdapter = mapper.getAdapter(TypeToken.get(keyAndValueTypes[1]));
@SuppressWarnings({"unchecked", "rawtypes"})
// we don't define a type parameter for the key or value types
TypeAdapter<T> result = new MapTypeAdapter(mapper, keyAndValueTypes[0], keyAdapter, keyAndValueTypes[1], valueAdapter);
return result;
}
private static class MapTypeAdapter<K, V> extends TypeAdapter<Map<K, V>> {
private final TypeAdapter<K> keyTypeAdapter;
private final TypeAdapter<V> valueTypeAdapter;
public MapTypeAdapter(
ObjectMapper context,
Type keyType,
TypeAdapter<K> keyTypeAdapter,
Type valueType,
TypeAdapter<V> valueTypeAdapter) {
this.keyTypeAdapter = new TypeAdapterRuntimeTypeWrapper<>(context, keyTypeAdapter, keyType);
this.valueTypeAdapter =
new TypeAdapterRuntimeTypeWrapper<>(context, valueTypeAdapter, valueType);
}
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(Map<K, V> value, Writer writer) throws TEx, MalformedDataException {
writer.beginObject();
Map<String, Tuple<V, List<String>>> tmp = new LinkedHashMap<>();
List<Map.Entry<K, V>> toWrite = new ArrayList<>(value.entrySet());
int written = 0;
for (Map.Entry<K, V> entry : toWrite) {
try (MapKeyWriter keyWriter = new MapKeyWriter()) {
try {
keyTypeAdapter.serialize(entry.getKey(), keyWriter);
} catch (MalformedDataException e) {
if (keyWriter.isFailureOrigin()) {
// This mode of serialization won't work for the type in use. Try again with array serialization.
writer.beginArray();
// Write all entries that have been written so far
for (Map.Entry<String, Tuple<V, List<String>>> se : tmp.entrySet()) {
for (String comment : se.getValue().right()) {
writer.comment(comment);
}
writer.name(se.getKey());
valueTypeAdapter.serialize(se.getValue().left(), writer);
}
// Write the rest of the entries
for (Map.Entry<K, V> se : toWrite) {
if (written-- > 0) continue;
writer.beginArray();
keyTypeAdapter.serialize(se.getKey(), writer);
valueTypeAdapter.serialize(se.getValue(), writer);
writer.endArray();
}
writer.endArray();
return;
}
}
tmp.put(keyWriter.getResult(), Tuple.of(entry.getValue(), keyWriter.getComments()));
written++;
}
}
for (Map.Entry<String, Tuple<V, List<String>>> entry : tmp.entrySet()) {
for (String comment : entry.getValue().right()) {
writer.comment(comment);
}
writer.name(entry.getKey());
valueTypeAdapter.serialize(entry.getValue().left(), writer);
}
writer.endObject();
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> Map<K, V> deserialize(Reader reader) throws TEx, MalformedDataException {
Token peek = reader.peek();
Map<K, V> map = new HashMap<>();
if (peek == Token.BEGIN_ARRAY) {
reader.beginArray();
while (reader.hasNext()) {
reader.beginArray(); // entry array
K key = keyTypeAdapter.deserialize(reader);
V value = valueTypeAdapter.deserialize(reader);
if (map.put(key, value) != null && !reader.isLenient()) {
throw new MalformedDataException("duplicate key: " + key);
}
reader.endArray();
}
reader.endArray();
return map;
} else if (peek == Token.BEGIN_OBJECT) {
reader.beginObject();
while (reader.hasNext()) {
K key;
try (MapKeyReader keyReader = new MapKeyReader(reader.getPath(), reader.getPreviousPath(), reader.nextName())) {
key = keyTypeAdapter.deserialize(keyReader);
}
V value = valueTypeAdapter.deserialize(reader);
if (map.put(key, value) != null && !reader.isLenient()) {
throw new MalformedDataException("duplicate key: " + key);
}
}
reader.endObject();
return map;
} else {
throw new MalformedDataException("Expected object or array but was " + peek);
}
}
}
}

View File

@ -0,0 +1,30 @@
package io.gitlab.jfronny.commons.serialize.databind.impl.adapter;
import io.gitlab.jfronny.commons.serialize.databind.*;
import io.gitlab.jfronny.commons.serialize.databind.impl.TypeUtils;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class SerializationAdapterTypeAdapterFactory implements TypeAdapterFactory {
private final ConcurrentMap<Class<?>, Optional<TypeAdapterFactory>> adapterFactoryMap = new ConcurrentHashMap<>();
@Override
public <T> TypeAdapter<T> create(ObjectMapper mapper, TypeToken<T> type) {
SerializeWithAdapter adapter = type.getRawType().getAnnotation(SerializeWithAdapter.class);
if (adapter == null) {
return null;
}
Optional<TypeAdapter<T>> result;
if (TypeAdapter.class.isAssignableFrom(adapter.adapter())) {
result = TypeUtils.instantiate(adapter.adapter());
} else if (TypeAdapterFactory.class.isAssignableFrom(adapter.adapter())) {
result = adapterFactoryMap.computeIfAbsent(type.getRawType(), TypeUtils::instantiate)
.map(factory -> factory.create(mapper, type));
} else {
result = Optional.empty();
}
return result.map(instance -> adapter.nullSafe() ? instance.nullSafe() : instance).orElse(null);
}
}

View File

@ -0,0 +1,7 @@
package io.gitlab.jfronny.commons.serialize.databind.impl.adapter;
import io.gitlab.jfronny.commons.serialize.databind.TypeAdapter;
public abstract class SerializationDelegatingTypeAdapter<T> extends TypeAdapter<T> {
public abstract TypeAdapter<T> getSerializationDelegate();
}

View File

@ -0,0 +1,30 @@
package io.gitlab.jfronny.commons.serialize.databind.impl.adapter;
import io.gitlab.jfronny.commons.serialize.databind.ObjectMapper;
import io.gitlab.jfronny.commons.serialize.databind.TypeAdapter;
import io.gitlab.jfronny.commons.serialize.stream.SerializeReader;
import io.gitlab.jfronny.commons.serialize.stream.SerializeWriter;
import java.lang.reflect.Type;
public class TypeAdapterRuntimeTypeWrapper<T> extends TypeAdapter<T> {
private final ObjectMapper context;
private final TypeAdapter<T> delegate;
private final Type type;
TypeAdapterRuntimeTypeWrapper(ObjectMapper context, TypeAdapter<T> delegate, Type type) {
this.context = context;
this.delegate = delegate;
this.type = type;
}
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(T value, Writer writer) throws TEx {
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> T deserialize(Reader reader) throws TEx {
return null;
}
}

View File

@ -0,0 +1,604 @@
package io.gitlab.jfronny.commons.serialize.databind.impl.adapter;
import io.gitlab.jfronny.commons.data.LazilyParsedNumber;
import io.gitlab.jfronny.commons.data.NumberLimits;
import io.gitlab.jfronny.commons.serialize.databind.SerializerFor;
import io.gitlab.jfronny.commons.serialize.databind.TypeAdapter;
import io.gitlab.jfronny.commons.serialize.stream.MalformedDataException;
import io.gitlab.jfronny.commons.serialize.stream.SerializeReader;
import io.gitlab.jfronny.commons.serialize.stream.SerializeWriter;
import io.gitlab.jfronny.commons.serialize.stream.Token;
import io.gitlab.jfronny.commons.serialize.stream.emulated.DataElement;
import io.gitlab.jfronny.commons.serialize.stream.emulated.DataElementSerializer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.URI;
import java.net.URL;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerArray;
public class TypeAdapters {
@SerializerFor(targets = {boolean.class, Boolean.class})
public static class BooleanTypeAdapter extends TypeAdapter<Boolean> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(Boolean value, Writer writer) throws TEx, MalformedDataException {
writer.value(value);
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> Boolean deserialize(Reader reader) throws TEx, MalformedDataException {
if (reader.peek() == Token.STRING) {
// special casing for boolean strings
String value = reader.nextString();
if (value.equalsIgnoreCase("true")) {
return true;
} else if (value.equalsIgnoreCase("false")) {
return false;
} else {
throw new MalformedDataException("Expected boolean, got " + value);
}
}
return reader.nextBoolean();
}
}
@SerializerFor(targets = {byte.class, Byte.class})
public static class ByteTypeAdapter extends TypeAdapter<Byte> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(Byte value, Writer writer) throws TEx, MalformedDataException {
writer.value(value);
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> Byte deserialize(Reader reader) throws TEx, MalformedDataException {
int value;
try {
value = reader.nextInt();
} catch (NumberFormatException e) {
throw new MalformedDataException(e);
}
// Up to 255 to support unsigned values
if (value < Byte.MIN_VALUE || value > 255) {
throw new MalformedDataException("Value " + value + " is out of range for byte");
}
return (byte) value;
}
}
@SerializerFor(targets = {short.class, Short.class})
public static class ShortTypeAdapter extends TypeAdapter<Short> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(Short value, Writer writer) throws TEx, MalformedDataException {
writer.value(value);
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> Short deserialize(Reader reader) throws TEx, MalformedDataException {
int value;
try {
value = reader.nextInt();
} catch (NumberFormatException e) {
throw new MalformedDataException(e);
}
// Up to 65535 to support unsigned values
if (value < Short.MIN_VALUE || value > 65535) {
throw new MalformedDataException("Value " + value + " is out of range for short");
}
return (short) value;
}
}
@SerializerFor(targets = {int.class, Integer.class})
public static class IntegerTypeAdapter extends TypeAdapter<Integer> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(Integer value, Writer writer) throws TEx, MalformedDataException {
writer.value(value);
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> Integer deserialize(Reader reader) throws TEx, MalformedDataException {
try {
return reader.nextInt();
} catch (NumberFormatException e) {
throw new MalformedDataException(e);
}
}
}
@SerializerFor(targets = {long.class, Long.class})
public static class LongTypeAdapter extends TypeAdapter<Long> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(Long value, Writer writer) throws TEx, MalformedDataException {
writer.value(value);
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> Long deserialize(Reader reader) throws TEx, MalformedDataException {
try {
return reader.nextLong();
} catch (NumberFormatException e) {
throw new MalformedDataException(e);
}
}
}
@SerializerFor(targets = {float.class, Float.class})
public static class FloatTypeAdapter extends TypeAdapter<Float> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(Float value, Writer writer) throws TEx, MalformedDataException {
writer.value(value);
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> Float deserialize(Reader reader) throws TEx, MalformedDataException {
try {
return (float) reader.nextDouble();
} catch (NumberFormatException e) {
throw new MalformedDataException(e);
}
}
}
@SerializerFor(targets = {double.class, Double.class})
public static class DoubleTypeAdapter extends TypeAdapter<Double> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(Double value, Writer writer) throws TEx, MalformedDataException {
writer.value(value);
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> Double deserialize(Reader reader) throws TEx, MalformedDataException {
try {
return reader.nextDouble();
} catch (NumberFormatException e) {
throw new MalformedDataException(e);
}
}
}
@SerializerFor(targets = {char.class, Character.class})
public static class CharacterTypeAdapter extends TypeAdapter<Character> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(Character value, Writer writer) throws TEx, MalformedDataException {
writer.value(String.valueOf(value));
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> Character deserialize(Reader reader) throws TEx, MalformedDataException {
String value = reader.nextString();
if (value.length() != 1) {
throw new MalformedDataException("Expected single character, got " + value);
}
return value.charAt(0);
}
}
@SerializerFor(targets = String.class)
public static class StringTypeAdapter extends TypeAdapter<String> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(String value, Writer writer) throws TEx, MalformedDataException {
writer.value(value);
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> String deserialize(Reader reader) throws TEx, MalformedDataException {
return reader.nextString();
}
}
@SerializerFor(targets = BitSet.class)
public static class BitSetTypeAdapter extends TypeAdapter<BitSet> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(BitSet value, Writer writer) throws TEx {
writer.beginArray();
for (int i = 0; i < value.length(); i++) {
writer.value(value.get(i) ? 1 : 0);
}
writer.endArray();
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> BitSet deserialize(Reader reader) throws TEx, MalformedDataException {
BitSet bitset = new BitSet();
reader.beginArray();
int i = 0;
Token tokenType = reader.peek();
while (tokenType != Token.END_ARRAY) {
boolean set;
switch (tokenType) {
case NUMBER:
case STRING:
int intValue = reader.nextInt();
if (intValue == 0) {
set = false;
} else if (intValue == 1) {
set = true;
} else {
throw new MalformedDataException(
"Invalid bitset value "
+ intValue
+ ", expected 0 or 1; at path "
+ reader.getPreviousPath());
}
break;
case BOOLEAN:
set = reader.nextBoolean();
break;
default:
throw new MalformedDataException(
"Invalid bitset value type: " + tokenType + "; at path " + reader.getPath());
}
if (set) {
bitset.set(i);
}
++i;
tokenType = reader.peek();
}
reader.endArray();
return bitset;
}
}
@SerializerFor(targets = AtomicInteger.class)
public static class AtomicIntegerTypeAdapter extends TypeAdapter<AtomicInteger> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(AtomicInteger value, Writer writer) throws TEx {
writer.value(value.get());
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> AtomicInteger deserialize(Reader reader) throws TEx, MalformedDataException {
try {
return new AtomicInteger(reader.nextInt());
} catch (NumberFormatException e) {
throw new MalformedDataException(e);
}
}
}
@SerializerFor(targets = AtomicBoolean.class)
public static class AtomicBooleanTypeAdapter extends TypeAdapter<AtomicBoolean> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(AtomicBoolean value, Writer writer) throws TEx {
writer.value(value.get());
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> AtomicBoolean deserialize(Reader reader) throws TEx, MalformedDataException {
return new AtomicBoolean(reader.nextBoolean());
}
}
@SerializerFor(targets = AtomicIntegerArray.class)
public static class AtomicIntegerArrayTypeAdapter extends TypeAdapter<AtomicIntegerArray> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(AtomicIntegerArray value, Writer writer) throws TEx {
writer.beginArray();
for (int i = 0; i < value.length(); i++) {
writer.value(value.get(i));
}
writer.endArray();
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> AtomicIntegerArray deserialize(Reader reader) throws TEx, MalformedDataException {
if (reader.isLenient() && reader.peek() != Token.BEGIN_ARRAY) {
// Coerce
return new AtomicIntegerArray(new int[]{reader.nextInt()});
}
reader.beginArray();
int length = 0;
while (reader.hasNext()) {
reader.nextInt();
length++;
}
reader.endArray();
AtomicIntegerArray array = new AtomicIntegerArray(length);
reader.beginArray();
for (int i = 0; i < length; i++) {
array.set(i, reader.nextInt());
}
reader.endArray();
return array;
}
}
@SerializerFor(targets = BigDecimal.class)
public static class BigDecimalTypeAdapter extends TypeAdapter<BigDecimal> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(BigDecimal value, Writer writer) throws TEx, MalformedDataException {
writer.value(value);
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> BigDecimal deserialize(Reader reader) throws TEx, MalformedDataException {
String value = reader.nextString();
try {
return NumberLimits.parseBigDecimal(value);
} catch (NumberFormatException e) {
throw new MalformedDataException(e);
}
}
}
@SerializerFor(targets = BigInteger.class)
public static class BigIntegerTypeAdapter extends TypeAdapter<BigInteger> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(BigInteger value, Writer writer) throws TEx, MalformedDataException {
writer.value(value);
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> BigInteger deserialize(Reader reader) throws TEx, MalformedDataException {
try {
return NumberLimits.parseBigInteger(reader.nextString());
} catch (NumberFormatException e) {
throw new MalformedDataException(e);
}
}
}
@SerializerFor(targets = Number.class)
public static class NumberTypeAdapter extends TypeAdapter<Number> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(Number value, Writer writer) throws TEx, MalformedDataException {
writer.value(value);
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> Number deserialize(Reader reader) throws TEx, MalformedDataException {
return reader.nextNumber();
}
}
@SerializerFor(targets = LazilyParsedNumber.class)
public static class LazilyParsedNumberTypeAdapter extends TypeAdapter<LazilyParsedNumber> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(LazilyParsedNumber value, Writer writer) throws TEx, MalformedDataException {
writer.value(value);
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> LazilyParsedNumber deserialize(Reader reader) throws TEx, MalformedDataException {
if (reader.peek() == Token.NUMBER) {
Number number = reader.nextNumber();
if (number instanceof LazilyParsedNumber l) return l;
return new LazilyParsedNumber(number.toString());
}
// Legacy compatibility with Gson
return new LazilyParsedNumber(reader.nextString());
}
}
@SerializerFor(targets = StringBuilder.class)
public static class StringBuilderTypeAdapter extends TypeAdapter<StringBuilder> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(StringBuilder value, Writer writer) throws TEx, MalformedDataException {
writer.value(value.toString());
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> StringBuilder deserialize(Reader reader) throws TEx, MalformedDataException {
return new StringBuilder(reader.nextString());
}
}
@SerializerFor(targets = StringBuffer.class)
public static class StringBufferTypeAdapter extends TypeAdapter<StringBuffer> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(StringBuffer value, Writer writer) throws TEx, MalformedDataException {
writer.value(value.toString());
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> StringBuffer deserialize(Reader reader) throws TEx, MalformedDataException {
return new StringBuffer(reader.nextString());
}
}
@SerializerFor(targets = URL.class)
public static class URLTypeAdapter extends TypeAdapter<URL> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(URL value, Writer writer) throws TEx, MalformedDataException {
writer.value(value.toExternalForm());
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> URL deserialize(Reader reader) throws TEx, MalformedDataException {
try {
String str = reader.nextString();
if (str.equals("null")) return null;
return new URI(str).toURL();
} catch (Exception e) {
throw new MalformedDataException(e);
}
}
}
@SerializerFor(targets = URI.class)
public static class URITypeAdapter extends TypeAdapter<URI> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(URI value, Writer writer) throws TEx, MalformedDataException {
writer.value(value.toString());
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> URI deserialize(Reader reader) throws TEx, MalformedDataException {
try {
String str = reader.nextString();
if (str.equals("null")) return null;
return new URI(str);
} catch (Exception e) {
throw new MalformedDataException(e);
}
}
}
@SerializerFor(targets = InetAddress.class)
public static class InetAddressTypeAdapter extends TypeAdapter<InetAddress> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(InetAddress value, Writer writer) throws TEx, MalformedDataException {
writer.value(value.getHostAddress());
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> InetAddress deserialize(Reader reader) throws TEx, MalformedDataException {
try {
return InetAddress.getByName(reader.nextString());
} catch (Exception e) {
throw new MalformedDataException(e);
}
}
}
@SerializerFor(targets = UUID.class)
public static class UUIDTypeAdapter extends TypeAdapter<UUID> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(UUID value, Writer writer) throws TEx, MalformedDataException {
writer.value(value.toString());
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> UUID deserialize(Reader reader) throws TEx, MalformedDataException {
try {
return UUID.fromString(reader.nextString());
} catch (IllegalArgumentException e) {
throw new MalformedDataException(e);
}
}
}
@SerializerFor(targets = Currency.class)
public static class CurrencyTypeAdapter extends TypeAdapter<Currency> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(Currency value, Writer writer) throws TEx, MalformedDataException {
writer.value(value.getCurrencyCode());
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> Currency deserialize(Reader reader) throws TEx, MalformedDataException {
return Currency.getInstance(reader.nextString());
}
}
@SerializerFor(targets = Calendar.class)
public static class CalendarTypeAdapter extends TypeAdapter<Calendar> {
private static final String YEAR = "year";
private static final String MONTH = "month";
private static final String DAY_OF_MONTH = "dayOfMonth";
private static final String HOUR_OF_DAY = "hourOfDay";
private static final String MINUTE = "minute";
private static final String SECOND = "second";
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(Calendar value, Writer writer) throws TEx, MalformedDataException {
if (value == null) {
writer.nullValue();
return;
}
writer.beginObject();
writer.name(YEAR);
writer.value(value.get(Calendar.YEAR));
writer.name(MONTH);
writer.value(value.get(Calendar.MONTH));
writer.name(DAY_OF_MONTH);
writer.value(value.get(Calendar.DAY_OF_MONTH));
writer.name(HOUR_OF_DAY);
writer.value(value.get(Calendar.HOUR_OF_DAY));
writer.name(MINUTE);
writer.value(value.get(Calendar.MINUTE));
writer.name(SECOND);
writer.value(value.get(Calendar.SECOND));
writer.endObject();
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> Calendar deserialize(Reader reader) throws TEx, MalformedDataException {
reader.beginObject();
int year = 0;
int month = 0;
int dayOfMonth = 0;
int hourOfDay = 0;
int minute = 0;
int second = 0;
while (reader.peek() != Token.END_OBJECT) {
String name = reader.nextName();
int value = reader.nextInt();
switch (name) {
case YEAR:
year = value;
break;
case MONTH:
month = value;
break;
case DAY_OF_MONTH:
dayOfMonth = value;
break;
case HOUR_OF_DAY:
hourOfDay = value;
break;
case MINUTE:
minute = value;
break;
case SECOND:
second = value;
break;
default:
// Ignore unknown JSON property
}
}
reader.endObject();
return new GregorianCalendar(year, month, dayOfMonth, hourOfDay, minute, second);
}
}
@SerializerFor(targets = Locale.class)
public static class LocaleTypeAdapter extends TypeAdapter<Locale> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(Locale value, Writer writer) throws TEx, MalformedDataException {
writer.value(value == null ? null : value.toString());
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> Locale deserialize(Reader reader) throws TEx, MalformedDataException {
String locale = reader.nextString();
StringTokenizer tokenizer = new StringTokenizer(locale, "_");
String language = null;
String country = null;
String variant = null;
if (tokenizer.hasMoreElements()) {
language = tokenizer.nextToken();
}
if (tokenizer.hasMoreElements()) {
country = tokenizer.nextToken();
}
if (tokenizer.hasMoreElements()) {
variant = tokenizer.nextToken();
}
if (country == null && variant == null) {
return new Locale(language);
} else if (variant == null) {
return new Locale(language, country);
} else {
return new Locale(language, country, variant);
}
}
}
@SerializerFor(targets = DataElement.class)
public static class DataElementTypeAdapter extends TypeAdapter<DataElement> {
@Override
public <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void serialize(DataElement value, Writer writer) throws TEx, MalformedDataException {
DataElementSerializer.serialize(value, writer);
}
@Override
public <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> DataElement deserialize(Reader reader) throws TEx, MalformedDataException {
return DataElementSerializer.deserialize(reader);
}
}
}

View File

@ -0,0 +1,8 @@
module io.gitlab.jfronny.commons.serialize.databind {
uses io.gitlab.jfronny.commons.serialize.databind.TypeAdapterFactory;
uses io.gitlab.jfronny.commons.serialize.databind.TypeAdapter;
requires io.gitlab.jfronny.commons;
requires io.gitlab.jfronny.commons.serialize;
requires static org.jetbrains.annotations;
exports io.gitlab.jfronny.commons.serialize.databind;
}

View File

@ -0,0 +1,28 @@
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$BooleanTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$ByteTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$ShortTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$IntegerTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$LongTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$FloatTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$DoubleTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$CharacterTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$StringTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$BitSetTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$AtomicIntegerTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$AtomicBooleanTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$AtomicIntegerArrayTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$BigDecimalTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$BigIntegerTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$NumberTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$LazilyParsedNumberTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$StringBuilderTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$StringBufferTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$URLTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$URITypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$InetAddressTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$UUIDTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$CurrencyTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$CalendarTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$LocaleTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.TypeAdapters$DataElementTypeAdapter
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.DefaultDateTypeAdapter

View File

@ -0,0 +1,4 @@
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.SerializationAdapterTypeAdapterFactory
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.ArrayTypeAdapterFactory
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.EnumTypeAdapterFactory
io.gitlab.jfronny.commons.serialize.databind.impl.adapter.MapTypeAdapterFactory

View File

@ -1,6 +1,6 @@
package io.gitlab.jfronny.commons.serialize.gson.impl;
import io.gitlab.jfronny.commons.serialize.gson.api.v2.Ignore;
import io.gitlab.jfronny.commons.serialize.annotations.Ignore;
import io.gitlab.jfronny.gson.*;
/**

View File

@ -3,7 +3,7 @@ package io.gitlab.jfronny.commons.test;
import io.gitlab.jfronny.commons.ComparableVersion;
import io.gitlab.jfronny.commons.data.String2ObjectMap;
import io.gitlab.jfronny.commons.serialize.Serializer;
import io.gitlab.jfronny.commons.serialize.gson.api.v2.Ignore;
import io.gitlab.jfronny.commons.serialize.annotations.Ignore;
import io.gitlab.jfronny.commons.serialize.gson.api.v2.GsonHolders;
import io.gitlab.jfronny.gson.reflect.TypeToken;
import org.junit.jupiter.api.BeforeAll;

View File

@ -1,4 +1,4 @@
package io.gitlab.jfronny.commons.serialize.gson.api.v2;
package io.gitlab.jfronny.commons.serialize.annotations;
import java.lang.annotation.*;

View File

@ -0,0 +1,25 @@
package io.gitlab.jfronny.commons.serialize.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface SerializedName {
/**
* The desired name of the field when it is serialized or deserialized.
*
* @return the desired name of the field when it is serialized or deserialized
*/
String value();
/**
* The alternative names of the field when it is deserialized
*
* @return the alternative names of the field when it is deserialized
*/
String[] alternate() default {};
}

View File

@ -3,4 +3,6 @@ module io.gitlab.jfronny.commons.serialize {
requires io.gitlab.jfronny.commons;
exports io.gitlab.jfronny.commons.serialize;
exports io.gitlab.jfronny.commons.serialize.stream;
exports io.gitlab.jfronny.commons.serialize.stream.emulated;
exports io.gitlab.jfronny.commons.serialize.annotations;
}

View File

@ -11,11 +11,7 @@ public class LazilyParsedNumber extends Number {
}
private BigDecimal asBigDecimal() {
if (value.length() > 10_000) {
throw new NumberFormatException("Number string too large: " + value.substring(0, 30) + "...");
}
BigDecimal decimal = new BigDecimal(value);
BigDecimal decimal = NumberLimits.parseBigDecimal(value);
// Cast to long to avoid issues with abs when value is Integer.MIN_VALUE
if (Math.abs((long) decimal.scale()) >= 10_000) {
throw new NumberFormatException("Number has unsupported scale: " + value);

View File

@ -0,0 +1,30 @@
package io.gitlab.jfronny.commons.data;
import java.math.BigDecimal;
import java.math.BigInteger;
public class NumberLimits {
private static final int MAX_NUMBER_STRING_LENGTH = 10_000;
private static void checkNumberStringLength(String s) {
if (s.length() > MAX_NUMBER_STRING_LENGTH) {
throw new NumberFormatException("Number string too large: " + s.substring(0, 30) + "...");
}
}
public static BigDecimal parseBigDecimal(String s) throws NumberFormatException {
checkNumberStringLength(s);
BigDecimal decimal = new BigDecimal(s);
// Cast to long to avoid issues with abs when value is Integer.MIN_VALUE
if (Math.abs((long) decimal.scale()) >= 10_000) {
throw new NumberFormatException("Number has unsupported scale: " + s);
}
return decimal;
}
public static BigInteger parseBigInteger(String s) throws NumberFormatException {
checkNumberStringLength(s);
return new BigInteger(s);
}
}

View File

@ -3,8 +3,10 @@ rootProject.name = "JfCommons"
include("commons")
include("commons-serialize")
include("commons-serialize-gson")
include("commons-serialize-json")
include("commons-serialize-gson-dsl")
include("commons-serialize-json")
include("commons-serialize-databind")
include("commons-serialize-databind-sql")
include("commons-io")
include("commons-logger")
include("commons-http-client")