gson-compile/gson-compile-processor-core/src/main/java/io/gitlab/jfronny/gson/compile/processor/core/value/Properties.java

279 lines
11 KiB
Java

package io.gitlab.jfronny.gson.compile.processor.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.*;
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"));
}
}