gson-compile/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/Properties.java

250 lines
10 KiB
Java

package io.gitlab.jfronny.gson.compile.processor.util.valueprocessor;
import io.gitlab.jfronny.gson.compile.processor.util.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.*;
public class Properties extends DelegateList<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.ConstructorParam> constructorParams;
public final List<Property.BuilderParam> builderParams;
public static Properties build(Types types, ConstructionSource constructionSource) throws ElementException {
Builder builder = new Builder(types);
// constructor params
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(builderClass.asType(), method);
}
}
var targetClass = constructionSource.getTargetClass();
builder.addFieldsAndGetters(targetClass);
return builder.build();
}
private Properties(List<Property<?>> names,
List<Property.Param> params,
List<Property.Field> fields,
List<Property.Getter> getters,
List<Property.ConstructorParam> constructorParams,
List<Property.BuilderParam> builderParams) {
super(names);
this.names = Objects.requireNonNull(names);
this.params = Objects.requireNonNull(params);
this.fields = Objects.requireNonNull(fields);
this.getters = Objects.requireNonNull(getters);
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 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.ConstructorParam> constructorParams = new ArrayList<>();
public final List<Property.BuilderParam> builderParams = new ArrayList<>();
public Builder(Types types) {
this.types = types;
}
public void addFieldsAndGetters(TypeElement targetClass) {
// getters
for (ExecutableElement method : ElementFilter.methodsIn(targetClass.getEnclosedElements())) {
addGetter(targetClass, method);
}
// fields
for (VariableElement field : ElementFilter.fieldsIn(targetClass.getEnclosedElements())) {
addField(field);
}
for (TypeMirror superInterface : targetClass.getInterfaces()) {
addFieldsAndGetters((TypeElement) types.asElement(superInterface));
}
TypeMirror superclass = targetClass.getSuperclass();
if (superclass.getKind() != TypeKind.NONE && !superclass.toString().equals("java.lang.Object")) {
addFieldsAndGetters((TypeElement) types.asElement(superclass));
}
}
public void addGetter(TypeElement classElement, ExecutableElement method) {
Set<Modifier> modifiers = method.getModifiers();
if (modifiers.contains(Modifier.PRIVATE)
|| modifiers.contains(Modifier.STATIC)
|| method.getReturnType().getKind() == TypeKind.VOID
|| !method.getParameters().isEmpty()
|| isMethodToSkip(classElement, method)) {
return;
}
getters.add(new Property.Getter(method));
}
public void addField(VariableElement field) {
Set<Modifier> modifiers = field.getModifiers();
if (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(TypeMirror builderType, ExecutableElement method) {
if (method.getReturnType().equals(builderType) && method.getParameters().size() == 1) {
Property.BuilderParam prop = new Property.BuilderParam(method);
builderParams.add(prop);
params.add(prop);
}
}
public Properties build() throws ElementException {
stripBeans(getters);
removeExtraBuilders();
removeGettersForTransientFields();
mergeSerializeNames(params, fields, getters);
removeExtraFields();
names.addAll(params);
fields.stream().filter(f -> !containsName(names, f)).forEach(names::add);
getters.stream().filter(f -> !containsName(names, f)).forEach(names::add);
return new Properties(names, params, fields, getters, 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.BuilderParam builderParam = builderParams.get(i);
if (containsName(constructorParams, builderParam)) {
builderParams.remove(i);
params.remove(builderParam);
}
}
}
private void removeExtraFields() {
for (int i = fields.size() - 1; i >= 0; i--) {
Property.Field field = fields.get(i);
Set<Modifier> modifiers = field.element.getModifiers();
if (modifiers.contains(Modifier.PRIVATE)
|| modifiers.contains(Modifier.TRANSIENT)
|| containsName(getters, field)) {
fields.remove(i);
}
}
}
private void removeGettersForTransientFields() {
for (int i = getters.size() - 1; i >= 0; i--) {
Property.Getter getter = getters.get(i);
Property<?> field = findName(fields, getter);
if (field != null && field.element.getModifiers().contains(Modifier.TRANSIENT)) {
getters.remove(i);
}
}
}
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);
}
}
private static <N extends Property<?>> N findName(List<N> names, Property<?> property) {
return names.stream().filter(n -> n.getName().equals(property.getName())).findFirst().orElse(null);
}
private 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"));
}
}