CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-com-google-auto-value--auto-value

Generated immutable value classes for Java 8+ using annotation processing

Pending
Overview
Eval results
Files

extensions.mddocs/

Extension Framework

AutoValue provides an extension framework that allows you to customize the generated code by creating custom AutoValueExtension implementations.

Basic Extension Structure

public class MyExtension extends AutoValueExtension {
  @Override
  public boolean applicable(Context context) {
    // Determine if this extension should apply to the given AutoValue class
    return context.properties().containsKey("specialProperty");
  }
  
  @Override
  public String generateClass(
      Context context, 
      String className, 
      String classToExtend, 
      boolean isFinal) {
    // Generate the extension class code
    return "package " + context.packageName() + ";\n" +
           "public " + (isFinal ? "final" : "abstract") + " class " + className + 
           " extends " + classToExtend + " {\n" +
           "  // Extension implementation\n" +
           "}";
  }
}

Extension Registration

Register extensions using the ServiceLoader mechanism by creating: META-INF/services/com.google.auto.value.extension.AutoValueExtension

com.example.MyExtension
com.example.AnotherExtension

Context Interface

The Context provides access to AutoValue class metadata:

public interface Context {
  ProcessingEnvironment processingEnvironment();
  String packageName();
  TypeElement autoValueClass();
  String finalAutoValueClassName();
  Map<String, ExecutableElement> properties();
  Map<String, TypeMirror> propertyTypes();
  Set<ExecutableElement> abstractMethods();
  Set<ExecutableElement> builderAbstractMethods();
  List<AnnotationMirror> classAnnotationsToCopy(TypeElement classToCopyFrom);
  List<AnnotationMirror> methodAnnotationsToCopy(ExecutableElement method);
  Optional<BuilderContext> builder();
}

Simple Extension Example

Create an extension that adds validation methods:

public class ValidationExtension extends AutoValueExtension {
  @Override
  public boolean applicable(Context context) {
    // Apply to classes with @Validated annotation
    return context.autoValueClass()
        .getAnnotationMirrors()
        .stream()
        .anyMatch(mirror -> mirror.getAnnotationType().toString().endsWith("Validated"));
  }
  
  @Override
  public String generateClass(
      Context context, 
      String className, 
      String classToExtend, 
      boolean isFinal) {
    
    StringBuilder code = new StringBuilder();
    code.append("package ").append(context.packageName()).append(";\n\n");
    code.append("public ").append(isFinal ? "final" : "abstract")
        .append(" class ").append(className)
        .append(" extends ").append(classToExtend).append(" {\n");
    
    // Constructor
    code.append("  ").append(className).append("(");
    boolean first = true;
    for (Map.Entry<String, TypeMirror> property : context.propertyTypes().entrySet()) {
      if (!first) code.append(", ");
      code.append(property.getValue().toString()).append(" ").append(property.getKey());
      first = false;
    }
    code.append(") {\n    super(");
    
    first = true;
    for (String propertyName : context.properties().keySet()) {
      if (!first) code.append(", ");
      code.append(propertyName);
      first = false;
    }
    code.append(");\n");
    
    // Add validation
    for (Map.Entry<String, TypeMirror> property : context.propertyTypes().entrySet()) {
      if (property.getValue().toString().equals("java.lang.String")) {
        code.append("    if (").append(property.getKey()).append(".isEmpty()) {\n");
        code.append("      throw new IllegalArgumentException(\"")
            .append(property.getKey()).append(" cannot be empty\");\n");
        code.append("    }\n");
      }
    }
    
    code.append("  }\n");
    code.append("}\n");
    
    return code.toString();
  }
}

Usage:

@Validated
@AutoValue
public abstract class Person {
  public abstract String name();
  public abstract String email();
  
  public static Person create(String name, String email) {
    return new AutoValue_Person(name, email);
  }
}

// The extension will add validation to the constructor
Person person = Person.create("", "test@example.com"); // Throws IllegalArgumentException

Consuming Properties and Methods

Extensions can consume properties to exclude them from the default implementation:

public class TimestampExtension extends AutoValueExtension {
  @Override
  public boolean applicable(Context context) {
    return context.properties().containsKey("timestamp");
  }
  
  @Override
  public Set<String> consumeProperties(Context context) {
    // Consume the timestamp property - AutoValue won't include it in equals/hashCode
    return ImmutableSet.of("timestamp");
  }
  
  @Override
  public String generateClass(
      Context context, 
      String className, 
      String classToExtend, 
      boolean isFinal) {
    
    return "package " + context.packageName() + ";\n" +
           "public " + (isFinal ? "final" : "abstract") + " class " + className + 
           " extends " + classToExtend + " {\n" +
           "  private final long timestamp;\n" +
           "  \n" +
           "  " + className + "(/* constructor parameters */) {\n" +
           "    super(/* super parameters */);\n" +
           "    this.timestamp = System.currentTimeMillis();\n" +
           "  }\n" +
           "  \n" +
           "  @Override\n" +
           "  public long timestamp() {\n" +
           "    return timestamp;\n" +
           "  }\n" +
           "}";
  }
}

Builder Extension

Extensions can also modify builder behavior:

public class DefaultsExtension extends AutoValueExtension {
  @Override
  public boolean applicable(Context context) {
    return context.builder().isPresent();
  }
  
  @Override
  public String generateClass(
      Context context, 
      String className, 
      String classToExtend, 
      boolean isFinal) {
    
    Optional<BuilderContext> builderContext = context.builder();
    if (!builderContext.isPresent()) {
      return null; // No builder, no extension needed
    }
    
    StringBuilder code = new StringBuilder();
    code.append("package ").append(context.packageName()).append(";\n\n");
    code.append("public ").append(isFinal ? "final" : "abstract")
        .append(" class ").append(className)
        .append(" extends ").append(classToExtend).append(" {\n");
    
    // Add builder with smart defaults
    code.append("  public static class Builder extends ")
        .append(classToExtend).append(".Builder {\n");
    code.append("    public Builder() {\n");
    
    // Set intelligent defaults based on property types
    for (Map.Entry<String, TypeMirror> property : context.propertyTypes().entrySet()) {
      String propertyType = property.getValue().toString();
      String propertyName = property.getKey();
      
      if (propertyType.equals("java.lang.String") && propertyName.equals("id")) {
        code.append("      ").append(propertyName).append("(java.util.UUID.randomUUID().toString());\n");
      } else if (propertyType.startsWith("java.time.") && propertyName.contains("timestamp")) {
        code.append("      ").append(propertyName).append("(java.time.Instant.now());\n");
      }
    }
    
    code.append("    }\n");
    code.append("  }\n");
    code.append("}\n");
    
    return code.toString();
  }
}

Method Consumption

Extensions can consume abstract methods to provide custom implementations:

public class JsonExtension extends AutoValueExtension {
  @Override
  public boolean applicable(Context context) {
    return context.abstractMethods().stream()
        .anyMatch(method -> method.getSimpleName().toString().equals("toJson"));
  }
  
  @Override
  public Set<ExecutableElement> consumeMethods(Context context) {
    return context.abstractMethods().stream()
        .filter(method -> method.getSimpleName().toString().equals("toJson"))
        .collect(Collectors.toSet());
  }
  
  @Override
  public String generateClass(
      Context context, 
      String className, 
      String classToExtend, 
      boolean isFinal) {
    
    StringBuilder code = new StringBuilder();
    code.append("package ").append(context.packageName()).append(";\n\n");
    code.append("public ").append(isFinal ? "final" : "abstract")
        .append(" class ").append(className)
        .append(" extends ").append(classToExtend).append(" {\n");
    
    // Constructor
    code.append("  ").append(className).append("(");
    // ... constructor parameters
    code.append(") {\n    super(");
    // ... super call
    code.append(");\n  }\n");
    
    // Generate toJson() method
    code.append("  @Override\n");
    code.append("  public String toJson() {\n");
    code.append("    StringBuilder json = new StringBuilder();\n");
    code.append("    json.append(\"{\");\n");
    
    boolean first = true;
    for (String propertyName : context.properties().keySet()) {
      if (!first) {
        code.append("    json.append(\",\");\n");
      }
      code.append("    json.append(\"\\\"\").append(\"").append(propertyName).append("\")");
      code.append(".append(\"\\\":\\\"\").append(").append(propertyName).append("())");
      code.append(".append(\"\\\"\");\n");
      first = false;
    }
    
    code.append("    json.append(\"}\");\n");
    code.append("    return json.toString();\n");
    code.append("  }\n");
    code.append("}\n");
    
    return code.toString();
  }
}

Incremental Processing Support

Extensions can support incremental compilation:

public class MyExtension extends AutoValueExtension {
  @Override
  public IncrementalExtensionType incrementalType(ProcessingEnvironment processingEnvironment) {
    return IncrementalExtensionType.ISOLATING; // or AGGREGATING, UNKNOWN
  }
  
  @Override
  public Set<String> getSupportedOptions() {
    return ImmutableSet.of("myExtension.option1", "myExtension.option2");
  }
  
  // ... other methods
}

Extension Ordering

Extensions are applied in the order they appear on the classpath. If you need specific ordering:

public class FinalExtension extends AutoValueExtension {
  @Override
  public boolean mustBeFinal(Context context) {
    return true; // This extension must be the final class in the hierarchy
  }
  
  // ... other methods
}

Only one extension can return true from mustBeFinal().

Code Generation Utilities

Use JavaPoet for sophisticated code generation:

public class JavaPoetExtension extends AutoValueExtension {
  @Override
  public String generateClass(
      Context context, 
      String className, 
      String classToExtend, 
      boolean isFinal) {
    
    TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className)
        .superclass(ClassName.bestGuess(classToExtend))
        .addModifiers(isFinal ? Modifier.FINAL : Modifier.ABSTRACT);
    
    // Add constructor
    MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder();
    CodeBlock.Builder superCallBuilder = CodeBlock.builder().add("super(");
    
    boolean first = true;
    for (Map.Entry<String, TypeMirror> property : context.propertyTypes().entrySet()) {
      TypeName typeName = TypeName.get(property.getValue());
      String paramName = property.getKey();
      
      constructorBuilder.addParameter(typeName, paramName);
      if (!first) superCallBuilder.add(", ");
      superCallBuilder.add("$N", paramName);
      first = false;
    }
    
    superCallBuilder.add(")");
    constructorBuilder.addStatement(superCallBuilder.build());
    classBuilder.addMethod(constructorBuilder.build());
    
    TypeSpec generatedClass = classBuilder.build();
    
    JavaFile javaFile = JavaFile.builder(context.packageName(), generatedClass)
        .build();
    
    return javaFile.toString();
  }
}

Testing Extensions

Test extensions using compile-testing:

@Test
public void testMyExtension() {
  JavaFileObject autoValueClass = JavaFileObjects.forSourceString("test.Test",
      "package test;",
      "",
      "import com.google.auto.value.AutoValue;",
      "",
      "@AutoValue",
      "abstract class Test {",
      "  abstract String value();",
      "}");
  
  Compilation compilation = Compiler.javac()
      .withProcessors(new AutoValueProcessor())
      .withClasspath(/* extension classpath */)
      .compile(autoValueClass);
  
  assertThat(compilation).succeeded();
  assertThat(compilation).generatedSourceFile("test.AutoValue_Test")
      .contentsAsUtf8String()
      .contains("// Expected extension code");
}

Built-in Extensions

AutoValue includes several built-in extensions:

  • MemoizeExtension: Implements @Memoized functionality
  • SerializableAutoValueExtension: Handles @SerializableAutoValue
  • ToPrettyStringExtension: Implements @ToPrettyString

These serve as excellent examples for creating custom extensions.

Install with Tessl CLI

npx tessl i tessl/maven-com-google-auto-value--auto-value

docs

annotation-generation.md

builders.md

extensions.md

index.md

memoization.md

pretty-strings.md

serialization.md

standalone-builders.md

tagged-unions.md

value-classes.md

tile.json