Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Auto generate replace methods

I am running in to a lot of boilerplate code when creating language files for the application I am making. I currently have a class with all the language strings in it and then I use reflection to write these strings to the file.

What I run into quite often is that I have certain placeholders in my strings that I want to replace, for an example I might have a String like this:

public static String USER_INFO = "Username: %name% money: %balance%";

What I would like to achieve is to generate a few methods based on Annotations like I can generate getters/setters and other methods with lombok. Based on the above string I would have an annotation called Arguments(Properly should have been named Replacers or something more meaningfull) like seen here:

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface Arguments {
  String[] value();
}

What I would like to do is to add the annotation like this:

@Arguments(
        value = {"%balance%", "%name%"}
)
public static String USER_INFO = "Username: %name% - money: %balance%";

and get the following replacement methods auto generated:

public static String USER_INFONameReplacement(String name) {
    return USER_INFO.replace("%name%", name);
}
public static String USER_INFOAllReplacement(String name, String balance) {
    return USER_INFO.replace("%name%", name).replace("%balance%", balance);
}
public static String USER_INFOBalanceReplacement(String balance) {
    return USER_INFO.replace("%balance%", balance);
}

After doing some searching I ended up trying to implement AbstractProcessor in a class like this:

@SupportedAnnotationTypes(
    {"io.github.freakyville.configHelper.annotations.Arguments"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class SuggestProcessor extends AbstractProcessor {

@Override
public synchronized void init(ProcessingEnvironment env) {
}

@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) {
    for (TypeElement annoation : annoations) {
        Set<? extends Element> annotatedElements = env.getElementsAnnotatedWith(annoation);
        Map<Boolean, List<Element>> annotatedFields = annotatedElements.stream().collect(
                Collectors.partitioningBy(element ->
                        ((ArrayType) element.asType()).getComponentType().getClass().equals(PrimitiveType.class)));
        List<Element> setters = annotatedFields.get(true);
        if (setters.isEmpty()) {
            continue;
        }
        String className = ((TypeElement) setters.get(0)
                .getEnclosingElement()).getQualifiedName().toString();

        Map<String, List<String>> setterMap = setters.stream().collect(Collectors.toMap(
                setter -> setter.getSimpleName().toString(),
                setter -> Arrays.asList(setter.getAnnotation(Arguments.class).value()))
        );
        try {
            writeBuilderFile(className, setterMap);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return true;
}

private void writeBuilderFile(
        String className, Map<String, List<String>> setterMap)
        throws IOException {

    String packageName = null;
    int lastDot = className.lastIndexOf('.');
    if (lastDot > 0) {
        packageName = className.substring(0, lastDot);
    }

    String builderSimpleClassName = className
            .substring(lastDot + 1);

    JavaFileObject builderFile = processingEnv.getFiler()
            .createSourceFile(className);

    try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {

        if (packageName != null) {
            out.print("package ");
            out.print(packageName);
            out.println(";");
            out.println();
        }

        out.print("public class ");
        out.print(builderSimpleClassName);
        out.println(" {");
        out.println();

        setterMap.forEach((key, orgArgNames) -> {

            for (int i = 0; i < orgArgNames.size(); i++) {
                List<String> subList = orgArgNames.subList(0, i + 1);
                List<String> argNames = subList.stream().map(v -> v.replace("%", "") + "Replacement").collect(Collectors.toList());
                List<String> argsWithTypes = argNames.stream().map(v -> "String " + v).collect(Collectors.toList());
                String argumentList = "(" + String.join("", argsWithTypes).substring(0, argsWithTypes.size() - 3) + ")";
                String methodName;

                if (orgArgNames.size() <= 1) {
                    methodName = key + "Replace" + subList.stream().map(v -> v.replace("%", "")).collect(Collectors.joining(""));
                } else {
                    methodName = key + "Replace" + subList.stream().map(v -> v.replace("%", "").substring(0, 1).toUpperCase() + v.substring(1)).collect(Collectors.joining(""));
                }

                out.print("    public static ");
                out.print(methodName);
                out.print(argumentList);
                out.println("{");
                StringBuilder replaceSB = new StringBuilder();
                replaceSB.append(key);
                for (int i1 = 0; i1 < subList.size(); i1++) {
                    replaceSB
                            .append(".replace(")
                            .append("\"")
                            .append(subList.get(i))
                            .append("\"")
                            .append(",")
                            .append(argNames.get(i))
                            .append(")");

                }
                String replace = replaceSB.toString();
                out.println("return " + replace + ";");
                out.println("}");
                out.println("");
            }
        });

        out.println("}");
    }
}


}

But I can't seem to get it to register it?

So my first question is, is AbstractProcessor the way to go if I want to achieve this? If not how then? if yes, then why is this not registering? I am using IntelliJ and went into settings -> build-> compiler and changed Annotation Processors to enabled and set the processor path to my SuggestProcessor

like image 547
Sumsar1812 Avatar asked Jul 29 '19 23:07

Sumsar1812


1 Answers

Java Annotation Processing (APT) plugins are intended for generating code based on other classes. These classes end up in a generated sources folder which is then later compiled as well. These APT plugins are discovered from the classpath / build tool configuration and ran by the IntelliJ compiler as well. Keep in mind: APT is ment to be for generated source code generation, and not at all for replacing existing classes. The only reason why Lombok is still able to do so is because they hack their way very deep into the compiler and are by that means able to manipulate the AST of classes under compilation.

Because this approach is largely controversial and error-prone with future versions of Java, it is highly unlikely that anyone will ever even attempt at building a APT-based class replacement framework or an extension of Lombok that is able to do this (weren't it for the fact that Lombok is the only tool that could be considered a "framework" for this type of APT usage and Lombok itself is not at all build in an extendible manner).

In conclusion: APT is probably the way to go, but your processor will have to create a new class rather than trying to modify an existing one.

An example of how the annotation processor should be created you can look at the following repository: https://github.com/galberola/java-apt-simple-example

I'm not sure why your current annotation processor is not associated correctly with your compiler. If you're using Maven, you could try to install the artifact for your processor locally and add it as a compile dependency to your other project. Don't forget to register the class as annotation processor with your compiler too, the example project that I referenced does this here: https://github.com/galberola/java-apt-simple-example/blob/master/example/pom.xml#L29-L31 . The same configuration can be applied to other build systems too.

There is no real way in Java of modifying classes under compilation, so if you really must have the method in the same class then this, unfortunately, means that it cannot be done.

like image 110
Jan-Willem Gmelig Meyling Avatar answered Sep 28 '22 04:09

Jan-Willem Gmelig Meyling