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> { public final List> names; public final List params; public final List fields; public final List getters; public final List constructorParams; public final List 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> names, List params, List fields, List getters, List constructorParams, List 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 METHODS_TO_SKIP = Set.of("hashCode", "toString", "clone"); private final Types types; public final List> names = new ArrayList<>(); public final List params = new ArrayList<>(); public final List fields = new ArrayList<>(); public final List getters = new ArrayList<>(); public final List constructorParams = new ArrayList<>(); public final List 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 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 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 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 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 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>... 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 findName(List names, Property property) { return names.stream().filter(n -> n.getName().equals(property.getName())).findFirst().orElse(null); } private static boolean containsName(List> 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")); } }