Skip to content

Latest commit

 

History

History
657 lines (517 loc) · 18 KB

File metadata and controls

657 lines (517 loc) · 18 KB

Overview

The azure-autorest-customization package provides APIs for customizing Autorest code generation safely and programmatically to support special cases not supported by Autorest code generation directly using Eclipse language server to ensure valid Java code.

Before You Customize

Before customizing generated code, consider whether your change should be made in TypeSpec (client.tsp) instead. TypeSpec customizations are cleaner and survive regeneration. See the TypeSpec Client Customizations Reference for available decorators like @@clientName, @@access, etc.

Use Java code customizations only when TypeSpec cannot express the behavior you need.

To set up customizations, create a Maven project with dependency:

<dependency>
  <groupId>com.azure.tools</groupId>
  <artifactId>azure-autorest-customization</artifactId>
  <version>1.0.0-beta.11</version>
</dependency>

Create a customization class extending from com.azure.autorest.customization.Customization and override the void customize(LibraryCustomization, Logger) method. You will have access to a LibraryCustomization class where you will be able to customize the generated Java code before it's written to the disk. Currently, the following customizations are supported:

Navigate through the packages and classes

There are three primary customization classes currently available, LibraryCustomization, PackageCustomization, and ClassCustomization. From a given LibraryCustomization, you can navigate through the packages and classes intuitively.

Change class modifier

A class Foo

public class Foo {
}

with customization

@Override
public void customize(LibraryCustomization customization, Logger logger) {
    // Calling setModifiers without passing anything is equivalent to package-private.
    customization.getClass("com.azure.myservice.models", "Foo").customizeAst(ast -> ast.getClassByName("Foo")
        .ifPresent(ClassOrInterfaceDeclaration::setModifiers));
}

will generate

class Foo {
}

Change method modifier

A method getBar in the Foo class

public class Foo {
    private Bar bar;

    public Bar getBar() {
        return this.bar;
    }
}

with customization

@Override
public void customize(LibraryCustomization customization, Logger logger) {
    customization.getClass("com.azure.myservice.models", "Foo").customizeAst(ast -> ast.getClassByName("Foo")
        .ifPresent(clazz -> clazz.getMethodsByName("getBar")
            .forEach(method -> method.setModifiers(Modifier.Keyword.PRIVATE))));
}

will generate

public class Foo {
    private Bar bar;

    private Bar getBar() {
        return this.bar;
    }
}

Change method return type

You can change a method's return type, and pass a String formatter to transform the original return value statement. If the original return type is void, simply pass the full return value String expression in place of the String formatter; if the new return type is void, simply pass null.

A method getId in the Foo class

public class Foo {
    private Bar bar;

    public Bar getBar() {
        return this.bar;
    }
}

with customization

@Override
public void customize(LibraryCustomization customization, Logger logger) {
    customization.getClass("com.azure.myservice.models", "Foo").customizeAst(ast -> {
        ast.addImport(UUID.class);
        ast.getClassByName("Foo").ifPresent(clazz -> clazz.getMethodsByName("getId").forEach(method -> {
            // change return type to UUID
            method.setType("UUID");
            method.setBody(StaticJavaParser.parseBlock("{ return UUID.fromString(this.id); }"));
        }));
    });
}

will generate

public class Foo {
    private String id;

    public UUID getId() {
        return UUID.fromString(this.id);
    }
}

The UUID class will be automatically imported.

Change class super type

A class Foo extends Bar

public class Bar {
}
public class Foo extends Bar {
}

with customization

@Override
public void customize(LibraryCustomization customization, Logger logger) {
    customization.getClass("com.azure.myservice.models", "Foo")
        .customizeAst(ast -> ast.getClassByName("foo").ifPresent(clazz -> {
            String newTypeFullName = "com.azure.myservice.models.Bar1";
            ast.addImport(newTypeFullName);
            clazz.getExtendedTypes().clear();
            clazz.addExtendedType(new ClassOrInterfaceType(null, "Bar1"));
        }));
}

will generate

public class Foo extends Bar1 {
}

Add an annotation to a class

A class Foo

public class Foo {
}

with customization

@Override
public void customize(LibraryCustomization customization, Logger logger) {
    customization.getClass("com.azure.myservice.models", "Foo").customizeAst(ast -> ast.getClassByName("Foo")
        .ifPresent(clazz -> clazz.addMarkerAnnotation("Deprecated")));
}

will generate

@Deprecated
public class Foo {
}

The JsonClassDescription class will be automatically imported.

Add an annotation to a method

A method getBar in the Foo class

public class Foo {
    private Bar bar;

    public Bar getBar() {
        return this.bar;
    }
}

with customization

@Override
public void customize(LibraryCustomization customization, Logger logger) {
    customization.getClass("com.azure.myservice.models", "Foo").customizeAst(ast -> ast.getClassByName("Foo")
        .ifPresent(clazz -> clazz.getMethodsByName("getBar")
            .forEach(method -> method.addMarkerAnnotation("Deprecated"))));
}

will generate

public class Foo {
    private Bar bar;

    @Deprecated
    public Bar getBar() {
        return this.bar;
    }
}

The Deprecated class will be automatically imported.

Add a field default value

A class Foo

public class Foo {
    private String bar;
}

with customization

@Override
public void customize(LibraryCustomization customization, Logger logger) {
    customization.getClass("com.azure.myservice.models", "Foo")
        .customizeAst(ast -> ast.getClassByName("Foo")
            .flatMap(clazz -> clazz.getFieldByName("bar"))
            .ifPresent(barField ->
                barField.getVariables().forEach(var -> {
                    if (var.getNameAsString().equals("bar")) {
                        var.setInitializer("\"bar\"");
                    }
                })));
}

will generate

public class Foo {
    private String bar = "bar";
}

Remove an annotation from a class

A class Foo

@Deprecated
public class Foo {
}

with customization

@Override
public void customize(LibraryCustomization customization, Logger logger) {
    customization.getClass("com.azure.myservice.models", "Foo").customizeAst(ast -> ast.getClassByName("Foo")
        .flatMap(clazz -> clazz.getAnnotationByName("Deprecated"))
        .ifPresent(Node::remove));
}

will generate

public class Foo {
}

Refactor: Generate the getter and setter methods for a property

A property active in the Foo class

public class Foo {
    private boolean active;
}

with customization

@Override
public void customize(LibraryCustomization customization, Logger logger) {
    customization.getClass("com.azure.myservice.models", "Foo").customizeAst(ast -> ast.getClassByName("Foo")
        .ifPresent(clazz -> {
            clazz.addMethod("isActive", Modifier.Keyword.PUBLIC)
                .setType("boolean")
                .setBody(StaticJavaParser.parseBlock("{ return this.active; }"));
            clazz.addMethod("setActive", Modifier.Keyword.PUBLIC)
                .setType("Foo")
                .addParameter("boolean", "active")
                .setBody(StaticJavaParser.parseBlock("{ this.active = active; return this; }"));
        }));
}

will generate

public class Foo {
    private boolean active;

    public boolean isActive() {
        return this.active;
    }

    public Foo setActive(boolean active) {
        this.active = active;
        return this;
    }
}

Refactor: Rename an enum member name

An enum member JPG in an enum class ImageFileType:

public enum ImageFileType {
    GIF("gif"),
    JPG("jpg"),
    TIFF("tiff"),
    PNG("png");

    private final String value;

    ImageFileType(String value) {
        this.value = value;
    }
}

with customization

@Override
public void customize(LibraryCustomization customization, Logger logger) {
    customization.getClass("com.azure.myservice.models", "Foo").customizeAst(ast -> ast.getEnumByName("Foo")
        .ifPresent(clazz -> clazz.getEntries().stream()
            .filter(entry -> "JPG".equals(entry.getName().getIdentifier()))
            .forEach(entry -> entry.setName("JPEG"))));
}

will generate

public enum ImageFileType {
    GIF("gif"),
    JPEG("jpg"),
    TIFF("tiff"),
    PNG("png");

    private final String value;

    ImageFileType(String value) {
        this.value = value;
    }
}

This customization is guaranteed to not break the build.

Javadoc: Set the description for a class / method

A class Foo

/** Class Foo. */
public class Foo {
    private boolean active;

    public boolean isActive() {
        return this.active;
    }

    public Foo setActive(boolean active) {
        this.active = active;
        return this;
    }
}

with customization

@Override
public void customize(LibraryCustomization customization, Logger logger) {
    customization.getClass("com.azure.myservice.models", "Foo").customizeAst(ast -> ast.getClassByName("Foo")
        .ifPresent(clazz -> {
            clazz.setJavadocComment("A Foo object stored in Azure.");
            clazz.getMethodsByName("setActive")
                .forEach(method -> method.setJavadocComment("Set the active value."));
        }));
}

will generate

/**
 * A Foo object stored in Azure.
 */
public class Foo {
    private boolean active;

    public boolean isActive() {
        return this.active;
    }

    /**
     * Set the active value.
     */
    public Foo setActive(boolean active) {
        this.active = active;
        return this;
    }
}

Javadoc: Set / remove a parameter's javadoc on a method

A class Foo

public class Foo {
    private boolean active;

    public boolean isActive() {
        return this.active;
    }

    /**
     * Set the active value.
     */
    public Foo setActive(boolean active) {
        this.active = active;
        return this;
    }
}

with customization

@Override
public void customize(LibraryCustomization customization, Logger logger) {
    customization.getClass("com.azure.myservice.models", "Foo").customizeAst(ast -> ast.getClassByName("Foo")
        .ifPresent(clazz -> clazz.getMethodsByName("setActive").forEach(method -> method.getJavadoc()
            .ifPresent(javadoc -> method.setJavadocComment(javadoc.addBlockTag("param", "active",
                "if the foo object is in active state"))))));
}

will generate

public class Foo {
    private boolean active;

    public boolean isActive() {
        return this.active;
    }

    /**
     * Set the active value.
     *
     * @param active if the foo object is in active state
     */
    public Foo setActive(boolean active) {
        this.active = active;
        return this;
    }
}

Javadoc: Set the return javadoc on a method

A Foo class

public class Foo {
    private boolean active;

    public boolean isActive() {
        return this.active;
    }

    /**
     * Set the active value.
     *
     * @param active if the foo object is in active state
     */
    public Foo setActive(boolean active) {
        this.active = active;
        return this;
    }
}

with customization

@Override
public void customize(LibraryCustomization customization, Logger logger) {
    customization.getClass("com.azure.myservice.models", "Foo").customizeAst(ast -> ast.getClassByName("Foo")
        .ifPresent(clazz -> clazz.getMethodsByName("setActive").forEach(method -> method.getJavadoc()
            .ifPresent(javadoc -> method.setJavadocComment(javadoc.addBlockTag("return",
                "the current foo object"))))));
}

will generate

public class Foo {
    private boolean active;

    public boolean isActive() {
        return this.active;
    }

    /**
     * Set the active value.
     *
     * @param active if the foo object is in active state
     * @return the current foo object
     */
    public Foo setActive(boolean active) {
        this.active = active;
        return this;
    }
}

Javadoc: Add / remove an exception's javadoc on a method

A FooClient class

public class FooClient {
    /**
     * Create a Foo object.
     *
     * @param foo the foo object to create in Azure
     * @return the response for creating the foo object
     */
    public CreateFooResponse createFoo(Foo foo) {
        /* REST call to create foo */
        return null;
    }
}

with customization

@Override
public void customize(LibraryCustomization customization, Logger logger) {
    customization.getClass("com.azure.myservice.models", "Foo").customizeAst(ast -> ast.getClassByName("Foo")
        .ifPresent(clazz -> clazz.getMethodsByName("setActive").forEach(method -> method.getJavadoc()
            .ifPresent(javadoc -> method.setJavadocComment(javadoc.addBlockTag("throws", "RuntimeException",
                "An unsuccessful response is received"))))));
}

will generate

/**
 * Create a Foo object.
 *
 * @param foo the foo object to create in Azure
 * @return the response for creating the foo object
 * @throws RuntimeException An unsuccessful response is received
 */
public CreateFooResponse createFoo(Foo foo) {
    /* REST call to create foo */
    return null;
}

Troubleshooting

AutoRest fails with "Unable to format output file"

The cause is that the customized Java code has syntax error.

Possible root cause:

  • Bug in customization code.
  • Bug in the customization-base package.

Steps to diagnose and fix:

  1. Add skip-formatting flag to skip Java code formatting, and hence not checking the syntax error.
  2. Compile or inspect the generated code, find the error.
  3. Determine the root cause. If it is caused by bug in customization code, fix it. If it is caused by bug in customization-base package, report it in GitHub issues.
  4. Remove skip-formatting flag.

Developer note

azure-autorest-customization passes all generated code files to an Eclipse language server process to enable richer, IDE-like functionality. For this to work correctly all required dependencies must be passed to the language server using a dummy pom.xml, which can be found in src/main/resources. If you make changes to code generation and begin seeing code customizations failing it may be that the pom.xml is missing required dependencies and is causing the language server to change processing from IDE-like to a regex, or find and replace, style.