Skip to main content
ElastiCORE 3.0 is a release candidate: 3.0.261022-rc1 on Maven Central. Projects on the 2.2 line: see the 2.2 documentation.
Version: 3.0

Annotations and methods

Most of what a rule does to generated code is "add or remove something somewhere". All of it is said to the file being made (SourceCode), which ctx(spec) returns. Sorting imports, laying out lines and Java/Kotlin syntax are the generator's job.

Where​

WhereAddRemove
Classctx(e).ensureClassAnnotation(anno)ctx(e).removeClassAnnotation("a.b.C")
Fieldctx(e).field(f).ensure(anno)ctx(e).field(f).removeAnnotation("a.b.C")
Method (by name)ctx(e).method("name").ensure(anno)-
The field itself-ctx(e).removeField(f)
The file itself-ctx(e).dontMake()

Removing also removes what the default rules added, because team rules always run after the default rules.

    /** Takes the @DynamicUpdate the built-in rules put on every entity back off. */
@Rule("ps-no-dynamic-update")
void noDynamicUpdate(EntitySpec e) {
ctx(e).removeClassAnnotation("org.hibernate.annotations.DynamicUpdate");
}

In the pet store this rule removed @DynamicUpdate not only from the generator's files (Customer.java, PurchaseOrderBase.java …) but also from the developer's classes of the split roots (PurchaseOrder.java, OrderLine.java). The generator keeps a developer class's table annotations (@Entity, @Table, @DynamicUpdate …) in line with the DSL, and leaves the body and the other annotations the developer added alone.

Fields: ensure, add, merge, replace​

MethodIf the same annotation is already thereIf notWhen
ensure(anno)does nothingadds itmost of the time
add(anno)adds one moreadds itrepeatable annotations
merge(anno)adds only the attributesadds itone more attribute on the @Schema or @Column a default rule added
replace(anno)replaces it with this oneadds itthe team's value instead of the default rule's

ElastiCORE already puts @Schema(description = ...) on DTO fields, so adding hidden = true takes merge. With ensure nothing happens - see the example in Targets.

    /** A field marked @pii in the DSL is accepted in requests but never sent back in responses. */
@Rule(value = "ps-pii", target = "dto")
void pii(EntitySpec dto, FieldSpec f) {
if (f.hasDsl("pii")) {
ctx(dto).field(f).ensure(AnnoCode.of("com.fasterxml.jackson.annotation.JsonProperty")
.valRaw("access", "JsonProperty.Access.WRITE_ONLY"));
}
}
CustomerDTO.java
	@Schema(description = "email address"  )
@Size(max=100)
@Email(message = "Email format invalid")
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY) // ps-pii
private String email;

Checked by running it: send email to sign-up (POST /api/petstore/customer) and it is stored, while the response JSON has no email.

{"customerId":3,"firstName":"Rule","homeAddress":{…},"lastName":"Demo","login":"rulesdemo3","role":"U"}

AnnoCode - building an annotation​

Never build an annotation by concatenating strings ("@Column(name = \"" + n + "\")"). AnnoCode takes care of quotes, imports, and Java's {} versus Kotlin's []. That is why a rule needs no if (kotlin).

CodeResult
AnnoCode.of("jakarta.validation.constraints.Size").val("min", 2)@Size(min = 2)
.val("name", "user_id")name = "user_id" - strings get quotes
.val("hidden", true)hidden = true
.valRaw("access", "JsonProperty.Access.WRITE_ONLY")as written - constants, class literals
.valRaw("length", "%d", 200)formatted
.value("CreditCheckPort")the unnamed attribute - @ExternalSystem("CreditCheckPort")
AnnoCode.of("org.jmolecules.ddd.annotation.%s", "Entity").qualified()@org.jmolecules.ddd.annotation.Entity - the full name, when the simple name clashes with jakarta.persistence.Entity

The annotation class does not have to be on the generator's classpath. The rule only writes its name; the class is needed when the application compiles (implementation 'org.jmolecules:jmolecules-ddd:…').

Annotating a method - method("name")​

Annotates a method of the generated file, by name. Use it for methods whose name is in the DSL, such as the query methods in repository.yml and the external call methods in port.yml. Example in Targets.

Adding a method - addMethod​

Adds a method the rule wrote by hand at the end of the class (before the closing brace). For when every root needs the same helper method:

    /** Every root entity class says who it is in an audit log: "Customer#42". */
@Rule("ps-audit-label")
void auditLabel(EntitySpec e) {
if (!e.aggregateRoot() || e.hasMeta("abstract") || e.pkFields().size() != 1) return;
FieldSpec id = e.pkFields().get(0);
ctx(e).addMethod(String.format(
"\t/** Who this is in an audit log: %s#<id>. */%n"
+ "\tpublic String auditLabel() {%n"
+ "\t\treturn \"%s#\" + get%s();%n"
+ "\t}", e.getName(), e.getName(), id.capitalName()));
}
Customer.java
	/** Who this is in an audit log: Customer#<id>. */
public String auditLabel() {
return "Customer#" + getCustomerId();
}
}
  • The method body goes in exactly as the rule wrote it, so it is language-specific. To use it in a Kotlin project too, branch on ctx(e).syntax() or ask Syntax.
  • Use it on files the generator rewrites every time, such as entities. On a developer's file (controllers, XxxService) rules apply only when the file is first created.
  • In a root split into two files (PurchaseOrderBase + the developer's PurchaseOrder) the method goes only into the generator's PurchaseOrderBase, and the developer class inherits it. To add a method to the developer class only, write the rule with @Rule(target = "behavior") - it applies when that file is first created.
  • Enums (target = "enum") take added methods too; they go after the constants and the constructor.
  • 3.0.261022-rc1 has a bug: addMethod is dropped on a class with no methods at all (an entity with only fields), and it also goes once more into the developer class of a split root. It is fixed in the next release. On rc1, write it as members after the identifier field instead: ctx(e).field(id).after("public String auditLabel() {").after("\treturn …;").after("}") (in a field rule).

Leaving out endpoints - excludeMethod​

The DSL already has @restExclude(deleteById) and @readonly. When you want the same with a word of your team's own, leave out endpoints of the generated controller by name.

NameEndpoint
createPOST /
readGET /{id}
searchPOST /search
update, batchUpdatePUT /, PUT /batch
deleteById, deleteBatchDELETE /{id}, POST /delete
schemaInfoGET /schema/info

Leaving out deleteById also leaves out deleteBatch, which calls it; leaving out update also leaves out batchUpdate.

    /** meta @nodelete: the REST API has no delete endpoints. */
@Rule(value = "ps-no-delete", target = "control")
void noDelete(EntitySpec e) {
if (e.hasMeta("nodelete")) ctx(e).excludeMethod("deleteById");
}
model/entity.yml
  Category:
meta: entity @root @editable @expose(20) @access(read=public) @nodelete

The generated CategoryController has no @DeleteMapping and no POST /delete; at run time DELETE /api/petstore/category/1000 and POST /api/petstore/category/delete return 405.

Controllers only

Use excludeMethod in a team rule on controllers (target = "control"). The hexagonal use case port (CategoryUseCase) is made before the team rules run, so leaving deleteById out of the service (service) makes CategoryService implements CategoryUseCase fail to compile. To leave it out of the service and the port too, use @restExclude(deleteById) in the DSL - the default rules read it directly and leave it out of every file together.

A controller is a developer's file. The rule does not apply to a controller you have already changed; a controller never changed is brought in line on the next generation.

Changing a field's declaration​

Besides annotations, FieldCode changes the declaration. Most of this is better done in the DSL; use a rule only when it is a convention for the whole team.

MethodDoes
type("String")the declared type
rename("x")the name
initial("0")the initial value
comment("…")the field comment
before("…")a line above the declaration
after("…")a member after the declaration (calling it several times continues the lines)