package io.gitlab.jfronny.gson.compile.processor.util.valueprocessor; 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> { public final List> names; public final List params; public final List fields; public final List getters; public final List setters; 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.constructionElement.parameters) { builder.addConstructorParam(param); } if (constructionSource instanceof ConstructionSource.Builder csb) { var builderClass = csb.builderClass; for (ExecutableElement method : ElementFilter.methodsIn(builderClass.enclosedElements)) { builder.addBuilderParam(method); } } var targetClass = constructionSource.targetClass; builder.addFieldsAndAccessors(targetClass); return builder.build(); } private Properties(List> names, List params, List fields, List getters, List setters, 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.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 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 setters = new ArrayList<>(); public final List constructorParams = new ArrayList<>(); public final List builderParams = new ArrayList<>(); public Builder(Types types) { this.types = types; } public void addFieldsAndAccessors(TypeElement targetClass) { // accessors for (ExecutableElement method : ElementFilter.methodsIn(targetClass.enclosedElements)) { addGetter(targetClass, method); addSetter(targetClass, method); } // fields for (VariableElement field : ElementFilter.fieldsIn(targetClass.enclosedElements)) { addField(field); } for (TypeMirror superInterface : targetClass.interfaces) { addFieldsAndAccessors((TypeElement) types.asElement(superInterface)); } TypeMirror superclass = targetClass.superclass; if (superclass.kind != TypeKind.NONE && !superclass.toString().equals("java.lang.Object")) { addFieldsAndAccessors((TypeElement) types.asElement(superclass)); } } public void addGetter(TypeElement classElement, ExecutableElement method) { Set modifiers = method.modifiers; if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.STATIC) || method.returnType.kind == TypeKind.VOID || !method.parameters.isEmpty || isMethodToSkip(classElement, method)) { return; } getters.add(new Property.Getter(method)); } public void addSetter(TypeElement classElement, ExecutableElement method) { Set modifiers = method.modifiers; if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.STATIC) || method.returnType.kind != TypeKind.VOID || method.parameters.size() != 1 || isMethodToSkip(classElement, method) || !method.simpleName.toString().startsWith("set")) { return; } setters.add(new Property.Setter(method)); } public void addField(VariableElement field) { Set modifiers = field.modifiers; 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(ExecutableElement method) { if (method.getParameters().size() == 1 && method.simpleName.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 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 modifiers = field.element.modifiers; 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.modifiers.contains(Modifier.TRANSIENT); }); } private void removeSettersForTransientFields() { getters.removeIf(getter -> { Property field = findName(fields, getter); return field != null && field.element.modifiers.contains(Modifier.TRANSIENT); }); } private boolean isMethodToSkip(TypeElement classElement, ExecutableElement method) { String name = method.simpleName.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.annotations.isEmpty) { if (annotations == null) annotations = new ArrayList<>(name.annotations); else { for (AnnotationMirror annotation : name.annotations) { 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); } } public static > N findName(List names, Property property) { return names.stream().filter(n -> n.name.equals(property.name)).findFirst().orElse(null); } public static boolean containsName(List> properties, Property property) { return findName(properties, property) != null; } private static boolean isKotlinClass(TypeElement element) { return element.annotationMirrors.stream().anyMatch(m -> m.annotationType.toString().equals("kotlin.Metadata")); } }