feat(serialize): implement data binding
This commit is contained in:
parent
1f51e9025d
commit
e9dffa3a33
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
io.gitlab.jfronny.commons.serialize.databind.sql.SqlDateTypeAdapter
|
||||
io.gitlab.jfronny.commons.serialize.databind.sql.SqlTimeTypeAdapter
|
|
@ -0,0 +1 @@
|
|||
io.gitlab.jfronny.commons.serialize.databind.sql.SqlTimestampTypeAdapterFactory
|
|
@ -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)
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package io.gitlab.jfronny.commons.serialize.databind;
|
||||
|
||||
public interface TypeAdapterFactory {
|
||||
<T> TypeAdapter<T> create(ObjectMapper mapper, TypeToken<T> type);
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() + ')';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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.*;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package io.gitlab.jfronny.commons.serialize.gson.api.v2;
|
||||
package io.gitlab.jfronny.commons.serialize.annotations;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
|
@ -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 {};
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue