Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/contributor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ If you are a **consumer** of the SDK looking for usage guidance, start at the [U
| [Versioning](https://github.com/Azure/azure-sdk-for-java/blob/main/docs/contributor/versioning.md) | `version_client.txt`, dependency tags, incrementing versions |
| [Adding a Module](https://github.com/Azure/azure-sdk-for-java/blob/main/docs/contributor/adding-a-module.md) | Create a new SDK module: dir structure, POM, versioning, CODEOWNERS |
| [TypeSpec Quickstart](https://github.com/Azure/azure-sdk-for-java/blob/main/docs/contributor/typespec-quickstart.md) | End-to-end workflow: generate → build → test → release |
| [TypeSpec Java Customization](https://github.com/Azure/azure-sdk-for-java/blob/main/docs/contributor/typespec-java-customization.md) | Customize generated Java code when TypeSpec cannot express the required behavior |
| [Writing Performance Tests](https://github.com/Azure/azure-sdk-for-java/blob/main/docs/contributor/performance-tests.md) | Set up and run `perf-test-core` benchmarks |
| [JavaDoc & Code Snippets](https://github.com/Azure/azure-sdk-for-java/blob/main/docs/contributor/javadocs.md) | Javadoc standards and codesnippet-maven-plugin workflow |
| [Access Helpers](https://github.com/Azure/azure-sdk-for-java/blob/main/docs/contributor/access-helpers.md) | Cross-package internal access without public APIs |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ please check whether it causes failure, and fix them before apiview.
> - *generated code*: produced by the generator; API surface may change when the spec or generator changes, which can break compilation.
> - *customization code*: maintained by SDK developers; commonly wired via `customization-class` and preserved during regeneration with `partial-update: true`.
>
> Reference: autorest.java customization-base: https://github.com/Azure/autorest.java/tree/main/customization-base
> Reference: [TypeSpec Java Customization](https://github.com/Azure/azure-sdk-for-java/blob/main/docs/contributor/typespec-java-customization.md)

**Prerequisites for customization:** When adding customization code, two options must be set in `tspconfig.yaml`:
```yaml
Expand Down
165 changes: 165 additions & 0 deletions docs/contributor/typespec-java-customization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# TypeSpec Java Customization

The `azure-autorest-customization` package provides APIs to safely customize generated Java code for cases that
TypeSpec Java cannot represent directly. Customizations use JavaParser ASTs through `ClassCustomization.customizeAst`.

## Before you customize

First consider whether the change belongs in TypeSpec (`client.tsp`). TypeSpec customizations are cleaner and persist
through regeneration. See the [TypeSpec Client Customizations Reference](https://github.com/Azure/azure-sdk-for-java/blob/main/eng/common/knowledge/customizing-client-tsp.md) for decorators such as `@@clientName` and `@@access`.

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

## Set up a customization project

Create a Maven project that depends on `azure-autorest-customization`:

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

Create a class that extends `com.azure.autorest.customization.Customization` and overrides
`void customize(LibraryCustomization, Logger)`. The `LibraryCustomization` parameter is the entry point for changing
generated Java code before it is written to disk.

```java
@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")));
}
```

Configure the generator to use the class:

```diff
"@azure-tools/typespec-java":
emitter-output-dir: "{output-dir}/{service-dir}/azure-contoso-widgetmanager"
namespace: com.azure.contoso.widgetmanager
+ customization-class: customization/src/main/java/MyCustomization.java
```

## Navigate generated code

Start with `LibraryCustomization`, then navigate to a package or class with `getClass(packageName, className)`.
`LibraryCustomization`, `PackageCustomization`, and `ClassCustomization` provide the primary navigation APIs.
Use `customizeAst` to change the JavaParser AST for the selected source file. Add imports with
`ast.addImport(...)` whenever the new code references a type not already imported.

## Supported customizations

The following common changes can be made through the JavaParser AST:

| Customization | Typical AST operation |
|---|---|
| Change a class modifier | `ClassOrInterfaceDeclaration.setModifiers(...)` |
| Change a method modifier | `MethodDeclaration.setModifiers(...)` |
| Change a method return type or body | `MethodDeclaration.setType(...)` and `setBody(...)` |
| Change a class supertype | Clear and add `ClassOrInterfaceType` extended types |
| Add or remove a class or method annotation | `addMarkerAnnotation(...)` or remove the matching annotation node |
| Add a field default value | `VariableDeclarator.setInitializer(...)` |
| Add getter and setter methods | `ClassOrInterfaceDeclaration.addMethod(...)` |
| Rename an enum member | `EnumConstantDeclaration.setName(...)` |
| Update class or method Javadoc | `setJavadocComment(...)` |
| Add or remove Javadoc tags | Update the `Javadoc` returned by `getJavadoc()` |

### Change a method modifier

```java
@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))));
}
```

### Change a method return type

When changing a return type, update both the declared type and method body. Add an import for the replacement type.

```java
@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 -> {
method.setType("UUID");
method.setBody(StaticJavaParser.parseBlock("{ return UUID.fromString(this.id); }"));
}));
});
}
```

For a method originally returning `void`, provide the complete return expression. To change a method to return
`void`, set its body appropriately and do not add a return value.

### Change a supertype

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

### Add a field default value

```java
@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(field -> field.getVariables().forEach(variable -> {
if ("bar".equals(variable.getNameAsString())) {
variable.setInitializer("\"bar\"");
}
})));
}
```

### Generate accessor methods

```java
@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; }"));
}));
}
```

### Update Javadoc

Set a complete description with `setJavadocComment`. For parameter, return, and exception tags, get the existing
Javadoc and add a block tag:

```java
method.getJavadoc().ifPresent(javadoc -> method.setJavadocComment(
javadoc.addBlockTag("param", "active", "whether the Foo is active")));
```

Use `return` for a return-value tag and `throws` with the exception type and description for an exception tag.

## Troubleshooting

### TypeSpec Java reports “Failed to format file: `<path>`. File content: `<file-content>`.”

Customized Java code likely contains a syntax error.

Inspect the source shown after `File content:` to locate the malformed code, then fix the customization and regenerate SDK.
2 changes: 2 additions & 0 deletions docs/contributor/typespec-quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ Set `partial-update: true` in `tspconfig.yaml` emitter options. TypeSpec-Java wi
customization-class: customization/src/main/java/MyCustomization.java
```

See [TypeSpec Java Customization](https://github.com/Azure/azure-sdk-for-java/blob/main/docs/contributor/typespec-java-customization.md) for guidance on writing AST-based Java customizations.

---

## 5. Improve Documentation
Expand Down
Loading