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> { 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, 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> 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 boolean isStatic; 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, 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 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 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 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 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.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 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); } } public static > N findName(List names, Property property) { return names.stream().filter(n -> n.getName().equals(property.getName())).findFirst().orElse(null); } public 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")); } }