apache / apache/fesod

[Enhancement] Add composable annotations support

Open
#918 0 comments 0 reactions 0 assignees View on GitHub
enhancement
Dominant language
Java
Stars
6.2k
Forks
532
Avg merge
1d 3h
Merged PRs (30d)
42

Description

### Search before asking

- [x] I searched in the [issues](https://github.com/apache/fesod/issues) and found nothing similar.

### Motivation

This proposal aims to introduce Composable Annotation Support for Fesod-Sheet's Java Model mode, allowing users to create custom, reusable annotation combinations (presets) that group multiple built-in Fesod annotations into a single meta-annotation. This dramatically reduces boilerplate when repetitive style/format configurations are applied across many model classes.

### Solution

#### Content

Composable annotations solve the above problems by letting users define their own annotations that bundle multiple built-in annotations with preset defaults.

Here, two meta-annotations `@FesodMarked` and `@FesodMarked.AliasFor` are introduced to implement the following composite annotation strategy:

**1. Preset mode:** composable-annotations (marked by `@FesodMarked`) do not declare any attributes themselves; they simply bundle several meta-annotations with fixed default values together into a preset configuration, and users cannot override its attributes.
**2. AliasFor mode:** composable-annotations (marked with `@FesodMarked`) can explicitly declare attribute mapping relationships through `@FesodMarked.AliasFor`, forwarding their own attribute values to the specified attributes of the target annotation. Users can override its attributes.

> Both class-level and field-level composable annotations are supported. When a direct annotation and a composable annotation of the same type coexist at the same level, the direct annotation takes priority.

#### Supported Annotations

In the `fesod-sheet` module, all built-in annotations except `@ExcelIgnore` and `@ExcelIgnoreUnannotated` can be used within composable annotations.

#### API Usage Example

1. Define a composable annotation with `@FesodMarked.AliasFor`

Create a custom annotation that forwards attribute values to `@ExcelProperty`:

```java
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@FesodMarked
@ExcelProperty
@Inherited
public @interface ComposableExcelProperty {

@FesodMarked.AliasFor(annotation = ExcelProperty.class, attribute = "value")
String value() default "";

@FesodMarked.AliasFor(annotation = ExcelProperty.class, attribute = "index")
int index() default -1;

// If the 'attribute' value is not explicitly set, the name of the current attribute is used by default for mapping
@FesodMarked.AliasFor(annotation = ExcelProperty.class)
int order() default Integer.MAX_VALUE;
}
```

```java
public class ExcelModel {

// Same as @ExcelProperty(value = {"Order ID"}, index = 0)
@ComposableExcelProperty(value = "Order ID", index = 0)
private String orderId;

// Same as @ExcelProperty(value = {"Total Amount"}, order = 1)
@ComposableExcelProperty(value = "Total Amount", order = 1)
private BigDecimal amount;
}
```

2. Define a no-attributes style preset

Group multiple annotations with fixed defaults into a single reusable annotation:

```java
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@FesodMarked
@HeadRowHeight(30)
@ContentRowHeight(20)
@OnceAbsoluteMerge(firstRowIndex = 0, lastRowIndex = 0, firstColumnIndex = 0, lastColumnIndex = 3)
@Inherited
public @interface CommonTableStyle {}
```

```java
// Same as:
// @HeadRowHeight(30)
// @ContentRowHeight(20)
// @OnceAbsoluteMerge(firstRowIndex = 0, lastRowIndex = 0, firstColumnIndex = 0, lastColumnIndex = 3)
@CommonTableStyle
public class ExcelModel {

@ExcelProperty("Date")
@DateTimeFormat("yyyy-MM-dd")
private Date date;

@ExcelProperty("Revenue")
@NumberFormat("#,##0.00")
private BigDecimal revenue;
}
```

3. Combine field-level and class-level composable annotations

```java
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@FesodMarked
@ContentStyle(wrapped = BooleanEnum.TRUE, fillForegroundColor = 10)
@ContentFontStyle(fontName = "Arial", fontHeightInPoints = 12, bold = BooleanEnum.TRUE)
@Inherited
public @interface ContentPreset {}
```

```java
// Same as:
// @HeadRowHeight(30)
// @ContentRowHeight(20)
// @OnceAbsoluteMerge(firstRowIndex = 0, lastRowIndex = 0, firstColumnIndex = 0, lastColumnIndex = 3)
@CommonTableStyle
public class ExcelModel {

// Same as @ExcelProperty(value = {"Product Name"})
@ComposableExcelProperty("Product Name")
private String product;

// Same as:
// @ContentStyle(wrapped = BooleanEnum.TRUE, fillForegroundColor = 10)
// @ContentFontStyle(fontName = "Arial", fontHeightInPoints = 12, bold = BooleanEnum.TRUE)
@ContentPreset
@NumberFormat("#,##0.00")
private BigDecimal sales;
}
```

4. Direct annotations override composable annotations

When both a direct annotation and a composable annotation of the same type exist at the same level, the direct annotation wins:

```java
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@FesodMarked
@ExcelProperty(value = {"Full Name"})
@Inherited
public @interface FullNamePreset {}
```

```java
public class ExcelModel {

@ExcelProperty("First Name") // takes priority
@FullNamePreset
private String firstName;
}
```

5. `@FesodMarked.AliasFor` targets must be meta-present

Every `@FesodMarked.AliasFor` must reference an annotation that is meta-present on the composable.

```java
// INVALID: @ColumnWidth is NOT meta-present on this annotation
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@FesodMarked
@Inherited
public @interface BadComposable {

// will throw
@FesodMarked.AliasFor(annotation = ColumnWidth.class, attribute = "value")
int width() default -1;
}
```

6. Custom annotation attribute values must be explicitly marked with `@FesodMarked.AliasFor` if they need to be forwarded.

```java
// INVALID: value() value is not forward into @ExcelProperty because there is no mark @FesodMarked.AliasFor.
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@FesodMarked
@ExcelProperty
@Inherited
public @interface BadComposable {

// not work
String[] value() default {"Name"};
}
```

7. No changes required for existing API usage

```java
FesodSheet.write(pathname, ExcelModel.class)
.sheet()
.doWrite(dataList);
```

### Alternatives

_No response_

### Anything else?

_No response_

### Are you willing to submit a PR?

- [x] I'm willing to submit a PR!

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.