feat(serialize): Initial work on porting gson-compile

This commit is contained in:
Johannes Frohnmeyer 2024-04-17 12:56:06 +02:00
parent 5558a69a64
commit 988aefe96d
Signed by: Johannes
GPG Key ID: E76429612C2929F4
54 changed files with 3780 additions and 387 deletions

View File

@ -17,6 +17,9 @@ Common code for my java projects. Uses my common [build scripts](https://git.fro
- commons-serialize-databind: Provides data binding inspired by Gson for `commons-serialize`
- commons-serialize-databind-api: Provides a thin API around `commons-serialize-databind` for use in other libraries that don't want to depend on `commons-serialize-databind` directly
- commons-serialize-databind-sql: Provides TypeAdapters for SQL types for `commons-serialize-databind` (separate module because of the dependency on `java.sql`)
- commons-serialize-generator-core: Provides a common core for annotation processors that generate serializers and deserializers. Not directly dependent on commons-serialize, as it is also used in libjf-config.
- commons-serialize-generator-annotations: Provides the annotations used to configure `commons-serialize-generator`
- commons-serialize-generator: Provides an annotation processor for generating serializers and deserializers for `commons-serialize`
### muScript
(muscript is a simple scripting language, see [here](./muscript-runtime/README.md) for more information

View File

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

View File

@ -2,7 +2,7 @@ package io.gitlab.jfronny.commons.serialize.databind.impl.adapter;
import io.gitlab.jfronny.commons.serialize.databind.api.SerializerFor;
import io.gitlab.jfronny.commons.serialize.databind.api.TypeAdapter;
import io.gitlab.jfronny.commons.serialize.databind.impl.ISO8601Utils;
import io.gitlab.jfronny.commons.serialize.ISO8601Utils;
import io.gitlab.jfronny.commons.serialize.databind.impl.PreJava9DateFormatProvider;
import io.gitlab.jfronny.commons.serialize.MalformedDataException;
import io.gitlab.jfronny.commons.serialize.SerializeReader;

View File

@ -0,0 +1,22 @@
import io.gitlab.jfronny.scripts.*
plugins {
commons.library
}
dependencies {
}
publishing {
publications {
create<MavenPublication>("maven") {
groupId = "io.gitlab.jfronny"
artifactId = "commons-serialize-generator-annotations"
from(components["java"])
}
}
}
tasks.javadoc {
}

View File

@ -0,0 +1,12 @@
package io.gitlab.jfronny.commons.serialize.generator.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface GComment {
String value();
}

View File

@ -0,0 +1,11 @@
package io.gitlab.jfronny.commons.serialize.generator.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface GPrefer {
}

View File

@ -0,0 +1,35 @@
package io.gitlab.jfronny.commons.serialize.generator.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface GSerializable {
/**
* @return The class implementing this types serialization/deserialization. Must have static read/write methods
*/
Class<?> with() default void.class;
/**
* @return The builder class to use for creating this type. Incompatible with custom serialization
*/
Class<?> builder() default void.class;
/**
* @return The class to use for configuring JsonReader/JsonWriter instances from convenience methods in the generated class. Must have static configure methods for both.
*/
Class<?> configure() default void.class;
/**
* @return Whether to generate an adapter class to use with normal gson adapter resolution
*/
boolean generateAdapter() default false;
/**
* @return Whether to serialize static fields/methods. Incompatible with generateAdapter and builder
*/
boolean isStatic() default false;
}

View File

@ -0,0 +1,12 @@
package io.gitlab.jfronny.commons.serialize.generator.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface GWith {
Class<?> serializer();
}

View File

@ -0,0 +1,3 @@
module io.gitlab.jfronny.commons.serialize.generator.annotations {
exports io.gitlab.jfronny.commons.serialize.generator.annotations;
}

View File

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

View File

@ -0,0 +1,56 @@
package io.gitlab.jfronny.commons.serialize.generator.core;
import io.gitlab.jfronny.commons.serialize.generator.core.value.ValueCreator;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import java.util.*;
import java.util.stream.Collectors;
public abstract class AbstractProcessor2 extends AbstractProcessor {
protected Map<String, String> options;
protected Messager message;
protected Filer filer;
protected ValueCreator valueCreator;
protected Elements elements;
protected Types types;
protected SourceVersion sourceVersion;
protected Locale locale;
protected boolean isPreviewEnabled;
protected boolean hasManifold = false;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
try {
Class.forName("manifold.ext.Model");
System.out.println("Detected manifold!");
hasManifold = true;
} catch (ClassNotFoundException e) {
hasManifold = false;
}
options = processingEnv.getOptions();
message = processingEnv.getMessager();
filer = processingEnv.getFiler();
elements = processingEnv.getElementUtils();
types = processingEnv.getTypeUtils();
sourceVersion = processingEnv.getSourceVersion();
locale = processingEnv.getLocale();
isPreviewEnabled = processingEnv.isPreviewEnabled();
valueCreator = new ValueCreator(processingEnv);
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return Optional.ofNullable(this.getClass().getAnnotation(SupportedAnnotationTypes.class))
.map(SupportedAnnotationTypes::value)
.map(Set::of)
.or(() -> Optional.ofNullable(this.getClass().getAnnotation(SupportedAnnotationTypes2.class))
.map(SupportedAnnotationTypes2::value)
.map(Arrays::stream)
.map(s -> s.map(Class::getCanonicalName).collect(Collectors.toSet()))
).orElse(Set.of());
}
}

View File

@ -0,0 +1,19 @@
package io.gitlab.jfronny.commons.serialize.generator.core;
import java.util.Comparator;
import java.util.List;
public enum StringListComparator implements Comparator<List<String>> {
INSTANCE;
@Override
public int compare(List<String> left, List<String> right) {
int dif = left.size() - right.size();
if (dif != 0) return dif;
for (int i = 0; i < left.size(); i++) {
dif = left.get(i).compareTo(right.get(i));
if (dif != 0) return dif;
}
return 0;
}
}

View File

@ -0,0 +1,10 @@
package io.gitlab.jfronny.commons.serialize.generator.core;
import java.lang.annotation.*;
@Documented
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SupportedAnnotationTypes2 {
Class<?>[] value();
}

View File

@ -0,0 +1,83 @@
package io.gitlab.jfronny.commons.serialize.generator.core;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.SimpleTypeVisitor14;
import javax.lang.model.util.TypeKindVisitor14;
import javax.lang.model.util.Types;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class TypeHelper {
public static boolean isComplexType(TypeMirror type, Types typeUtils) {
Element element = typeUtils.asElement(type);
if (!(element instanceof TypeElement typeElement)) return false;
return !typeElement.getTypeParameters().isEmpty();
}
public static boolean isGenericType(TypeMirror type) {
return type.getKind() == TypeKind.TYPEVAR;
}
public static List<? extends TypeMirror> getGenericTypes(TypeMirror type) {
DeclaredType declaredType = asDeclaredType(type);
if (declaredType == null) {
return Collections.emptyList();
}
ArrayList<TypeMirror> result = new ArrayList<>();
for (TypeMirror argType : declaredType.getTypeArguments()) {
if (argType.getKind() == TypeKind.TYPEVAR) {
result.add(argType);
}
}
return result;
}
public static DeclaredType asDeclaredType(TypeMirror type) {
return type.accept(new SimpleTypeVisitor14<>() {
@Override
public DeclaredType visitDeclared(DeclaredType t, Object o) {
return t;
}
}, null);
}
public static ArrayType asArrayType(TypeMirror type) {
return type.accept(new TypeKindVisitor14<>() {
@Override
public ArrayType visitArray(ArrayType t, Object o) {
return t;
}
}, null);
}
public static boolean isInstance(DeclaredType type, String parentClassName, Types typeUtils) {
if (type == null) return false;
TypeElement element = (TypeElement) type.asElement();
for (TypeMirror interfaceType : element.getInterfaces()) {
if (typeUtils.erasure(interfaceType).toString().equals(parentClassName)) return true;
}
TypeMirror superclassType = element.getSuperclass();
if (superclassType != null) {
if (typeUtils.erasure(superclassType).toString().equals(parentClassName)) {
return true;
} else {
return isInstance(asDeclaredType(superclassType), parentClassName, typeUtils);
}
}
return false;
}
public static String getDefaultValue(TypeMirror type) {
return switch (type.getKind()) {
case BYTE, SHORT, INT, LONG, FLOAT, CHAR, DOUBLE -> "0";
case BOOLEAN -> "false";
default -> "null";
};
}
}

View File

@ -0,0 +1,303 @@
package io.gitlab.jfronny.commons.serialize.generator.core.value;
import org.jetbrains.annotations.ApiStatus;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Types;
import java.util.Locale;
import java.util.Objects;
/**
* How a [Value] can be constructed. Either a constructor, factory method, builder or none (static fields only)
*/
public sealed interface ConstructionSource {
/**
* The target [Value] class to construct.
*/
TypeElement getTargetClass();
/**
* The executable element to construct the [Value]. This may be a constructor, factory method, or builder.
*/
ExecutableElement getConstructionElement();
/**
* If this source is a constructor (either of the value or the builder).
*/
boolean isConstructor();
/**
* If this source is a builder.
*/
boolean isBuilder();
/**
* If this source is static
*/
boolean isStatic();
final class Constructor implements ConstructionSource {
private final ExecutableElement constructor;
private TypeElement targetClass;
public Constructor(ExecutableElement constructor) {
this.constructor = Objects.requireNonNull(constructor);
}
@Override
public TypeElement getTargetClass() {
return targetClass != null ? targetClass : (targetClass = (TypeElement) constructor.getEnclosingElement());
}
@Override
public ExecutableElement getConstructionElement() {
return constructor;
}
@Override
public boolean isConstructor() {
return true;
}
@Override
public boolean isBuilder() {
return false;
}
@Override
public boolean isStatic() {
return false;
}
}
final class Factory implements ConstructionSource {
private final Types types;
private final ExecutableElement method;
private TypeElement targetClass;
public Factory(Types types, ExecutableElement method) {
this.types = types;
this.method = method;
}
@Override
public TypeElement getTargetClass() {
return targetClass != null ? targetClass : (targetClass = (TypeElement) types.asElement(method.getReturnType()));
}
@Override
public ExecutableElement getConstructionElement() {
return method;
}
@Override
public boolean isConstructor() {
return false;
}
@Override
public boolean isBuilder() {
return false;
}
@Override
public boolean isStatic() {
return false;
}
}
sealed abstract class Builder implements ConstructionSource {
public abstract TypeElement getBuilderClass();
public abstract ExecutableElement getBuildMethod();
@Override
public boolean isBuilder() {
return true;
}
@Override
public boolean isStatic() {
return false;
}
}
final class BuilderConstructor extends Builder {
private final Types types;
private final ExecutableElement constructor;
private TypeElement targetClass;
private TypeElement builderClass;
private ExecutableElement buildMethod;
public BuilderConstructor(Types types, ExecutableElement constructor) {
this.types = types;
this.constructor = constructor;
}
@Override
public TypeElement getTargetClass() {
return targetClass != null ? targetClass : (targetClass = (TypeElement) types.asElement(getBuildMethod().getReturnType()));
}
@Override
public ExecutableElement getConstructionElement() {
return constructor;
}
@Override
public boolean isConstructor() {
return true;
}
@Override
public TypeElement getBuilderClass() {
return builderClass != null ? builderClass : (builderClass = (TypeElement) constructor.getEnclosingElement());
}
@Override
public ExecutableElement getBuildMethod() {
return buildMethod != null ? buildMethod : (buildMethod = findBuildMethod((TypeElement) constructor.getEnclosingElement()));
}
}
final class BuilderFactory extends Builder {
private final Types types;
private final ExecutableElement method;
private TypeElement targetClass;
private TypeElement builderClass;
private ExecutableElement buildMethod;
public BuilderFactory(Types types, ExecutableElement method) {
this.types = types;
this.method = method;
}
@Override
public TypeElement getTargetClass() {
return targetClass != null ? targetClass : (targetClass = (TypeElement) types.asElement(getBuildMethod().getReturnType()));
}
@Override
public ExecutableElement getConstructionElement() {
return method;
}
@Override
public boolean isConstructor() {
return false;
}
@Override
public TypeElement getBuilderClass() {
return builderClass != null ? builderClass : (builderClass = (TypeElement) types.asElement(method.getReturnType()));
}
@Override
public ExecutableElement getBuildMethod() {
return buildMethod != null ? buildMethod : (buildMethod = findBuildMethod((TypeElement) types.asElement(method.getReturnType())));
}
}
final class Static implements ConstructionSource {
private final TypeElement targetClass;
public Static(TypeElement targetClass) {
this.targetClass = targetClass;
}
@Override
public TypeElement getTargetClass() {
return targetClass;
}
@Override
public ExecutableElement getConstructionElement() {
return null;
}
@Override
public boolean isConstructor() {
return false;
}
@Override
public boolean isBuilder() {
return false;
}
@Override
public boolean isStatic() {
return true;
}
}
@ApiStatus.Internal
static ExecutableElement findBuildMethod(TypeElement builderClass) {
// Ok, maybe there is just one possible builder method.
{
ExecutableElement candidate = null;
boolean foundMultipleCandidates = false;
boolean isCandidateReasonableBuilderMethodName = false;
for (ExecutableElement method : ElementFilter.methodsIn(builderClass.getEnclosedElements())) {
if (isPossibleBuilderMethod(method, builderClass)) {
if (candidate == null) {
candidate = method;
} else {
// Multiple possible methods, keep the one with a reasonable builder name if possible.
foundMultipleCandidates = true;
isCandidateReasonableBuilderMethodName = isCandidateReasonableBuilderMethodName || isReasonableBuilderMethodName(candidate);
if (isCandidateReasonableBuilderMethodName) {
if (isReasonableBuilderMethodName(method)) {
// both reasonable, too ambiguous.
candidate = null;
break;
}
} else {
candidate = method;
}
}
}
}
if (candidate != null && (!foundMultipleCandidates || isCandidateReasonableBuilderMethodName)) {
return candidate;
}
}
// Last try, check to see if the immediate parent class makes sense.
{
Element candidate = builderClass.getEnclosingElement();
if (candidate.getKind() == ElementKind.CLASS) {
for (ExecutableElement method : ElementFilter.methodsIn(builderClass.getEnclosedElements())) {
if (method.getReturnType().equals(candidate.asType()) && method.getParameters().isEmpty()) {
return method;
}
}
}
}
// Well, I give up.
return null;
}
/**
* A possible builder method has no parameters and a return type of the class we want to
* construct. Therefore, the return type is not going to be void, primitive, or a platform
* class.
*/
@ApiStatus.Internal
static boolean isPossibleBuilderMethod(ExecutableElement method, TypeElement builderClass) {
if (!method.getParameters().isEmpty()) return false;
TypeMirror returnType = method.getReturnType();
if (returnType.getKind() == TypeKind.VOID) return false;
if (returnType.getKind().isPrimitive()) return false;
if (returnType.equals(builderClass.asType())) return false;
String returnTypeName = returnType.toString();
return !(returnTypeName.startsWith("java.") || returnTypeName.startsWith("javax.") || returnTypeName.startsWith("android."));
}
@ApiStatus.Internal
static boolean isReasonableBuilderMethodName(ExecutableElement method) {
String methodName = method.getSimpleName().toString().toLowerCase(Locale.ROOT);
return methodName.startsWith("build") || methodName.startsWith("create");
}
}

View File

@ -0,0 +1,29 @@
package io.gitlab.jfronny.commons.serialize.generator.core.value;
import javax.annotation.processing.Messager;
import javax.lang.model.element.Element;
import javax.tools.Diagnostic;
import java.util.List;
import java.util.stream.Collectors;
public class ElementException extends Exception {
private final List<Message> messages;
public ElementException(String message, Element element) {
this(List.of(new Message(message, element)));
}
public ElementException(List<Message> messages) {
super(messages.stream().map(Message::message).collect(Collectors.joining("\n")));
this.messages = messages;
}
public void printMessage(Messager messager) {
for (Message message : messages) {
if (message.element != null) messager.printMessage(Diagnostic.Kind.ERROR, message.message, message.element);
else messager.printMessage(Diagnostic.Kind.ERROR, message.message);
}
}
public record Message(String message, Element element) {}
}

View File

@ -0,0 +1,281 @@
package io.gitlab.jfronny.commons.serialize.generator.core.value;
import io.gitlab.jfronny.commons.data.delegate.DelegateList;
import javax.lang.model.element.*;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Types;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
public class Properties extends DelegateList.Simple<Property<?>> {
public final List<Property<?>> names;
public final List<Property.Param> params;
public final List<Property.Field> fields;
public final List<Property.Getter> getters;
public final List<Property.Setter> setters;
public final List<Property.ConstructorParam> constructorParams;
public final List<Property.Setter> builderParams;
public static Properties build(Types types, ConstructionSource constructionSource, boolean isStatic) throws ElementException {
Builder builder = new Builder(types, isStatic);
// constructor params
if (constructionSource.getConstructionElement() != null) {
for (VariableElement param : constructionSource.getConstructionElement().getParameters()) {
builder.addConstructorParam(param);
}
}
if (constructionSource instanceof ConstructionSource.Builder csb) {
var builderClass = csb.getBuilderClass();
for (ExecutableElement method : ElementFilter.methodsIn(builderClass.getEnclosedElements())) {
builder.addBuilderParam(method);
}
}
var targetClass = constructionSource.getTargetClass();
builder.addFieldsAndAccessors(targetClass);
return builder.build();
}
private Properties(List<Property<?>> names,
List<Property.Param> params,
List<Property.Field> fields,
List<Property.Getter> getters,
List<Property.Setter> setters,
List<Property.ConstructorParam> constructorParams,
List<Property.Setter> builderParams) {
super(names);
this.names = Objects.requireNonNull(names);
this.params = Objects.requireNonNull(params);
this.fields = Objects.requireNonNull(fields);
this.getters = Objects.requireNonNull(getters);
this.setters = Objects.requireNonNull(setters);
this.constructorParams = Objects.requireNonNull(constructorParams);
this.builderParams = Objects.requireNonNull(builderParams);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Properties other)) return false;
return names.equals(other.names);
}
@Override
public int hashCode() {
return names.hashCode();
}
@Override
public String toString() {
return names.toString();
}
private static class Builder {
private static final Set<String> METHODS_TO_SKIP = Set.of("hashCode", "toString", "clone");
private final boolean isStatic;
private final Types types;
public final List<Property<?>> names = new ArrayList<>();
public final List<Property.Param> params = new ArrayList<>();
public final List<Property.Field> fields = new ArrayList<>();
public final List<Property.Getter> getters = new ArrayList<>();
public final List<Property.Setter> setters = new ArrayList<>();
public final List<Property.ConstructorParam> constructorParams = new ArrayList<>();
public final List<Property.Setter> builderParams = new ArrayList<>();
public Builder(Types types, boolean isStatic) {
this.types = types;
this.isStatic = isStatic;
}
public void addFieldsAndAccessors(TypeElement targetClass) {
// accessors
for (ExecutableElement method : ElementFilter.methodsIn(targetClass.getEnclosedElements())) {
addGetter(targetClass, method);
addSetter(targetClass, method);
}
// fields
for (VariableElement field : ElementFilter.fieldsIn(targetClass.getEnclosedElements())) {
addField(field);
}
for (TypeMirror superInterface : targetClass.getInterfaces()) {
addFieldsAndAccessors((TypeElement) types.asElement(superInterface));
}
TypeMirror superclass = targetClass.getSuperclass();
if (superclass.getKind() != TypeKind.NONE && !superclass.toString().equals("java.lang.Object")) {
addFieldsAndAccessors((TypeElement) types.asElement(superclass));
}
}
public void addGetter(TypeElement classElement, ExecutableElement method) {
Set<Modifier> modifiers = method.getModifiers();
if (modifiers.contains(Modifier.PRIVATE)
|| (isStatic != modifiers.contains(Modifier.STATIC))
|| method.getReturnType().getKind() == TypeKind.VOID
|| !method.getParameters().isEmpty()
|| isMethodToSkip(classElement, method)) {
return;
}
getters.add(new Property.Getter(method));
}
public void addSetter(TypeElement classElement, ExecutableElement method) {
Set<Modifier> modifiers = method.getModifiers();
if (modifiers.contains(Modifier.PRIVATE)
|| (isStatic != modifiers.contains(Modifier.STATIC))
|| method.getReturnType().getKind() != TypeKind.VOID
|| method.getParameters().size() != 1
|| isMethodToSkip(classElement, method)
|| !method.getSimpleName().toString().startsWith("set")) {
return;
}
setters.add(new Property.Setter(method));
}
public void addField(VariableElement field) {
Set<Modifier> modifiers = field.getModifiers();
if (isStatic != modifiers.contains(Modifier.STATIC)) return;
fields.add(new Property.Field(field));
}
public void addConstructorParam(VariableElement param) {
Property.ConstructorParam prop = new Property.ConstructorParam(param);
constructorParams.add(prop);
params.add(prop);
}
public void addBuilderParam(ExecutableElement method) {
if (method.getParameters().size() == 1 && method.getSimpleName().toString().startsWith("set")) {
Property.Setter prop = new Property.Setter(method);
builderParams.add(prop);
params.add(prop);
}
}
public Properties build() throws ElementException {
stripBeans(getters);
removeExtraBuilders();
removeExtraSetters();
removeGettersForTransientFields();
removeSettersForTransientFields();
mergeSerializeNames(params, fields, getters, setters);
removeExtraFields();
names.addAll(params);
getters.stream().filter(f -> !containsName(names, f)).forEach(names::add);
setters.stream().filter(f -> !containsName(names, f)).forEach(names::add);
fields.stream().filter(f -> !containsName(names, f)).forEach(names::add);
return new Properties(names, params, fields, getters, setters, constructorParams, builderParams);
}
private void stripBeans(List<Property.Getter> getters) {
if (getters.stream().allMatch(Property.Getter::isBean)) {
for (Property.Getter getter : getters) {
getter.stripBean();
}
}
}
private void removeExtraBuilders() {
for (int i = builderParams.size() - 1; i >= 0; i--) {
Property.Setter builderParam = builderParams.get(i);
if (containsName(constructorParams, builderParam)) {
builderParams.remove(i);
params.remove(builderParam);
}
}
}
private void removeExtraSetters() {
setters.removeIf(s -> containsName(constructorParams, s));
}
private void removeExtraFields() {
fields.removeIf(field -> {
Set<Modifier> modifiers = field.element.getModifiers();
return modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.TRANSIENT);
});
}
private void removeGettersForTransientFields() {
getters.removeIf(getter -> {
Property<?> field = findName(fields, getter);
return field != null && field.element.getModifiers().contains(Modifier.TRANSIENT);
});
}
private void removeSettersForTransientFields() {
getters.removeIf(getter -> {
Property<?> field = findName(fields, getter);
return field != null && field.element.getModifiers().contains(Modifier.TRANSIENT);
});
}
private boolean isMethodToSkip(TypeElement classElement, ExecutableElement method) {
String name = method.getSimpleName().toString();
if (METHODS_TO_SKIP.contains(name)) {
return true;
}
return isKotlinClass(classElement) && name.matches("component[0-9]+");
}
}
private static void merge(Property<?>[] properties) throws ElementException {
if (properties.length == 0) return;
List<AnnotationMirror> annotations = null;
for (Property<?> name : properties) {
if (name == null) continue;
if (!name.getAnnotations().isEmpty()) {
if (annotations == null) annotations = new ArrayList<>(name.getAnnotations());
else {
for (AnnotationMirror annotation : name.getAnnotations()) {
if (annotations.contains(annotation)) {
throw new ElementException("Duplicate annotation " + annotation + " found on " + name, name.element);
} else annotations.add(annotation);
}
}
}
}
if (annotations != null) {
for (Property<?> name : properties) {
if (name == null) continue;
name.setAnnotations(annotations);
}
}
}
@SafeVarargs
private static void mergeSerializeNames(List<? extends Property<?>>... propertyLists) throws ElementException {
if (propertyLists.length == 0) return;
for (Property<?> name : propertyLists[0]) {
var names = new Property<?>[propertyLists.length];
names[0] = name;
for (int i = 1; i < propertyLists.length; i++) {
names[i] = findName(propertyLists[i], name);
}
merge(names);
}
}
public static <N extends Property<?>> N findName(List<N> names, Property<?> property) {
return names.stream().filter(n -> n.getName().equals(property.getName())).findFirst().orElse(null);
}
public static boolean containsName(List<? extends Property<?>> properties, Property<?> property) {
return findName(properties, property) != null;
}
private static boolean isKotlinClass(TypeElement element) {
return element.getAnnotationMirrors().stream()
.anyMatch(m -> m.getAnnotationType().toString().equals("kotlin.Metadata"));
}
}

View File

@ -0,0 +1,170 @@
package io.gitlab.jfronny.commons.serialize.generator.core.value;
import org.jetbrains.annotations.ApiStatus;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import java.util.List;
public abstract sealed class Property<T extends Element> {
protected final T element;
private List<? extends AnnotationMirror> annotations;
public Property(T element) {
this.element = element;
this.annotations = element.getAnnotationMirrors();
}
public T getElement() {
return element;
}
/**
* The name of the property. For fields and params this is the name in code. For getters, it may have the 'get' or
* 'is' prefix stripped.
* @see #getCallableName()
*/
public String getName() {
return element.getSimpleName().toString();
}
/**
* The actual name of the property. This will not have any 'get' or 'is' prefix stripped.
* @see #getName()
*/
public String getCallableName() {
return getName();
}
/**
* The property's type.
*/
public TypeMirror getType() {
return element.asType();
}
/**
* Annotations relevant to the property. These may be copied from another source. For example, if this is a getter
* it may contain the annotations on the backing private field.
*/
public List<? extends AnnotationMirror> getAnnotations() {
return annotations;
}
public void setAnnotations(List<? extends AnnotationMirror> annotations) {
this.annotations = annotations;
}
@Override
public String toString() {
return getName() + ": " + getType();
}
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
if (this == obj) return true;
if (!(obj instanceof Property<?> other)) return false;
return element.equals(other.element);
}
@Override
public int hashCode() {
return element.hashCode();
}
public static final class Field extends Property<VariableElement> {
public Field(VariableElement element) {
super(element);
}
}
public static final class Getter extends Property<ExecutableElement> {
private static final String BEAN_PREFIX = "get";
private static final String BEAN_PREFIX_BOOL = "is";
private boolean stripBean = false;
public Getter(ExecutableElement element) {
super(element);
}
public boolean isBean() {
return getBeanPrefix() != null;
}
@Override
public String getName() {
String name = super.getName();
if (stripBean) {
String prefix = getBeanPrefix();
if (prefix != null) {
return Character.toLowerCase(name.charAt(prefix.length())) + name.substring(prefix.length() + 1);
}
}
return name;
}
@Override
public String getCallableName() {
return super.getName();
}
@Override
public TypeMirror getType() {
return element.getReturnType();
}
@ApiStatus.Internal
public void stripBean() {
stripBean = true;
}
private String getBeanPrefix() {
String name = super.getName();
if (element.getReturnType().getKind() == TypeKind.BOOLEAN) {
if (name.length() > BEAN_PREFIX_BOOL.length() && name.startsWith(BEAN_PREFIX_BOOL)) {
return BEAN_PREFIX_BOOL;
}
}
return name.length() > BEAN_PREFIX.length() && name.startsWith(BEAN_PREFIX) ? BEAN_PREFIX : null;
}
}
public static sealed abstract class Param extends Property<VariableElement> {
public Param(VariableElement element) {
super(element);
}
}
public static final class ConstructorParam extends Param {
public ConstructorParam(VariableElement element) {
super(element);
}
}
public static final class Setter extends Param {
private final ExecutableElement method;
private final String name;
public Setter(ExecutableElement method) {
super(method.getParameters().getFirst());
this.method = method;
name = Character.toLowerCase(method.getSimpleName().toString().charAt(3)) + method.getSimpleName().toString().substring(4);
}
@Override
public String getName() {
return name;
}
@Override
public String getCallableName() {
return method.getSimpleName().toString();
}
}
}

View File

@ -0,0 +1,52 @@
package io.gitlab.jfronny.commons.serialize.generator.core.value;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.TypeElement;
import java.util.Objects;
import java.util.stream.Collectors;
public class Value {
private final ProcessingEnvironment env;
private final ConstructionSource constructionSource;
private final TypeElement element;
private Properties properties;
public Value(ProcessingEnvironment env, ConstructionSource constructionSource) {
this.env = env;
this.constructionSource = constructionSource;
this.element = constructionSource.getTargetClass();
}
public Properties getProperties() throws ElementException {
return properties != null ? properties : (properties = Properties.build(env.getTypeUtils(), constructionSource, constructionSource.isStatic()));
}
public ConstructionSource getConstructionSource() {
return constructionSource;
}
public TypeElement getElement() {
return element;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Value value)) return false;
return element.equals(value.element);
}
@Override
public int hashCode() {
return element.hashCode();
}
@Override
public String toString() {
try {
return element.toString() + '{' + getProperties().stream().map(Objects::toString).collect(Collectors.joining(", "));
} catch (ElementException e) {
return element.toString();
}
}
}

View File

@ -0,0 +1,167 @@
package io.gitlab.jfronny.commons.serialize.generator.core.value;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.*;
import javax.lang.model.util.ElementFilter;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class ValueCreator {
public static Class<? extends Annotation> preferAnnotation = null;
private final ProcessingEnvironment env;
public ValueCreator(ProcessingEnvironment env) {
this.env = env;
}
/**
* Creates a [Value] from the given element. This element can be the [TypeElement] of the target class, or a
* specific constructor or factory method. If [isBuilder] is true, then the element represents the builder class,
* constructor or factory method.
*/
public Value from(Element element, boolean isBuilder) throws ElementException {
if (element instanceof TypeElement tel) {
return isBuilder ? fromBuilderClass(tel) : fromClass(tel);
} else if (element instanceof ExecutableElement xel) {
if (xel.getKind() == ElementKind.CONSTRUCTOR) {
return isBuilder ? fromBuilderConstructor(xel) : fromConstructor(xel);
} else {
return isBuilder ? fromBuilderFactory(xel) : fromFactory(xel);
}
} else throw new IllegalArgumentException("Expected TypeElement or ExecutableElement but got: " + element);
}
public Value fromStatic(TypeElement element) throws ElementException {
return create(new ConstructionSource.Static(element));
}
/**
* Creates a [Value] from the given constructor element. ex:
* ```
* public class Value {
* > public Value(int arg1) { ... }
* }
* ```
*/
public Value fromConstructor(ExecutableElement constructor) {
checkKind(constructor, ElementKind.CONSTRUCTOR);
return create(new ConstructionSource.Constructor(constructor));
}
/**
* Creates a [Value] from the given builder's constructor element. ex:
* ```
* public class Builder {
* > public Builder() { ... }
* public Value build() { ... }
* }
* ```
*/
public Value fromBuilderConstructor(ExecutableElement constructor) {
checkKind(constructor, ElementKind.CONSTRUCTOR);
return create(new ConstructionSource.BuilderConstructor(env.getTypeUtils(), constructor));
}
/**
* Creates a [Value] from the given factory method element. ex:
* ```
* public class Value {
* > public static Value create(int arg) { ... }
* }
* ```
*/
public Value fromFactory(ExecutableElement factory) {
checkKind(factory, ElementKind.METHOD);
return create(new ConstructionSource.Factory(env.getTypeUtils(), factory));
}
/**
* Creates a [Value] from the given builder factory method element. ex:
* ```
* public class Value {
* > public static Builder builder() { ... }
* public static class Builder { ... }
* }
* ```
*/
public Value fromBuilderFactory(ExecutableElement builderFactory) {
checkKind(builderFactory, ElementKind.METHOD);
return create(new ConstructionSource.BuilderFactory(env.getTypeUtils(), builderFactory));
}
/**
* Creates a [Value] from the given class. ex:
* ```
* > public class Value { ... }
* ```
*/
public Value fromClass(TypeElement targetClass) throws ElementException {
ExecutableElement creator = findConstructorOrFactory(targetClass);
return creator.getKind() == ElementKind.CONSTRUCTOR ? fromConstructor(creator) : fromFactory(creator);
}
/**
* Creates a [Value] from the given builder class. ex:
* ```
* > public class Builder {
* public Value build() { ... }
* }
* ```
*/
public Value fromBuilderClass(TypeElement builderClass) throws ElementException {
ExecutableElement creator = findConstructorOrFactory(builderClass);
return creator.getKind() == ElementKind.CONSTRUCTOR ? fromBuilderConstructor(creator) : fromBuilderFactory(creator);
}
private Value create(ConstructionSource constructionSource) {
return new Value(env, constructionSource);
}
private static ExecutableElement findConstructorOrFactory(TypeElement klazz) throws ElementException {
ExecutableElement noArgConstructor = null;
List<ExecutableElement> constructors = ElementFilter.constructorsIn(klazz.getEnclosedElements());
if (constructors.size() == 1) {
ExecutableElement constructor = constructors.get(0);
if (constructor.getParameters().isEmpty()) {
noArgConstructor = constructor;
constructors.remove(0);
}
}
for (ExecutableElement method : ElementFilter.methodsIn(klazz.getEnclosedElements())) {
Set<Modifier> modifiers = method.getModifiers();
if (modifiers.contains(Modifier.STATIC)
&& !modifiers.contains(Modifier.PRIVATE)
&& method.getReturnType().equals(klazz.asType())) {
constructors.add(method);
}
}
if (constructors.isEmpty()) {
if (noArgConstructor != null) return noArgConstructor;
else throw new ElementException("Lacking constructor or factory method", klazz);
}
if (constructors.size() == 1) return constructors.get(0);
if (noArgConstructor != null) constructors.add(noArgConstructor);
if (preferAnnotation != null) {
List<ExecutableElement> preferred = new ArrayList<>();
for (ExecutableElement constructor : constructors) {
if (constructor.getAnnotationsByType(preferAnnotation).length != 0) {
preferred.add(constructor);
}
}
if (preferred.size() == 1) return preferred.get(0);
}
List<ElementException.Message> messages = new ArrayList<>();
messages.add(new ElementException.Message("More than one constructor or factory method found.", klazz));
constructors.stream().map(s -> new ElementException.Message(" " + s, s)).forEach(messages::add);
throw new ElementException(messages);
}
private static void checkKind(Element element, ElementKind kind) {
if (element.getKind() != kind) {
throw new IllegalArgumentException("Expected " + kind + " but got: " + element);
}
}
}

View File

@ -0,0 +1,7 @@
module io.gitlab.jfronny.commons.serialize.generator.core {
requires java.compiler;
requires static org.jetbrains.annotations;
requires io.gitlab.jfronny.commons;
exports io.gitlab.jfronny.commons.serialize.generator.core;
exports io.gitlab.jfronny.commons.serialize.generator.core.value;
}

View File

@ -0,0 +1,15 @@
plugins {
java
application
}
repositories {
mavenCentral()
}
dependencies {
implementation(projects.commonsSerialize)
compileOnly(projects.commonsSerializeGeneratorAnnotations)
annotationProcessor(projects.commonsSerializeGenerator)
}

View File

@ -0,0 +1,14 @@
package io.gitlab.jfronny.commons.serialize.generator.example;
import io.gitlab.jfronny.commons.serialize.SerializeReader;
import io.gitlab.jfronny.commons.serialize.SerializeWriter;
public class ExampleAdapter {
public static <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void write(boolean bool, Writer writer) throws TEx {
writer.value(!bool);
}
public static <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> boolean read(Reader reader) throws TEx {
return !reader.nextBoolean();
}
}

View File

@ -0,0 +1,141 @@
package io.gitlab.jfronny.commons.serialize.generator.example;
import io.gitlab.jfronny.commons.serialize.SerializeReader;
import io.gitlab.jfronny.commons.serialize.SerializeWriter;
import io.gitlab.jfronny.commons.serialize.annotations.SerializedName;
import io.gitlab.jfronny.commons.serialize.generator.annotations.GComment;
import io.gitlab.jfronny.commons.serialize.generator.annotations.GPrefer;
import io.gitlab.jfronny.commons.serialize.generator.annotations.GSerializable;
import io.gitlab.jfronny.commons.serialize.generator.annotations.GWith;
import java.io.Writer;
import java.util.*;
public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
}
@GSerializable(generateAdapter = true)
public static class ExamplePojo {
@SerializedName("someBalue")
public String someValue;
@GComment("Yes!")
public Boolean someBool;
@GComment("Halal")
@SerializedName("bingChiller")
public String getBass() {
return "Yes";
}
public ExamplePojo2 nested;
public Set<ExamplePojo> recursive1;
public LinkedList<ExampleRecord> recursive2;
public Queue<String> queue;
public Date date;
public EeE eEe;
public UnsupportedClass unsupported;
public String getUnsupported() {
return unsupported == null ? null : unsupported.text;
}
public void setUnsupported(String text) {
unsupported = new UnsupportedClass(text);
}
public void setJoe(String joe) {
}
public String getJoe() {
return "A";
}
}
@GSerializable(configure = Configuration.class)
public static class ExamplePojo2 {
@GComment("Yes!")
@GWith(serializer = ExampleAdapter.class)
public boolean primitive;
public ExamplePojo2[] recursiveTest;
public Map<EeE, Example4> map1;
public Map<String, String> map2;
public Map<Integer, String> map3;
public Map<UUID, String> map4;
public Inner inner;
@GSerializable
public record Inner(String s, Inner2 inner2) {
@GSerializable
public record Inner2(String s) {}
}
}
@GSerializable
public record ExampleRecord(String hello, @GComment("Sheesh") ExamplePojo2 pojo) {
@GPrefer
public ExampleRecord(String yes) {
this(yes, null);
}
}
@GSerializable(builder = Example4.Builder.class)
public static class Example4 {
public String someField;
public boolean shesh;
public static class Builder {
private String someField;
private boolean shesh;
public Builder(String someField) {
this.someField = someField;
}
public Builder setShesh(boolean shesh) {
this.shesh = shesh;
return this;
}
public Example4 build() {
Example4 e = new Example4();
e.someField = someField;
return e;
}
}
}
public enum EeE {
Yes, Yay, Aaee;
}
public static class UnsupportedClass {
private final String text;
public UnsupportedClass(String text) {
this.text = text;
}
}
public static class Configuration {
public static <TEx extends Throwable, Writer extends SerializeWriter<TEx, Writer>> void configure(Writer writer) {
}
public static <TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>> void configure(Reader reader) {
}
}
}

View File

@ -0,0 +1,9 @@
package io.gitlab.jfronny.commons.serialize.generator.example;
import io.gitlab.jfronny.commons.serialize.generator.annotations.GSerializable;
@GSerializable(isStatic = true, configure = Main.Configuration.class)
public class Static {
public boolean nonStatic;
public static boolean joe;
}

View File

@ -0,0 +1,6 @@
module io.gitlab.jfronny.commons.serialize.generator.example {
requires static io.gitlab.jfronny.commons.serialize.generator.annotations;
requires io.gitlab.jfronny.commons.serialize;
requires io.gitlab.jfronny.commons.serialize.databind.api;
exports io.gitlab.jfronny.commons.serialize.generator.example;
}

View File

@ -0,0 +1,29 @@
import io.gitlab.jfronny.scripts.*
plugins {
commons.library
}
dependencies {
implementation(projects.commons)
implementation(projects.commonsSerializeGeneratorCore)
implementation(projects.commonsSerializeGeneratorAnnotations)
implementation(projects.commonsSerializeDatabindApi)
implementation(libs.java.poet)
}
publishing {
publications {
create<MavenPublication>("maven") {
groupId = "io.gitlab.jfronny"
artifactId = "commons-serialize-generator-core"
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-generator-core/$version/raw", projects.commonsSerializeGeneratorCore)
}

View File

@ -0,0 +1,30 @@
package io.gitlab.jfronny.commons.serialize.generator;
import com.squareup.javapoet.ClassName;
public class Cl {
// Core
public static final ClassName GSON_ELEMENT = ClassName.get("io.gitlab.jfronny.commons.serialize.emulated", "DataElement");
public static final ClassName SERIALIZE_WRITER = ClassName.get("io.gitlab.jfronny.commons.serialize", "SerializeWriter");
public static final ClassName SERIALIZE_READER = ClassName.get("io.gitlab.jfronny.commons.serialize", "SerializeReader");
public static final ClassName EMULATED_READER = ClassName.get("io.gitlab.jfronny.commons.serialize.emulated", "EmulatedReader");
public static final ClassName EMULATED_WRITER = ClassName.get("io.gitlab.jfronny.commons.serialize.emulated", "EmulatedWriter");
public static final ClassName GSON_TOKEN = ClassName.get("io.gitlab.jfronny.commons.serialize", "Token");
public static final ClassName SERIALIZED_NAME = ClassName.get("io.gitlab.jfronny.commons.serialize.annotations", "SerializedName");
public static final ClassName MALFORMED_DATA_EXCEPTION = ClassName.get("io.gitlab.jfronny.commons.serialize", "MalformedDataException");
// Databind Interop
public static final ClassName TYPE_ADAPTER = ClassName.get("io.gitlab.jfronny.commons.serialize.databind.api", "TypeAdapter");
public static final ClassName TYPE_TOKEN = ClassName.get("io.gitlab.jfronny.commons.serialize.databind.api", "TypeToken");
public static final ClassName SERIALIZER_FOR = ClassName.get("io.gitlab.jfronny.commons.serialize.databind.api", "SerializerFor");
public static final ClassName OBJECT_MAPPER = ClassName.get("io.gitlab.jfronny.commons.serialize.databind", "ObjectMapper");
// Custom Utils
public static final ClassName GISO8601UTILS = ClassName.get("io.gitlab.jfronny.commons.serialize", "ISO8601Utils");
public static final ClassName GCOMMENT = ClassName.get("io.gitlab.jfronny.commons.serialize.generator.annotations", "GComment");
public static final ClassName GWITH = ClassName.get("io.gitlab.jfronny.commons.serialize.generator.annotations", "GWith");
// Manifold Interop
public static final ClassName MANIFOLD_EXTENSION = ClassName.get("manifold.ext.rt.api", "Extension");
public static final ClassName MANIFOLD_THIS = ClassName.get("manifold.ext.rt.api", "This");
}

View File

@ -0,0 +1,37 @@
package io.gitlab.jfronny.commons.serialize.generator;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.TypeName;
import io.gitlab.jfronny.commons.serialize.generator.core.value.ElementException;
import org.jetbrains.annotations.Nullable;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
public record SerializableClass(TypeElement classElement, ClassName generatedClassName, @Nullable TypeMirror adapter, boolean adapterIsStatic, @Nullable TypeMirror builder, @Nullable TypeMirror configure, boolean generateAdapter, boolean isStatic) {
public static SerializableClass of(TypeElement element, @Nullable TypeMirror with, boolean withIsStatic, @Nullable TypeMirror builder, @Nullable TypeMirror configure, boolean generateAdapter, boolean isStatic, boolean manifold) throws ElementException {
ClassName className = ClassName.get(element);
String pkg = manifold ? "serializeprocessor.extensions." + className.packageName() + '.' + className.simpleNames().getFirst() : className.packageName();
ClassName generatedClassName = ClassName.get(pkg, "GC_" + className.simpleNames().getFirst(), className.simpleNames().subList(1, className.simpleNames().size()).toArray(String[]::new));
return new SerializableClass(element, generatedClassName, voidToNull(with), withIsStatic, voidToNull(builder), voidToNull(configure), generateAdapter, isStatic).validate();
}
public ClassName getClassName() {
return ClassName.get(classElement);
}
public TypeName getTypeName() {
return TypeName.get(classElement.asType());
}
private SerializableClass validate() throws ElementException {
if (adapter != null && builder != null) throw new ElementException("@GSerializable with both an adapter and a builder. This is unsupported!", classElement);
if (isStatic && builder != null) throw new ElementException("@GSerializable which is static and has a builder. This is unsupported!", classElement);
if (isStatic && generateAdapter) throw new ElementException("@GSerializable which is static and generates an adapter. This is unsupported!", classElement);
return this;
}
private static TypeMirror voidToNull(TypeMirror type) {
return type == null || type.toString().equals("void") ? null : type;
}
}

View File

@ -0,0 +1,254 @@
package io.gitlab.jfronny.commons.serialize.generator;
import com.squareup.javapoet.*;
import io.gitlab.jfronny.commons.StringFormatter;
import io.gitlab.jfronny.commons.serialize.databind.api.SerializeWithAdapter;
import io.gitlab.jfronny.commons.serialize.generator.adapter.Adapter;
import io.gitlab.jfronny.commons.serialize.generator.adapter.Adapters;
import io.gitlab.jfronny.commons.serialize.generator.annotations.GPrefer;
import io.gitlab.jfronny.commons.serialize.generator.annotations.GSerializable;
import io.gitlab.jfronny.commons.serialize.generator.core.AbstractProcessor2;
import io.gitlab.jfronny.commons.serialize.generator.core.StringListComparator;
import io.gitlab.jfronny.commons.serialize.generator.core.SupportedAnnotationTypes2;
import io.gitlab.jfronny.commons.serialize.generator.core.value.ElementException;
import io.gitlab.jfronny.commons.serialize.generator.core.value.ValueCreator;
import io.gitlab.jfronny.commons.serialize.generator.gprocessor.GProcessor;
import io.gitlab.jfronny.commons.serialize.generator.gprocessor.InstanceProcessor;
import io.gitlab.jfronny.commons.serialize.generator.gprocessor.StaticProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedOptions;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.StandardLocation;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@SupportedAnnotationTypes2({GSerializable.class, SerializeWithAdapter.class})
@SupportedOptions({"serializeProcessorNoReflect", "serializeProcessorDisableSafe"})
public class SerializeGeneratorProcessor extends AbstractProcessor2 {
private Map<ClassName, TypeSpec.Builder> seen;
private InstanceProcessor instanceProcessor;
private StaticProcessor staticProcessor;
private List<ClassName> generatedAdapters;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
ValueCreator.preferAnnotation = GPrefer.class;
seen = new LinkedHashMap<>();
generatedAdapters = new ArrayList<>();
for (Adapter adapter : Adapters.ADAPTERS) {
adapter.init(processingEnv);
}
boolean disableSafe = options.containsKey("serializeProcessorDisableSafe");
instanceProcessor = new InstanceProcessor(valueCreator, message, hasManifold, disableSafe);
staticProcessor = new StaticProcessor(valueCreator, message, hasManifold, disableSafe);
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
Set<SerializableClass> toGenerate = new LinkedHashSet<>();
// Gather all serializable types
for (TypeElement annotation : annotations) {
for (Element element : roundEnvironment.getElementsAnnotatedWith(annotation)) {
for (AnnotationMirror mirror : element.getAnnotationMirrors()) {
try {
if (mirror.getAnnotationType().toString().equals(GSerializable.class.getCanonicalName())) {
var bld = new Object() {
TypeMirror with = null;
TypeMirror builder = null;
TypeMirror configure = null;
Boolean generateAdapter = null;
Boolean isStatic = null;
};
elements.getElementValuesWithDefaults(mirror).forEach((executableElement, value) -> {
String name = executableElement.getSimpleName().toString();
switch (name) {
case "with" -> {
if (bld.with != null) throw new IllegalArgumentException("Duplicate annotation parameter: with");
bld.with = (TypeMirror) value.getValue();
}
case "builder" -> {
if (bld.builder != null) throw new IllegalArgumentException("Duplicate annotation parameter: builder");
bld.builder = (TypeMirror) value.getValue();
}
case "configure" -> {
if (bld.configure != null) throw new IllegalArgumentException("Duplicate annotation parameter: configure");
bld.configure = (TypeMirror) value.getValue();
}
case "generateAdapter" -> {
if (bld.generateAdapter != null) throw new IllegalArgumentException("Duplicate annotation parameter: generateAdapter");
bld.generateAdapter = (Boolean) value.getValue();
}
case "isStatic" -> {
if (bld.isStatic != null) throw new IllegalArgumentException("Duplicate annotation parameter: isStatic");
bld.isStatic = (Boolean) value.getValue();
}
default -> throw new IllegalArgumentException("Unexpected annotation parameter: " + name);
}
});
if (bld.with == null) throw new IllegalArgumentException("Missing annotation parameter: with");
if (bld.builder == null) throw new IllegalArgumentException("Missing annotation parameter: builder");
if (bld.configure == null) throw new IllegalArgumentException("Missing annotation parameter: configure");
if (bld.generateAdapter == null) throw new IllegalArgumentException("Missing annotation parameter: generateAdapter");
if (bld.isStatic == null) throw new IllegalArgumentException("Missing annotation parameter: isStatic");
toGenerate.add(SerializableClass.of((TypeElement) element, bld.with, true, bld.builder, bld.configure, bld.generateAdapter, bld.isStatic, hasManifold));
} else if (mirror.getAnnotationType().toString().equals(SerializeWithAdapter.class.getCanonicalName())) {
var bld = new Object() {
TypeMirror adapter = null;
Boolean nullSafe = null;
};
elements.getElementValuesWithDefaults(mirror).forEach((executableElement, value) -> {
String name = executableElement.getSimpleName().toString();
switch (name) {
case "adapter" -> {
if (bld.adapter != null) throw new IllegalArgumentException("Duplicate annotation parameter: adapter");
bld.adapter = (TypeMirror) value.getValue();
}
case "nullSafe" -> {
if (bld.nullSafe != null) throw new IllegalArgumentException("Duplicate annotation parameter: nullSafe");
bld.nullSafe = (Boolean) value.getValue();
}
default -> throw new IllegalArgumentException("Unexpected annotation parameter: " + name);
}
});
if (bld.adapter == null) throw new IllegalArgumentException("Missing annotation parameter: adapter");
if (bld.nullSafe == null) throw new IllegalArgumentException("Missing annotation parameter: nullSafe");
TypeMirror $void = elements.getTypeElement("void").asType();
toGenerate.add(SerializableClass.of((TypeElement) element, bld.adapter, false, $void, $void, false, false, hasManifold));
} else {
throw new IllegalArgumentException("Unexpected annotation: " + mirror.getAnnotationType());
}
} catch (ElementException e) {
e.printMessage(message);
}
}
}
}
// Do not allow mutation past this point, especially not from individual process tasks
toGenerate = Set.copyOf(toGenerate);
// Generate adapters
for (SerializableClass toProcess : toGenerate) {
try {
process(toProcess, toGenerate);
} catch (ElementException e) {
e.printMessage(message);
}
}
for (var entry : seen.keySet().stream().collect(Collectors.groupingBy(ClassName::packageName)).entrySet()) {
Map<List<String>, TypeSpec.Builder> known = entry.getValue().stream()
.collect(Collectors.toMap(
ClassName::simpleNames,
seen::get,
(u, v) -> u,
() -> new TreeMap<>(StringListComparator.INSTANCE.reversed())
));
// Generate additional parent classes
for (List<String> klazz : known.keySet().stream().toList()) {
List<String> current = new LinkedList<>();
for (String s : klazz) {
current.add(s);
TypeSpec.Builder builder = find(known, current);
if (builder == null) {
builder = TypeSpec.classBuilder(s).addModifiers(Modifier.PUBLIC);
if (current.size() == 1 && hasManifold) builder.addAnnotation(Cl.MANIFOLD_EXTENSION);
known.put(List.copyOf(current), builder);
}
if (current.size() > 1) builder.addModifiers(Modifier.STATIC);
}
}
// Add to parent class
for (var entry1 : known.entrySet()) {
if (entry1.getKey().size() == 1) continue;
find(known, entry1.getKey().subList(0, entry1.getKey().size() - 1)).addType(entry1.getValue().build());
}
// Print
// System.out.println("Got " + known.size() + " classes");
// for (var entry1 : known.entrySet()) {
// System.out.println("Class " + entry.key + '.' + String.join(".", entry1.key));
// for (TypeSpec typeSpec : entry1.value.typeSpecs) {
// System.out.println("- " + typeSpec.name);
// }
// }
// Write top-level classes
for (var entry1 : known.entrySet()) {
if (entry1.getKey().size() == 1) {
JavaFile javaFile = JavaFile.builder(entry.getKey(), entry1.getValue().build())
.skipJavaLangImports(true)
.indent(" ")
.build();
try {
javaFile.writeTo(filer);
} catch (IOException e) {
message.printMessage(Diagnostic.Kind.ERROR, "Could not write source: " + StringFormatter.toString(e));
}
}
}
}
if (!generatedAdapters.isEmpty()) {
for (ClassName name : generatedAdapters) {
}
try {
filer.createResource(StandardLocation.SOURCE_OUTPUT, "", "META-INF/services/io.gitlab.jfronny.commons.serialize.databind.api.TypeAdapter")
.openWriter()
.append(generatedAdapters.stream().map(ClassName::canonicalName).collect(Collectors.joining("\n")))
.close();
} catch (IOException e) {
message.printMessage(Diagnostic.Kind.ERROR, "Could not write service loader file: " + StringFormatter.toString(e));
}
generatedAdapters.clear();
}
seen.clear();
return false;
}
private <K, V> V find(Map<List<K>, V> map, List<K> key) {
for (var entry : map.entrySet()) if (entry.getKey().equals(key)) return entry.getValue();
return null;
}
private void process(SerializableClass toProcess, Set<SerializableClass> other) throws ElementException {
if (seen.containsKey(toProcess.generatedClassName())) return; // Don't process the same class more than once
TypeName classType = toProcess.getTypeName();
List<TypeVariableName> typeVariables = new ArrayList<>();
if (!toProcess.isStatic() && classType instanceof ParameterizedTypeName type) {
for (TypeName argument : type.typeArguments) {
typeVariables.add(TypeVariableName.get(argument.toString()));
}
}
TypeSpec.Builder spec = TypeSpec.classBuilder(toProcess.generatedClassName().simpleName())
.addOriginatingElement(toProcess.classElement())
.addModifiers(Modifier.PUBLIC)
.addTypeVariables(typeVariables);
seen.put(toProcess.generatedClassName(), spec);
GProcessor processor = toProcess.isStatic() ? staticProcessor : instanceProcessor;
if (toProcess.adapter() != null) {
processor.generateDelegateToAdapter(spec, classType, toProcess.adapter(), toProcess.adapterIsStatic());
} else {
if (toProcess.generateAdapter()) {
generatedAdapters.add(processor.generateDelegatingAdapter(spec, classType, toProcess.generatedClassName()));
}
processor.generateSerialisation(spec, toProcess, typeVariables, other);
}
processor.generateAuxiliary(spec, classType, toProcess.configure());
}
}

View File

@ -0,0 +1,85 @@
package io.gitlab.jfronny.commons.serialize.generator.adapter;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import io.gitlab.jfronny.commons.serialize.generator.SerializableClass;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
import java.util.List;
import java.util.Map;
import java.util.Set;
public abstract class Adapter<T extends Adapter<T>.Hydrated> {
protected Messager message;
protected Types typeUtils;
protected Map<String, String> options;
public abstract T instantiate();
public void init(ProcessingEnvironment env) {
this.message = env.getMessager();
this.typeUtils = env.getTypeUtils();
this.options = env.getOptions();
}
public final T hydrate(TypeSpec.Builder klazz, CodeBlock.Builder code, List<TypeVariableName> typeVariables, Set<SerializableClass> other, TypeMirror type, String propName, List<? extends AnnotationMirror> annotations, Element sourceElement) {
T instance = instantiate();
instance.klazz = klazz;
instance.typeVariables = typeVariables;
instance.other = other;
instance.type = type;
instance.code = code;
instance.unboxedType = instance.unbox(type);
instance.name = propName;
instance.argName = "_" + propName;
instance.adapterName = "adapter_" + propName;
instance.typeName = TypeName.get(type).box();
instance.annotations = annotations;
instance.sourceElement = sourceElement;
instance.afterHydrate();
return instance;
}
public abstract class Hydrated {
protected TypeSpec.Builder klazz;
protected List<TypeVariableName> typeVariables;
protected Set<SerializableClass> other;
protected TypeMirror type;
protected TypeMirror unboxedType;
protected CodeBlock.Builder code;
protected String name;
protected String argName;
protected String adapterName;
protected TypeName typeName;
protected List<? extends AnnotationMirror> annotations;
protected Element sourceElement;
public abstract boolean applies();
public abstract void generateWrite(Runnable writeGet);
public abstract void generateRead();
protected void afterHydrate() {}
protected void generateRead(CodeBlock.Builder code, TypeMirror type, String name, List<? extends AnnotationMirror> annotations) {
Adapters.generateRead(klazz, code, typeVariables, other, type, name, annotations, sourceElement, message);
}
protected void generateWrite(CodeBlock.Builder code, TypeMirror type, String name, List<? extends AnnotationMirror> annotations, Runnable writeGet) {
Adapters.generateWrite(klazz, code, typeVariables, other, type, name, annotations, sourceElement, message, writeGet);
}
protected TypeMirror unbox(TypeMirror type) {
try {
return typeUtils.unboxedType(type);
} catch (IllegalArgumentException e) {
return type;
}
}
}
}

View File

@ -0,0 +1,74 @@
package io.gitlab.jfronny.commons.serialize.generator.adapter;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeVariableName;
import io.gitlab.jfronny.commons.serialize.generator.Cl;
import io.gitlab.jfronny.commons.serialize.generator.core.TypeHelper;
import javax.lang.model.type.TypeMirror;
import java.lang.reflect.ParameterizedType;
import java.util.Iterator;
import java.util.List;
public abstract class AdapterAdapter<T extends AdapterAdapter<T>.Hydrated> extends Adapter<T> {
public abstract class Hydrated extends Adapter<T>.Hydrated {
@Override
public void generateWrite(Runnable writeGet) {
Object adapter = getAdapter();
code.add("$" + (adapter instanceof String ? "L" : "T") + ".write(", adapter);
writeGet.run();
code.add(", writer);\n");
}
@Override
public void generateRead() {
Object adapter = getAdapter();
code.add("$" + (adapter instanceof String ? "L" : "T") + ".read(reader)", adapter);
}
private Object getAdapter() {
for (FieldSpec spec : klazz.fieldSpecs) {
if (spec.name.equals(adapterName)) return adapterName;
}
return createAdapter(adapterName);
}
protected abstract Object createAdapter(String name);
protected void appendFieldTypeToken(boolean allowClassType) {
TypeName typeName = TypeName.get(type);
if (TypeHelper.isComplexType(type, typeUtils)) {
TypeName typeTokenType = ParameterizedTypeName.get(Cl.TYPE_TOKEN, typeName);
List<? extends TypeMirror> typeParams = TypeHelper.getGenericTypes(type);
if (typeParams.isEmpty()) {
code.add("new $T() {}", typeTokenType);
} else {
code.add("($T) $T.getParameterized($T.class, ", typeTokenType, Cl.TYPE_TOKEN, typeUtils.erasure(type));
for (Iterator<? extends TypeMirror> iterator = typeParams.iterator(); iterator.hasNext(); ) {
TypeMirror typeParam = iterator.next();
int typeIndex = typeVariables.indexOf(TypeVariableName.get(typeParam.toString()));
code.add("(($T)typeToken.getType()).getActualTypeArguments()[$L]", ParameterizedType.class, typeIndex);
if (iterator.hasNext()) {
code.add(", ");
}
}
code.add(")");
}
} else if (TypeHelper.isGenericType(type)) {
TypeName typeTokenType = ParameterizedTypeName.get(Cl.TYPE_TOKEN, typeName);
int typeIndex = typeVariables.indexOf(TypeVariableName.get(type.toString()));
code.add("($T) $T.get((($T)typeToken.getType()).getActualTypeArguments()[$L])",
typeTokenType, Cl.TYPE_TOKEN, ParameterizedType.class, typeIndex);
} else {
if (allowClassType) {
code.add("$T.class", typeName);
} else {
code.add("TypeToken.get($T.class)", typeName);
}
}
}
}
}

View File

@ -0,0 +1,63 @@
package io.gitlab.jfronny.commons.serialize.generator.adapter;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import io.gitlab.jfronny.commons.serialize.generator.SerializableClass;
import io.gitlab.jfronny.commons.serialize.generator.adapter.impl.*;
import io.gitlab.jfronny.commons.serialize.generator.core.value.Property;
import javax.annotation.processing.Messager;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
public class Adapters {
public static final List<Adapter<?>> ADAPTERS = List.of(
new DeclaredAdapter(),
new PrimitiveAdapter(),
new StringAdapter(),
new DateAdapter(),
new EnumAdapter(),
new ArrayAdapter(),
new CollectionAdapter(),
new MapAdapter(),
new OtherSerializableAdapter(),
new ReflectAdapter()
);
public static void generateRead(Property<?> prop, TypeSpec.Builder klazz, CodeBlock.Builder code, List<TypeVariableName> typeVariables, Set<SerializableClass> otherAdapters, Messager message) {
withAdapter(prop, klazz, code, typeVariables, otherAdapters, message, Adapter.Hydrated::generateRead);
}
public static void generateWrite(Property<?> prop, TypeSpec.Builder klazz, CodeBlock.Builder code, List<TypeVariableName> typeVariables, Set<SerializableClass> otherAdapters, Messager message, Runnable writeGet) {
withAdapter(prop, klazz, code, typeVariables, otherAdapters, message, adapter -> adapter.generateWrite(writeGet));
}
private static void withAdapter(Property<?> prop, TypeSpec.Builder klazz, CodeBlock.Builder code, List<TypeVariableName> typeVariables, Set<SerializableClass> otherAdapters, Messager message, Consumer<Adapter<?>.Hydrated> action) {
withAdapter(klazz, code, typeVariables, otherAdapters, prop.getType(), prop.getName(), prop.getAnnotations(), prop.getElement(), message, action);
}
public static void generateRead(TypeSpec.Builder klazz, CodeBlock.Builder code, List<TypeVariableName> typeVariables, Set<SerializableClass> other, TypeMirror type, String propName, List<? extends AnnotationMirror> annotations, Element sourceElement, Messager message) {
withAdapter(klazz, code, typeVariables, other, type, propName, annotations, sourceElement, message, Adapter.Hydrated::generateRead);
}
public static void generateWrite(TypeSpec.Builder klazz, CodeBlock.Builder code, List<TypeVariableName> typeVariables, Set<SerializableClass> other, TypeMirror type, String propName, List<? extends AnnotationMirror> annotations, Element sourceElement, Messager message, Runnable writeGet) {
withAdapter(klazz, code, typeVariables, other, type, propName, annotations, sourceElement, message, adapter -> adapter.generateWrite(writeGet));
}
private static void withAdapter(TypeSpec.Builder klazz, CodeBlock.Builder code, List<TypeVariableName> typeVariables, Set<SerializableClass> other, TypeMirror type, String propName, List<? extends AnnotationMirror> annotations, Element sourceElement, Messager message, Consumer<Adapter<?>.Hydrated> action) {
for (Adapter<?> adapter : Adapters.ADAPTERS) {
Adapter<?>.Hydrated hydrated = adapter.hydrate(klazz, code, typeVariables, other, type, propName, annotations, sourceElement);
if (hydrated.applies()) {
action.accept(hydrated);
return;
}
}
message.printMessage(Diagnostic.Kind.ERROR, "Could not find applicable adapter for property " + propName, sourceElement);
}
}

View File

@ -0,0 +1,86 @@
package io.gitlab.jfronny.commons.serialize.generator.adapter.impl;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.MethodSpec;
import io.gitlab.jfronny.commons.serialize.generator.Cl;
import io.gitlab.jfronny.commons.serialize.generator.adapter.Adapter;
import io.gitlab.jfronny.commons.serialize.generator.core.TypeHelper;
import javax.lang.model.element.Modifier;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.TypeMirror;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class ArrayAdapter extends Adapter<ArrayAdapter.Hydrated> {
@Override
public Hydrated instantiate() {
return new Hydrated();
}
public class Hydrated extends Adapter<Hydrated>.Hydrated {
private ArrayType type;
private TypeMirror componentType;
@Override
public boolean applies() {
return type != null;
}
@Override
protected void afterHydrate() {
type = TypeHelper.asArrayType(super.type);
componentType = type == null ? null : type.getComponentType();
}
@Override
public void generateWrite(Runnable writeGet) {
code.add("for ($T $N : ", componentType, argName);
writeGet.run();
code.beginControlFlow(")")
.beginControlFlow("if ($N == null)", argName)
.addStatement("if (writer.getSerializeNulls()) writer.nullValue()")
.nextControlFlow("else");
generateWrite(code, componentType, argName, componentType.getAnnotationMirrors(), () -> code.add(argName));
code.endControlFlow().endControlFlow();
}
@Override
public void generateRead() {
CodeBlock.Builder kode = CodeBlock.builder();
// Coerce
kode.beginControlFlow("if (reader.isLenient() && reader.peek() != $T.BEGIN_ARRAY)", Cl.GSON_TOKEN)
.add("return new $T[] { ", componentType);
generateRead(kode, componentType, argName, componentType.getAnnotationMirrors());
kode.add(" };\n").endControlFlow();
kode.addStatement("$T<$T> list = new $T<>()", List.class, componentType, ArrayList.class)
.addStatement("reader.beginArray()")
.beginControlFlow("while (reader.hasNext())")
.beginControlFlow("if (reader.peek() == $T.NULL)", Cl.GSON_TOKEN)
.addStatement("reader.nextNull()")
.addStatement("list.add(null)")
.nextControlFlow("else")
.add("list.add(");
generateRead(kode, componentType, argName, componentType.getAnnotationMirrors());
kode.add(");\n")
.endControlFlow()
.endControlFlow()
.addStatement("reader.endArray()")
.addStatement("return list.toArray($T[]::new)", componentType);
String methodName = "read$" + name;
klazz.addMethod(
MethodSpec.methodBuilder(methodName)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.returns(typeName)
.addParameter(Cl.SERIALIZE_READER, "reader")
.addException(IOException.class)
.addCode(kode.build())
.build()
);
code.add("$N(reader)", methodName);
}
}
}

View File

@ -0,0 +1,118 @@
package io.gitlab.jfronny.commons.serialize.generator.adapter.impl;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import io.gitlab.jfronny.commons.data.MutCollection;
import io.gitlab.jfronny.commons.serialize.generator.Cl;
import io.gitlab.jfronny.commons.serialize.generator.adapter.Adapter;
import io.gitlab.jfronny.commons.serialize.generator.core.TypeHelper;
import javax.lang.model.element.Modifier;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import java.io.IOException;
import java.util.*;
public class CollectionAdapter extends Adapter<CollectionAdapter.Hydrated> {
@Override
public Hydrated instantiate() {
return new Hydrated();
}
public class Hydrated extends Adapter<Hydrated>.Hydrated {
private static final Map<Class<?>, List<Class<?>>> SUPPORTED = MutCollection.mapOf(
Set.class, List.of(LinkedHashSet.class, HashSet.class, TreeSet.class),
List.class, List.of(ArrayList.class, LinkedList.class),
Queue.class, List.of(ArrayDeque.class, LinkedList.class),
Deque.class, List.of(ArrayDeque.class)
);
private DeclaredType type;
private TypeName implType;
private TypeMirror componentType;
@Override
public boolean applies() {
return type != null;
}
@Override
protected void afterHydrate() {
type = TypeHelper.asDeclaredType(super.type);
componentType = null;
if (type == null) return;
List<? extends TypeMirror> typeArguments = type.getTypeArguments();
if (typeArguments.size() == 0) {
type = null;
} else {
componentType = typeArguments.get(0);
String ts = TypeHelper.asDeclaredType(typeUtils.erasure(type)).asElement().toString();
for (Map.Entry<Class<?>, List<Class<?>>> entry : SUPPORTED.entrySet()) {
if (entry.getKey().getCanonicalName().equals(ts)) {
implType = TypeName.get(entry.getValue().get(0));
return;
}
for (Class<?> klazz : entry.getValue()) {
if (klazz.getCanonicalName().equals(ts)) {
implType = TypeName.get(klazz);
return;
}
}
}
type = null;
componentType = null;
}
}
@Override
public void generateWrite(Runnable writeGet) {
code.addStatement("writer.beginArray()");
code.add("for ($T $N : ", componentType, argName);
writeGet.run();
code.beginControlFlow(")")
.beginControlFlow("if ($N == null)", argName)
.addStatement("if (writer.getSerializeNulls()) writer.nullValue()")
.nextControlFlow("else");
generateWrite(code, componentType, argName, componentType.getAnnotationMirrors(), () -> code.add(argName));
code.endControlFlow().endControlFlow().addStatement("writer.endArray()");
}
@Override
public void generateRead() {
CodeBlock.Builder kode = CodeBlock.builder();
kode.addStatement("$T list = new $T<>()", typeName, implType);
// Coerce
kode.beginControlFlow("if (reader.isLenient() && reader.peek() != $T.BEGIN_ARRAY)", Cl.GSON_TOKEN)
.add("list.add(");
generateRead(kode, componentType, argName, componentType.getAnnotationMirrors());
kode.add(");\n").addStatement("return list").endControlFlow();
kode.addStatement("reader.beginArray()")
.beginControlFlow("while (reader.hasNext())")
.beginControlFlow("if (reader.peek() == $T.NULL)", Cl.GSON_TOKEN)
.addStatement("reader.nextNull()")
.addStatement("list.add(null)")
.nextControlFlow("else")
.add("list.add(");
generateRead(kode, componentType, argName, componentType.getAnnotationMirrors());
kode.add(");\n")
.endControlFlow()
.endControlFlow()
.addStatement("reader.endArray()")
.addStatement("return list");
String methodName = "read$" + name;
klazz.addMethod(
MethodSpec.methodBuilder(methodName)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.returns(typeName)
.addParameter(Cl.SERIALIZE_READER, "reader")
.addException(IOException.class)
.addCode(kode.build())
.build()
);
code.add("$N(reader)", methodName);
}
}
}

View File

@ -0,0 +1,64 @@
package io.gitlab.jfronny.commons.serialize.generator.adapter.impl;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.MethodSpec;
import io.gitlab.jfronny.commons.serialize.generator.Cl;
import io.gitlab.jfronny.commons.serialize.generator.adapter.Adapter;
import javax.lang.model.element.Modifier;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Date;
public class DateAdapter extends Adapter<DateAdapter.Hydrated> {
@Override
public Hydrated instantiate() {
return new Hydrated();
}
public class Hydrated extends Adapter<Hydrated>.Hydrated {
@Override
public boolean applies() {
return type.toString().equals(Date.class.getCanonicalName());
}
@Override
public void generateWrite(Runnable writeGet) {
code.add("writer.value($T.format(", Cl.GISO8601UTILS);
writeGet.run();
code.add("));\n");
}
@Override
public void generateRead() {
boolean found = false;
for (MethodSpec spec : klazz.methodSpecs) {
if (spec.name.equals("parseDate")) {
found = true;
break;
}
}
if (!found) {
klazz.addMethod(
MethodSpec.methodBuilder("parseDate")
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.returns(Date.class)
.addParameter(String.class, "date")
.addCode(
CodeBlock.builder()
.beginControlFlow("try")
.addStatement("return $T.parse(date, new $T(0))", Cl.GISO8601UTILS, ParsePosition.class)
.nextControlFlow("catch ($T e)", ParseException.class)
.addStatement("throw new $T(\"Failed Parsing '\" + date + \"' as Date\", e)", Cl.MALFORMED_DATA_EXCEPTION)
.endControlFlow()
.build()
)
.build()
);
}
code.add("parseDate(reader.nextString())");
}
}
}

View File

@ -0,0 +1,52 @@
package io.gitlab.jfronny.commons.serialize.generator.adapter.impl;
import com.squareup.javapoet.TypeName;
import io.gitlab.jfronny.commons.serialize.generator.Cl;
import io.gitlab.jfronny.commons.serialize.generator.adapter.AdapterAdapter;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.DeclaredType;
import java.util.List;
import java.util.Map;
public class DeclaredAdapter extends AdapterAdapter<DeclaredAdapter.Hydrated> {
@Override
public Hydrated instantiate() {
return new Hydrated();
}
public class Hydrated extends AdapterAdapter<Hydrated>.Hydrated {
private DeclaredType typeAdapterClass;
@Override
public boolean applies() {
return typeAdapterClass != null;
}
@Override
protected void afterHydrate() {
this.typeAdapterClass = findTypeAdapterClass(annotations);
}
@Override
protected TypeName createAdapter(String typeAdapterName) {
return TypeName.get(typeAdapterClass);
}
private static DeclaredType findTypeAdapterClass(List<? extends AnnotationMirror> annotations) {
for (AnnotationMirror annotation : annotations) {
String typeName = annotation.getAnnotationType().toString();
if (typeName.equals(Cl.GWITH.toString())) {
Map<? extends ExecutableElement, ? extends AnnotationValue> elements = annotation.getElementValues();
if (!elements.isEmpty()) {
AnnotationValue value = elements.values().iterator().next();
return (DeclaredType) value.getValue();
}
}
}
return null;
}
}
}

View File

@ -0,0 +1,65 @@
package io.gitlab.jfronny.commons.serialize.generator.adapter.impl;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import io.gitlab.jfronny.commons.serialize.generator.adapter.Adapter;
import io.gitlab.jfronny.commons.serialize.generator.core.TypeHelper;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.type.DeclaredType;
public class EnumAdapter extends Adapter<EnumAdapter.Hydrated> {
@Override
public Hydrated instantiate() {
return new Hydrated();
}
public class Hydrated extends Adapter<Hydrated>.Hydrated {
private DeclaredType tel;
@Override
protected void afterHydrate() {
super.afterHydrate();
tel = null;
DeclaredType declared = TypeHelper.asDeclaredType(type);
if (declared == null) return;
if (declared.asElement().getKind() != ElementKind.ENUM) return;
tel = declared;
}
@Override
public boolean applies() {
return tel != null;
}
@Override
public void generateWrite(Runnable writeGet) {
code.add("writer.value(");
writeGet.run();
code.add(".name());\n");
}
@Override
public void generateRead() {
String methodName = "read$" + name;
klazz.addMethod(
MethodSpec.methodBuilder(methodName)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.returns(TypeName.get(tel))
.addParameter(String.class, "value")
.addCode(
CodeBlock.builder()
.beginControlFlow("for ($1T t : $1T.values())", tel)
.addStatement("if (t.name().equals(value)) return t")
.endControlFlow()
.addStatement("return null")
.build()
)
.build()
);
code.add("$N(reader.nextString())", methodName);
}
}
}

View File

@ -0,0 +1,196 @@
package io.gitlab.jfronny.commons.serialize.generator.adapter.impl;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import io.gitlab.jfronny.commons.data.String2ObjectMap;
import io.gitlab.jfronny.commons.serialize.generator.Cl;
import io.gitlab.jfronny.commons.serialize.generator.adapter.Adapter;
import io.gitlab.jfronny.commons.serialize.generator.core.TypeHelper;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import java.io.IOException;
import java.util.*;
public class MapAdapter extends Adapter<MapAdapter.Hydrated> {
@Override
public Hydrated instantiate() {
return new Hydrated();
}
public class Hydrated extends Adapter<Hydrated>.Hydrated {
private static final List<Class<? extends Map>> SUPPORTED = List.of(
LinkedHashMap.class,
HashMap.class,
TreeMap.class,
String2ObjectMap.class
);
private DeclaredType type;
private TypeName implType;
private TypeMirror componentType1;
private TypeMirror componentType2;
@Override
public boolean applies() {
return type != null;
}
@Override
protected void afterHydrate() {
type = TypeHelper.asDeclaredType(super.type);
componentType1 = componentType2 = null;
if (type == null) return;
List<? extends TypeMirror> typeArguments = type.getTypeArguments();
if (typeArguments.size() != 2) {
type = null;
} else {
componentType1 = typeArguments.get(0);
if (!isValidKey(componentType1)) {
type = null;
componentType1 = null;
return;
}
componentType2 = typeArguments.get(1);
String ts = TypeHelper.asDeclaredType(typeUtils.erasure(type)).asElement().toString();
if (Map.class.getCanonicalName().equals(ts)) {
implType = TypeName.get(SUPPORTED.get(0));
return;
}
for (Class<?> klazz : SUPPORTED) {
if (klazz.getCanonicalName().equals(ts)) {
implType = TypeName.get(klazz);
return;
}
}
type = null;
componentType1 = componentType2 = null;
}
}
private boolean isValidKey(TypeMirror tm) {
if (tm.toString().equals(String.class.getCanonicalName())) return true;
if (tm.toString().equals(UUID.class.getCanonicalName())) return true;
if (unbox(tm).getKind().isPrimitive()) return true;
if (isEnum(tm)) return true;
return false;
}
private void generateConvertKey(CodeBlock.Builder kode) {
if (componentType1.toString().equals(String.class.getCanonicalName())) {
kode.add("name");
}
else if (componentType1.toString().equals(UUID.class.getCanonicalName())) {
kode.add("name == null ? null : $T.fromString(name)", UUID.class);
}
else if (unbox(componentType1).getKind().isPrimitive()) {
kode.add("name == null ? null : " + switch (unbox(componentType1).getKind()) {
case BOOLEAN -> "Boolean.parseBoolean(name)";
case BYTE -> "Byte.parseByte(name)";
case SHORT -> "Short.parseShort(name)";
case INT -> "Integer.parseInt(name)";
case LONG -> "Long.parseLong(name)";
case CHAR -> "name.length() == 0 ? '\\0' : name.charAt(0)";
case FLOAT -> "Float.parseFloat(name)";
case DOUBLE -> "Double.parseDouble(name)";
default -> throw new IllegalArgumentException("Unsupported primitive: " + unbox(componentType1).getKind());
});
}
else if (isEnum(componentType1)) {
String methodName = "readName$" + name;
boolean found = false;
for (MethodSpec spec : klazz.methodSpecs) {
if (spec.name.equals(methodName)) {
found = true;
break;
}
}
if (!found) {
klazz.addMethod(
MethodSpec.methodBuilder(methodName)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.returns(TypeName.get(componentType1))
.addParameter(String.class, "value")
.addCode(
CodeBlock.builder()
.beginControlFlow("for ($1T t : $1T.values())", componentType1)
.addStatement("if (t.name().equals(value)) return t")
.endControlFlow()
.addStatement("return null")
.build()
)
.build()
);
}
kode.add("$N(name)", methodName);
}
}
private boolean isEnum(TypeMirror tm) {
DeclaredType declared = TypeHelper.asDeclaredType(tm);
return declared != null && declared.asElement().getKind() == ElementKind.ENUM;
}
@Override
public void generateWrite(Runnable writeGet) {
code.addStatement("writer.beginObject()");
code.add("for ($T.Entry<$T, $T> $N : (", Map.class, componentType1, componentType2, argName);
writeGet.run();
code.beginControlFlow(").entrySet())")
.beginControlFlow("if ($N.getKey() != null || writer.getSerializeNulls())", argName)
.add("writer.name(");
if (isEnum(componentType1)) {
code.add("$N.getKey() == null ? \"null\" : $N.getKey().name()", argName, argName);
} else {
code.add("$T.toString($N.getKey())", Objects.class, argName);
}
code.addStatement(")")
.addStatement("$T value$N = $N.getValue()", componentType2, argName, argName)
.beginControlFlow("if (value$N == null)", argName)
.addStatement("if (writer.getSerializeNulls()) writer.nullValue()")
.nextControlFlow("else");
generateWrite(code, componentType2, "value" + argName, componentType2.getAnnotationMirrors(), () -> code.add("value" + argName));
code.endControlFlow().endControlFlow().endControlFlow().addStatement("writer.endObject()");
}
@Override
public void generateRead() {
CodeBlock.Builder kode = CodeBlock.builder();
kode.addStatement("$T map = new $T<>()", typeName, implType);
kode.addStatement("reader.beginObject()")
.beginControlFlow("while (reader.hasNext())")
.addStatement("String name = reader.nextName()")
.beginControlFlow("if (reader.peek() == $T.NULL)", Cl.GSON_TOKEN)
.addStatement("reader.nextNull()")
.add("map.put(");
generateConvertKey(kode);
kode.addStatement(", null)")
.nextControlFlow("else")
.add("map.put(");
generateConvertKey(kode);
kode.add(", ");
generateRead(kode, componentType2, argName, componentType2.getAnnotationMirrors());
kode.addStatement(")")
.endControlFlow()
.endControlFlow()
.addStatement("reader.endObject()")
.addStatement("return map");
String methodName = "read$" + name;
klazz.addMethod(
MethodSpec.methodBuilder(methodName)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.returns(typeName)
.addParameter(Cl.SERIALIZE_READER, "reader")
.addException(IOException.class)
.addCode(kode.build())
.build()
);
code.add("$N(reader)", methodName);
}
}
}

View File

@ -0,0 +1,37 @@
package io.gitlab.jfronny.commons.serialize.generator.adapter.impl;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.TypeName;
import io.gitlab.jfronny.commons.serialize.generator.SerializableClass;
import io.gitlab.jfronny.commons.serialize.generator.adapter.AdapterAdapter;
public class OtherSerializableAdapter extends AdapterAdapter<OtherSerializableAdapter.Hydrated> {
@Override
public Hydrated instantiate() {
return new Hydrated();
}
public class Hydrated extends AdapterAdapter<Hydrated>.Hydrated {
private ClassName adapter;
@Override
public boolean applies() {
return adapter != null;
}
@Override
protected ClassName createAdapter(String name) {
return adapter;
}
@Override
protected void afterHydrate() {
for (SerializableClass adapter : other) {
if (TypeName.get(adapter.classElement().asType()).equals(typeName)) {
// Use self-made adapter
this.adapter = adapter.generatedClassName();
}
}
}
}
}

View File

@ -0,0 +1,39 @@
package io.gitlab.jfronny.commons.serialize.generator.adapter.impl;
import io.gitlab.jfronny.commons.serialize.generator.adapter.Adapter;
public class PrimitiveAdapter extends Adapter<PrimitiveAdapter.Hydrated> {
@Override
public Hydrated instantiate() {
return new Hydrated();
}
public class Hydrated extends Adapter<Hydrated>.Hydrated {
@Override
public boolean applies() {
return unboxedType.getKind().isPrimitive();
}
@Override
public void generateWrite(Runnable writeGet) {
code.add("writer.value(");
writeGet.run();
code.add(");\n");
}
@Override
public void generateRead() {
code.add(switch (unboxedType.getKind()) {
case BOOLEAN -> "reader.nextBoolean()";
case BYTE -> "(byte) reader.nextInt()";
case SHORT -> "(short) reader.nextInt()";
case INT -> "reader.nextInt()";
case LONG -> "reader.nextLong()";
case CHAR -> "(char) reader.nextInt()";
case FLOAT -> "(float) reader.nextDouble()";
case DOUBLE -> "reader.nextDouble()";
default -> throw new IllegalArgumentException("Unsupported primitive: " + unboxedType.getKind());
});
}
}
}

View File

@ -0,0 +1,42 @@
package io.gitlab.jfronny.commons.serialize.generator.adapter.impl;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import io.gitlab.jfronny.commons.serialize.generator.Cl;
import io.gitlab.jfronny.commons.serialize.generator.adapter.AdapterAdapter;
import javax.lang.model.element.Modifier;
import javax.tools.Diagnostic;
public class ReflectAdapter extends AdapterAdapter<ReflectAdapter.Hydrated> {
@Override
public Hydrated instantiate() {
return new Hydrated();
}
public class Hydrated extends AdapterAdapter<Hydrated>.Hydrated {
@Override
public boolean applies() {
return !options.containsKey("serializeProcessorNoReflect");
}
@Override
protected String createAdapter(String typeAdapterName) {
message.printMessage(Diagnostic.Kind.WARNING, "Falling back to adapter detection for unsupported type " + type, sourceElement);
TypeName typeAdapterType = ParameterizedTypeName.get(Cl.TYPE_ADAPTER, typeName);
CodeBlock.Builder block = CodeBlock.builder();
block.add("$T.CURRENT.get().getAdapter(", Cl.OBJECT_MAPPER);
appendFieldTypeToken(true);
block.add(")");
klazz.addField(
FieldSpec.builder(typeAdapterType, typeAdapterName)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer(block.build())
.build()
);
return typeAdapterName;
}
}
}

View File

@ -0,0 +1,29 @@
package io.gitlab.jfronny.commons.serialize.generator.adapter.impl;
import io.gitlab.jfronny.commons.serialize.generator.adapter.Adapter;
public class StringAdapter extends Adapter<StringAdapter.Hydrated> {
@Override
public Hydrated instantiate() {
return new Hydrated();
}
public class Hydrated extends Adapter<Hydrated>.Hydrated {
@Override
public boolean applies() {
return type.toString().equals(String.class.getCanonicalName());
}
@Override
public void generateWrite(Runnable writeGet) {
code.add("writer.value(");
writeGet.run();
code.add(");\n");
}
@Override
public void generateRead() {
code.add("reader.nextString()");
}
}
}

View File

@ -0,0 +1,229 @@
package io.gitlab.jfronny.commons.serialize.generator.gprocessor;
import com.squareup.javapoet.*;
import io.gitlab.jfronny.commons.serialize.generator.Cl;
import io.gitlab.jfronny.commons.serialize.generator.SerializableClass;
import io.gitlab.jfronny.commons.serialize.generator.core.value.ElementException;
import io.gitlab.jfronny.commons.serialize.generator.core.value.Property;
import io.gitlab.jfronny.commons.serialize.generator.core.value.ValueCreator;
import javax.annotation.processing.Messager;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Modifier;
import javax.lang.model.type.TypeMirror;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.Set;
import java.util.function.UnaryOperator;
public abstract class GProcessor {
protected final ValueCreator valueCreator;
protected final Messager message;
protected final boolean hasManifold;
private final boolean isStatic;
private final String readStatement;
private final String writeStatement;
private final boolean disableSafe;
public GProcessor(ValueCreator valueCreator, Messager message, boolean hasManifold, boolean isStatic, boolean disableSafe) {
this.valueCreator = valueCreator;
this.message = message;
this.hasManifold = hasManifold;
this.disableSafe = disableSafe;
this.isStatic = isStatic;
this.readStatement = isStatic ? "read(reader)" : "return read(reader)";
this.writeStatement = isStatic ? "write(writer)" : "write(value, writer)";
}
public abstract ClassName generateDelegatingAdapter(TypeSpec.Builder spec, TypeName classType, ClassName generatedClassName);
public abstract void generateSerialisation(TypeSpec.Builder spec, SerializableClass self, List<TypeVariableName> typeVariables, Set<SerializableClass> otherAdapters) throws ElementException;
protected String getSerializedName(Property<?> property) {
for (AnnotationMirror annotationMirror : property.getAnnotations()) {
if (annotationMirror.getAnnotationType().asElement().toString().equals(Cl.SERIALIZED_NAME.toString())) {
return (String) annotationMirror.getElementValues().values().iterator().next().getValue();
}
}
return property.getName();
}
protected MethodSpec.Builder extension(MethodSpec.Builder method) {
if (hasManifold) method.addAnnotation(Cl.MANIFOLD_EXTENSION);
return method;
}
protected MethodSpec.Builder extension(MethodSpec.Builder method, TypeName thizName) {
if (thizName == null) return extension(method);
if (hasManifold) {
method.addAnnotation(Cl.MANIFOLD_EXTENSION);
method.addParameter(ParameterSpec.builder(thizName, "value").addAnnotation(Cl.MANIFOLD_THIS).build());
}
else {
method.addParameter(thizName, "value");
}
return method;
}
protected void generateComments(Property<?> prop, CodeBlock.Builder code) {
for (AnnotationMirror annotation : prop.getAnnotations()) {
if (annotation.getAnnotationType().asElement().toString().equals(Cl.GCOMMENT.toString())) {
String comment = (String) annotation.getElementValues().values().iterator().next().getValue();
code.addStatement("if (writer.isLenient()) writer.comment($S)", comment);
}
}
}
public void generateDelegateToAdapter(TypeSpec.Builder spec, TypeName classType, TypeMirror adapter, boolean isStatic) {
if (!isStatic) {
spec.addField(
FieldSpec.builder(TypeName.get(adapter), "adapter", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("new $T()", adapter)
.build()
);
}
spec.addMethod(
extension(MethodSpec.methodBuilder("read"))
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(Cl.SERIALIZE_READER, "reader")
.addException(IOException.class)
.returns(isStatic ? TypeName.VOID : classType)
.addCode(isStatic ? this.isStatic ? "$T.read(reader);" : "return $T.read(reader);" : this.isStatic ? "$L.read(reader);" : "return $L.read(reader);", isStatic ? adapter : "adapter")
.build()
);
spec.addMethod(
extension(MethodSpec.methodBuilder("write"), this.isStatic ? null : classType)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(Cl.SERIALIZE_WRITER, "writer")
.addException(IOException.class)
.addCode(isStatic ? "$T.$L;" : "$L.$L", isStatic ? adapter : "adapter", writeStatement)
.build()
);
}
public void generateAuxiliary(TypeSpec.Builder spec, TypeName classType, TypeMirror configure) {
final UnaryOperator<CodeBlock.Builder> configureReader = cb -> {
if (configure != null) cb.addStatement("$T.configure(reader)", configure);
return cb;
};
final UnaryOperator<CodeBlock.Builder> configureWriter = cb -> {
if (configure != null) cb.addStatement("$T.configure(writer)", configure);
return cb;
};
spec.addMethod(
extension(MethodSpec.methodBuilder("read"))
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(TypeName.get(Reader.class), "in")
.addException(IOException.class)
.returns(isStatic ? TypeName.VOID : classType)
.addCode(configureReader.apply(CodeBlock.builder().beginControlFlow("try ($1T reader = new $1T(in))", Cl.SERIALIZE_READER))
.addStatement(readStatement)
.endControlFlow()
.build())
.build()
);
spec.addMethod(
extension(MethodSpec.methodBuilder("read"))
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(TypeName.get(String.class), "json")
.addException(IOException.class)
.returns(isStatic ? TypeName.VOID : classType)
.addCode(CodeBlock.builder().beginControlFlow("try ($1T reader = new $1T(json))", StringReader.class)
.addStatement(readStatement)
.endControlFlow()
.build())
.build()
);
spec.addMethod(
extension(MethodSpec.methodBuilder("read"))
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(Cl.GSON_ELEMENT, "tree")
.addException(IOException.class)
.returns(isStatic ? TypeName.VOID : classType)
.addCode(configureReader.apply(CodeBlock.builder().beginControlFlow("try ($1T reader = new $1T(tree))", Cl.EMULATED_READER))
.addStatement(readStatement)
.endControlFlow()
.build())
.build()
);
spec.addMethod(
extension(MethodSpec.methodBuilder("read"))
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(Path.class, "path")
.addException(IOException.class)
.returns(isStatic ? TypeName.VOID : classType)
.addCode(CodeBlock.builder().beginControlFlow("try ($T reader = $T.newBufferedReader(path))", BufferedReader.class, Files.class)
.addStatement(readStatement)
.endControlFlow()
.build())
.build()
);
spec.addMethod(
extension(MethodSpec.methodBuilder("write"), isStatic ? null : classType)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(Writer.class, "out")
.addException(IOException.class)
.addCode(configureWriter.apply(CodeBlock.builder().beginControlFlow("try ($1T writer = new $1T(out))", Cl.SERIALIZE_WRITER))
.addStatement(writeStatement)
.endControlFlow()
.build())
.build()
);
spec.addMethod(
extension(MethodSpec.methodBuilder("write"), isStatic ? null : classType)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(Path.class, "path")
.addException(IOException.class)
.addCode(disableSafe ? CodeBlock.builder().beginControlFlow("try ($1T writer = $2T.newBufferedWriter(path, $3T.CREATE, $3T.WRITE, $3T.TRUNCATE_EXISTING))", BufferedWriter.class, Files.class, StandardOpenOption.class)
.addStatement(writeStatement)
.endControlFlow()
.build() : CodeBlock.builder()
.addStatement("$T temp = $T.createTempFile($S, $S)", Path.class, Files.class, "serializegenerator-", ".json")
.beginControlFlow("try ($1T writer = $2T.newBufferedWriter(temp, $3T.CREATE, $3T.WRITE, $3T.TRUNCATE_EXISTING))", BufferedWriter.class, Files.class, StandardOpenOption.class)
.addStatement(writeStatement)
.addStatement("$T.move(temp, path, $T.REPLACE_EXISTING)", Files.class, StandardCopyOption.class)
.nextControlFlow("finally")
.addStatement("$T.deleteIfExists(temp)", Files.class)
.endControlFlow()
.build())
.build()
);
spec.addMethod(
extension(MethodSpec.methodBuilder("toJson"), isStatic ? null : classType)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addException(IOException.class)
.returns(String.class)
.addCode(CodeBlock.builder().beginControlFlow("try ($1T writer = new $1T())", StringWriter.class)
.addStatement(writeStatement)
.addStatement("return writer.toString()")
.endControlFlow()
.build())
.build()
);
spec.addMethod(
extension(MethodSpec.methodBuilder("toJsonTree"), isStatic ? null : classType)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addException(IOException.class)
.returns(Cl.GSON_ELEMENT)
.addCode(configureWriter.apply(CodeBlock.builder().beginControlFlow("try ($1T writer = new $1T())", Cl.EMULATED_WRITER))
.addStatement(writeStatement)
.addStatement("return writer.get()")
.endControlFlow()
.build())
.build()
);
}
}

View File

@ -0,0 +1,208 @@
package io.gitlab.jfronny.commons.serialize.generator.gprocessor;
import com.squareup.javapoet.*;
import io.gitlab.jfronny.commons.serialize.generator.Cl;
import io.gitlab.jfronny.commons.serialize.generator.SerializableClass;
import io.gitlab.jfronny.commons.serialize.generator.adapter.Adapters;
import io.gitlab.jfronny.commons.serialize.generator.core.TypeHelper;
import io.gitlab.jfronny.commons.serialize.generator.core.value.*;
import javax.annotation.processing.Messager;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import java.io.IOException;
import java.util.List;
import java.util.Set;
public class InstanceProcessor extends GProcessor {
public InstanceProcessor(ValueCreator valueCreator, Messager message, boolean hasManifold, boolean disableSafe) {
super(valueCreator, message, hasManifold, false, disableSafe);
}
@Override
public ClassName generateDelegatingAdapter(TypeSpec.Builder spec, TypeName classType, ClassName generatedClassName) {
spec.addType(
TypeSpec.classBuilder("Adapter")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addAnnotation(AnnotationSpec.builder(Cl.SERIALIZER_FOR).addMember("targets", CodeBlock.builder().add("$T.class", classType).build()).build())
.superclass(ParameterizedTypeName.get(Cl.TYPE_ADAPTER, classType))
.addMethod(MethodSpec.methodBuilder("write")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(Cl.SERIALIZE_WRITER, "writer")
.addParameter(classType, "value")
.addException(IOException.class)
.addCode(generatedClassName.simpleName() + ".write(value, writer);")
.build())
.addMethod(MethodSpec.methodBuilder("read")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(Cl.SERIALIZE_READER, "reader")
.addException(IOException.class)
.returns(classType)
.addCode("return " + generatedClassName.simpleName() + ".read(reader);")
.build())
.build()
);
return generatedClassName.nestedClass("Adapter");
}
// !!!WARNING!!!
// A lot of this code is common between InstanceProcessor and StaticProcessor
// Make sure they don't get out of sync!
// (Or, alternatively, create one common solution for these)
// !!!WARNING!!!
@Override
public void generateSerialisation(TypeSpec.Builder spec, SerializableClass self, List<TypeVariableName> typeVariables, Set<SerializableClass> otherAdapters) throws ElementException {
Value value = self.builder() == null ? valueCreator.from(self.classElement(), false) : valueCreator.from(TypeHelper.asDeclaredType(self.builder()).asElement(), true);
ConstructionSource constructionSource = value.getConstructionSource();
Properties properties = value.getProperties();
// public static void write(JsonWriter writer, T value) throws IOException
{
CodeBlock.Builder code = CodeBlock.builder();
code.beginControlFlow("if (value == null)")
.addStatement("writer.nullValue()")
.addStatement("return")
.endControlFlow();
code.addStatement("writer.beginObject()");
for (Property.Field param : properties.fields) {
if (Properties.containsName(properties.getters, param)) continue;
Runnable writeGet = () -> code.add("value.$N", param.getCallableName());
if (param.getType().getKind().isPrimitive()) {
generateComments(param, code);
code.addStatement("writer.name($S)", getSerializedName(param));
Adapters.generateWrite(param, spec, code, typeVariables, otherAdapters, message, writeGet);
} else {
code.beginControlFlow("if (value.$N != null || writer.getSerializeNulls())", param.getCallableName());
generateComments(param, code);
code.addStatement("writer.name($S)", getSerializedName(param));
code.addStatement("if (value.$N == null) writer.nullValue()", param.getCallableName());
code.beginControlFlow("else");
Adapters.generateWrite(param, spec, code, typeVariables, otherAdapters, message, writeGet);
code.endControlFlow();
code.endControlFlow();
}
}
for (Property.Getter param : properties.getters) {
if (param.getType().getKind().isPrimitive()) {
generateComments(param, code);
code.addStatement("writer.name($S)", getSerializedName(param));
Adapters.generateWrite(param, spec, code, typeVariables, otherAdapters, message, () -> code.add("value.$N()", param.getCallableName()));
} else {
code.addStatement("$T $L$N = value.$N()", param.getType(), "$", param.getCallableName(), param.getCallableName());
code.beginControlFlow("if ($L$N != null || writer.getSerializeNulls())", "$", param.getCallableName());
generateComments(param, code);
code.addStatement("writer.name($S)", getSerializedName(param));
code.addStatement("if ($L$N == null) writer.nullValue()", "$", param.getCallableName());
code.beginControlFlow("else");
Adapters.generateWrite(param, spec, code, typeVariables, otherAdapters, message, () -> code.add("$L$N", "$", param.getCallableName()));
code.endControlFlow();
code.endControlFlow();
}
}
code.addStatement("writer.endObject()");
spec.addMethod(extension(MethodSpec.methodBuilder("write"), self.getTypeName())
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(Cl.SERIALIZE_WRITER, "writer")
.addException(IOException.class)
.addCode(code.build())
.build());
}
// public static T read(JsonReader reader) throws IOException
{
CodeBlock.Builder code = CodeBlock.builder();
code.beginControlFlow("if (reader.peek() == $T.NULL)", Cl.GSON_TOKEN)
.addStatement("reader.nextNull()")
.addStatement("return null")
.endControlFlow();
boolean isEmpty = true;
for (Property<?> param : properties.names) {
isEmpty = false;
code.addStatement("$T _$N = $L", param.getType(), param.getName(), TypeHelper.getDefaultValue(param.getType()));
code.addStatement("boolean has_$N = false", param.getName());
}
if (isEmpty) {
code.addStatement("reader.skipValue()");
} else {
code.addStatement("reader.beginObject()")
.beginControlFlow("while (reader.hasNext())")
.beginControlFlow("switch (reader.nextName())");
for (Property<?> param : properties.names) {
code.beginControlFlow("case $S ->", getSerializedName(param));
if (param.getType().getKind().isPrimitive()) {
code.add("_$N = ", param.getName());
Adapters.generateRead(param, spec, code, typeVariables, otherAdapters, message);
code.add(";\n");
} else {
code.beginControlFlow("if (reader.peek() == $T.NULL)", Cl.GSON_TOKEN)
.addStatement("reader.nextNull()")
.addStatement("_$N = null", param.getName());
code.unindent().add("} else _$N = ", param.getName());
Adapters.generateRead(param, spec, code, typeVariables, otherAdapters, message);
code.add(";\n");
}
code.addStatement("has_$N = true", param.getName());
code.endControlFlow();
}
code.add("default -> ")
.addStatement("reader.skipValue()");
code.endControlFlow()
.endControlFlow()
.addStatement("reader.endObject()");
}
code.addStatement("$T result", self.getTypeName());
ClassName creatorName = ClassName.get((TypeElement) constructionSource.getConstructionElement().getEnclosingElement());
if (constructionSource instanceof ConstructionSource.Builder builder) {
StringBuilder args = new StringBuilder();
for (Property.ConstructorParam param : properties.constructorParams) {
args.append(", _").append(param.getName());
}
code.add("$T builder = ", builder.getBuilderClass());
if (constructionSource.isConstructor()) {
code.add("new $T($L)", builder.getBuilderClass(), args.length() > 0 ? args.substring(2) : "");
} else {
code.add("$T.$N($L)", creatorName, self.classElement().getSimpleName(), args.length() > 0 ? args.substring(2) : "");
}
code.add(";\n");
for (Property.Setter param : properties.builderParams) {
code.addStatement("if (has_$N) builder.$N(_$N)", param.getName(), param.getCallableName(), param.getName());
}
code.addStatement("result = builder.$N()", builder.getBuildMethod().getSimpleName());
} else {
StringBuilder args = new StringBuilder();
for (Property.Param param : properties.params) {
args.append(", _").append(param.getName());
}
if (constructionSource.isConstructor()) {
code.addStatement("result = new $T($L)", self.getTypeName(), args.length() > 0 ? args.substring(2) : "");
} else {
code.addStatement("result = $T.$N($L)", creatorName, constructionSource.getConstructionElement().getSimpleName(), args.length() > 0 ? args.substring(2) : "");
}
}
for (Property.Setter setter : properties.setters) {
code.addStatement("if (has_$N) result.$N(_$N)", setter.getName(), setter.getCallableName(), setter.getName());
}
for (Property.Field field : properties.fields) {
if (Properties.containsName(properties.setters, field)) continue;
if (Properties.containsName(properties.params, field)) continue;
code.addStatement("if (has_$N) result.$N = _$N", field.getName(), field.getCallableName(), field.getName());
}
code.addStatement("return result");
spec.addMethod(extension(MethodSpec.methodBuilder("read"))
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(self.getTypeName())
.addParameter(Cl.SERIALIZE_READER, "reader")
.addException(IOException.class)
.addCode(code.build())
.build());
}
}
}

View File

@ -0,0 +1,136 @@
package io.gitlab.jfronny.commons.serialize.generator.gprocessor;
import com.squareup.javapoet.*;
import io.gitlab.jfronny.commons.serialize.generator.Cl;
import io.gitlab.jfronny.commons.serialize.generator.SerializableClass;
import io.gitlab.jfronny.commons.serialize.generator.adapter.Adapters;
import io.gitlab.jfronny.commons.serialize.generator.core.TypeHelper;
import io.gitlab.jfronny.commons.serialize.generator.core.value.*;
import javax.annotation.processing.Messager;
import javax.lang.model.element.Modifier;
import java.io.IOException;
import java.util.List;
import java.util.Set;
public class StaticProcessor extends GProcessor {
public StaticProcessor(ValueCreator valueCreator, Messager message, boolean hasManifold, boolean disableSafe) {
super(valueCreator, message, hasManifold, true, disableSafe);
}
@Override
public ClassName generateDelegatingAdapter(TypeSpec.Builder spec, TypeName classType, ClassName generatedClassName) {
throw new UnsupportedOperationException();
}
// !!!WARNING!!!
// A lot of this code is common between InstanceProcessor and StaticProcessor
// Make sure they don't get out of sync!
// (Or, alternatively, create one common solution for these)
// !!!WARNING!!!
@Override
public void generateSerialisation(TypeSpec.Builder spec, SerializableClass self, List<TypeVariableName> typeVariables, Set<SerializableClass> otherAdapters) throws ElementException {
Value value = valueCreator.fromStatic(self.classElement());
Properties properties = value.getProperties();
// public static void write(JsonWriter writer) throws IOException
{
CodeBlock.Builder code = CodeBlock.builder();
code.addStatement("writer.beginObject()");
for (Property.Field param : properties.fields) {
if (Properties.containsName(properties.getters, param)) continue;
Runnable writeGet = () -> code.add("$T.$N", self.getTypeName(), param.getCallableName());
if (param.getType().getKind().isPrimitive()) {
generateComments(param, code);
code.addStatement("writer.name($S)", getSerializedName(param));
Adapters.generateWrite(param, spec, code, typeVariables, otherAdapters, message, writeGet);
} else {
code.beginControlFlow("if ($T.$N != null || writer.getSerializeNulls())", self.getTypeName(), param.getCallableName());
generateComments(param, code);
code.addStatement("writer.name($S)", getSerializedName(param));
code.addStatement("if ($T.$N == null) writer.nullValue()", self.getTypeName(), param.getCallableName());
code.beginControlFlow("else");
Adapters.generateWrite(param, spec, code, typeVariables, otherAdapters, message, writeGet);
code.endControlFlow();
code.endControlFlow();
}
}
for (Property.Getter param : properties.getters) {
if (param.getType().getKind().isPrimitive()) {
generateComments(param, code);
code.addStatement("writer.name($S)", getSerializedName(param));
Adapters.generateWrite(param, spec, code, typeVariables, otherAdapters, message, () -> code.add("$T.$N()", self.getTypeName(), param.getCallableName()));
} else {
code.addStatement("$T $L$N = $T.$N()", param.getType(), "$", param.getCallableName(), self.getTypeName(), param.getCallableName());
code.beginControlFlow("if ($L$N != null || writer.getSerializeNulls())", "$", param.getCallableName());
generateComments(param, code);
code.addStatement("writer.name($S)", getSerializedName(param));
code.addStatement("if ($L$N == null) writer.nullValue()", "$", param.getCallableName());
code.beginControlFlow("else");
Adapters.generateWrite(param, spec, code, typeVariables, otherAdapters, message, () -> code.add("$L$N", "$", param.getCallableName()));
code.endControlFlow();
code.endControlFlow();
}
}
code.addStatement("writer.endObject()");
spec.addMethod(extension(MethodSpec.methodBuilder("write"))
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(Cl.SERIALIZE_WRITER, "writer")
.addException(IOException.class)
.addCode(code.build())
.build());
}
// public static void read(JsonReader reader) throws IOException
{
CodeBlock.Builder code = CodeBlock.builder();
code.beginControlFlow("if (reader.peek() == $T.NULL)", Cl.GSON_TOKEN)
.addStatement("reader.nextNull()")
.addStatement("return")
.endControlFlow();
boolean isEmpty = true;
for (Property<?> param : properties.names) {
isEmpty = false;
code.addStatement("$T.$N = $L", self.getTypeName(), param.getName(), TypeHelper.getDefaultValue(param.getType()));
}
if (isEmpty) {
code.addStatement("reader.skipValue()");
} else {
code.addStatement("reader.beginObject()")
.beginControlFlow("while (reader.hasNext())")
.beginControlFlow("switch (reader.nextName())");
for (Property<?> param : properties.names) {
if (param.getType().getKind().isPrimitive()) {
code.add("case $S -> $T.$N = ", getSerializedName(param), self.getTypeName(), param.getName());
Adapters.generateRead(param, spec, code, typeVariables, otherAdapters, message);
code.add(";\n");
} else {
code.beginControlFlow("case $S ->", getSerializedName(param))
.beginControlFlow("if (reader.peek() == $T.NULL)", Cl.GSON_TOKEN)
.addStatement("reader.nextNull()")
.addStatement("$T.$N = null", self.getTypeName(), param.getName());
code.unindent().add("} else $T.$N = ", self.getTypeName(), param.getName());
Adapters.generateRead(param, spec, code, typeVariables, otherAdapters, message);
code.add(";\n")
.endControlFlow();
}
}
code.add("default -> ")
.addStatement("reader.skipValue()");
code.endControlFlow()
.endControlFlow()
.addStatement("reader.endObject()");
}
spec.addMethod(extension(MethodSpec.methodBuilder("read"))
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(Cl.SERIALIZE_READER, "reader")
.addException(IOException.class)
.addCode(code.build())
.build());
}
}
}

View File

@ -0,0 +1,9 @@
module io.gitlab.jfronny.commons.serialize.generator {
requires com.squareup.javapoet;
requires java.compiler;
requires io.gitlab.jfronny.commons;
requires static org.jetbrains.annotations;
requires io.gitlab.jfronny.commons.serialize.generator.core;
requires io.gitlab.jfronny.commons.serialize.generator.annotations;
requires io.gitlab.jfronny.commons.serialize.databind.api;
}

View File

@ -0,0 +1 @@
io.gitlab.jfronny.commons.serialize.generator.SerializeGeneratorProcessor

View File

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

View File

@ -1,8 +1,26 @@
package io.gitlab.jfronny.commons.serialize;
import io.gitlab.jfronny.commons.concurrent.ScopedValue;
import io.gitlab.jfronny.commons.concurrent.WithScopedValue;
import org.jetbrains.annotations.ApiStatus;
import java.io.StringReader;
public interface Transport<TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>, Writer extends SerializeWriter<TEx, Writer>> {
public interface Transport<TEx extends Throwable, Reader extends SerializeReader<TEx, Reader>, Writer extends SerializeWriter<TEx, Writer>> extends WithScopedValue<Transport<?, ?, ?>> {
ScopedValue<Transport<?, ?, ?>> DEFAULT = new ScopedValue<>();
@Override
@ApiStatus.Internal
default ScopedValue<Transport<?, ?, ?>> getAttached() {
return DEFAULT;
}
@Override
@ApiStatus.Internal
default Transport<?, ?, ?> self() {
return this;
}
Reader createReader(java.io.Reader source) throws TEx;
default Reader createReader(String source) throws TEx {

View File

@ -2,12 +2,13 @@ package io.gitlab.jfronny.commons.serialize.emulated;
import io.gitlab.jfronny.commons.serialize.SerializeWriter;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class EmulatedWriter extends SerializeWriter<RuntimeException, EmulatedWriter> {
public class EmulatedWriter extends SerializeWriter<RuntimeException, EmulatedWriter> implements Closeable {
/** Added to the top of the stack when this writer is closed to cause following ops to fail. */
private static final DataElement.Primitive.String SENTINEL_CLOSED = new DataElement.Primitive.String("closed");
@ -177,7 +178,7 @@ public class EmulatedWriter extends SerializeWriter<RuntimeException, EmulatedWr
public void flush() throws IOException {}
@Override
public void close() throws Exception {
public void close() throws IOException {
if (!stack.isEmpty()) {
throw new IOException("Incomplete document");
}

View File

@ -1,10 +1,11 @@
package io.gitlab.jfronny.commons.concurrent;
import io.gitlab.jfronny.commons.throwable.Assume;
import org.jetbrains.annotations.ApiStatus;
public interface WithScopedValue<T> {
ScopedValue<T> getAttached();
T self();
@ApiStatus.Internal ScopedValue<T> getAttached();
@ApiStatus.Internal T self();
default <TEx1 extends Throwable, TEx2 extends Throwable> void withContext(Action<TEx1, TEx2> action) throws TEx1, TEx2 {
Assume.<TEx1>reintroduce();

View File

@ -6,6 +6,7 @@ jf-scripts = "1.5-SNAPSHOT"
gson = "2.10.3-SNAPSHOT"
google-truth = "1.4.2"
shadow = "8.1.1"
java-poet = "1.13.0"
[libraries]
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
@ -14,6 +15,7 @@ junit-vintage = { module = "org.junit.vintage:junit-vintage-engine", version.ref
annotations = { module = "org.jetbrains:annotations", version.ref = "annotations" }
gson = { module = "io.gitlab.jfronny:gson", version.ref = "gson" }
google-truth = { module = "com.google.truth:truth", version.ref = "google-truth" }
java-poet = { module = "com.squareup:javapoet", version.ref = "java-poet" }
plugin-kotlin = { module = "org.gradle.kotlin:gradle-kotlin-dsl-plugins", version.ref = "gradle-kotlin-dsl"}
plugin-convention = { module = "io.gitlab.jfronny:convention", version.ref="jf-scripts" }

View File

@ -12,6 +12,10 @@ include("commons-serialize-xml")
include("commons-serialize-databind")
include("commons-serialize-databind-api")
include("commons-serialize-databind-sql")
include("commons-serialize-generator-core")
include("commons-serialize-generator-annotations")
include("commons-serialize-generator")
include("commons-serialize-generator-example")
// new muscript
include("muscript-all")
include("muscript-core")