Skip to content

Crysknife 0.11 Release Notes

Latest

Choose a tag to compare

@treblereel treblereel released this 12 Aug 23:09
· 169 commits to master since this release
3c0f93f

Crysknife 0.11 is a major feature release that brings the framework closer to the Jakarta CDI specification,
adds new UI modules for REST, WebSocket, security and data binding, and includes significant
improvements to the event system, navigation, internationalization, and template engine.

Minimum requirements: Java 21, Maven 3.9


CDI Core

1. Jakarta CDI Interceptors

Full support(well, what can be done in js env) for Jakarta Interceptors specification: @InterceptorBinding, @Interceptor, and @AroundInvoke.
Interceptors allow cross-cutting concerns (logging, metrics, authorization) to be applied declaratively
to bean methods without modifying business logic.

// Define an interceptor binding
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Logged {}

// Implement the interceptor
@ApplicationScoped
@Interceptor
@Logged
public class LoggingInterceptor {

    @AroundInvoke
    public Object log(InvocationContext ctx) throws Exception {
        String method = ctx.getMethod().getName();
        console.log(">> " + method);
        Object result = ctx.proceed();
        console.log("<< " + method);
        return result;
    }
}

// Apply to a bean — per method or on the entire class
@ApplicationScoped
public class OrderService {

    @Logged
    public void placeOrder(Order order) { ... }

    public void cancel(Order order) { ... } // not intercepted
}

2. Jakarta CDI Decorators

Support for the Decorator pattern from the CDI specification: @Decorator, @Delegate, and @Priority
for ordering. Decorators wrap beans transparently while preserving the contract of the decorated interface.

public interface Greeter {
    String greet(String name);
}

@ApplicationScoped
public class SimpleGreeter implements Greeter {
    public String greet(String name) {
        return "Hello, " + name + "!";
    }
}

// Decorator wraps the original bean
@ApplicationScoped
@Decorator
@Priority(1000)
public class UpperCaseGreeterDecorator implements Greeter {

    @Inject @Delegate
    Greeter delegate;

    public String greet(String name) {
        return delegate.greet(name).toUpperCase();
    }
}

Multiple decorators can be chained via @Priority ordering — higher priority = outermost wrapper:

@Decorator @Priority(1000)
public class BracketFormatter implements Formatter { ... }   // inner

@Decorator @Priority(2000)
public class StarFormatter implements Formatter { ... }      // outer
// Result: *[hello]*

3. Event System Rewrite

The event system has been rewritten around ObserverRegistry with type-hierarchy dispatch.
Firing ChildEvent extends BaseEvent now correctly notifies observers of BaseEvent.
Non-@Dependent scoped beans with @Observes are lazily activated on first event delivery.

public class BaseEvent {}
public class OrderEvent extends BaseEvent {}

@ApplicationScoped
public class AuditObserver {

    // Receives both BaseEvent and OrderEvent (type-hierarchy dispatch)
    public void onEvent(@Observes BaseEvent event) {
        audit(event);
    }
}

@Singleton
public class LazyMetricsObserver {

    // Bean is not instantiated until the first MetricsEvent fires
    public void onMetrics(@Observes MetricsEvent event) {
        record(event);
    }
}

New Modules

4. REST Client (Caller<T>)

New module ui/rest provides type-safe REST client generation. Define a JAX-RS interface, inject
Caller<T>, and make HTTP calls — the framework generates the implementation at compile time.

REST caller and JSON mapper generation are fully integrated into the crysknife processor —
users no longer need to add jakarta-rest:processor or json-mapper:processor as separate
annotation processor dependencies. Everything is handled by crysknife-ui-rest-generator.

Basic usage

// Define REST interface with standard JAX-RS annotations
@Path("/items")
public interface ItemService {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    List<Item> listItems();

    @GET @Path("/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    Item getItem(@PathParam("id") long id);

    @POST @Consumes(MediaType.APPLICATION_JSON)
    Item createItem(Item item);

    @PUT @Path("/{id}") @Consumes(MediaType.APPLICATION_JSON)
    Item updateItem(@PathParam("id") long id, Item item);

    @DELETE @Path("/{id}")
    Item deleteItem(@PathParam("id") long id);

    @PATCH @Path("/{id}") @Consumes(MediaType.APPLICATION_JSON)
    Item patchItem(@PathParam("id") long id, Item item);

    @GET @Path("/search")
    List<Item> searchItems(@QueryParam("q") String query);

    @GET @Path("/{id}/header")
    Item getItemWithHeader(@PathParam("id") long id, @HeaderParam("X-Custom-Header") String header);
}

// Configure base URL via CDI producer
@ApplicationScoped
public class RestDemoConfig {

    @Produces
    @Named("api")
    public RestConfig apiConfig() {
        return RestConfig.builder()
                .baseUrl("https://api.example.com")
                .build();
    }
}

// Inject and use
@Inject @Named("api")
Caller<ItemService> itemService;

public void loadItem() {
    itemService
        .onError((response, throwable) -> console.error("Failed: " + response.getStatusCode()))
        .call(result -> {
            Item item = (Item) result;
            console.log(item.getName());
        })
        .getItem(1);
}

Full response access (body + headers + status code):

itemService
    .call((body, response) -> {
        Item item = (Item) body;
        int status = response.getStatusCode();
    })
    .getItem(1);

Bearer token authentication

Static token or dynamic supplier (re-evaluated per request):

// Static token
RestConfig.builder()
    .baseUrl("https://api.example.com")
    .bearerToken("my-secret-token")
    .build();

// Dynamic token — supplier called on every request
RestConfig.builder()
    .baseUrl("https://api.example.com")
    .bearerToken(() -> currentToken)
    .build();

Retry policy

Automatic retry with configurable conditions — for example, retry on 401 with token refresh:

RestConfig.builder()
    .baseUrl("https://api.example.com")
    .bearerToken(() -> currentToken)
    .retryPolicy(RetryPolicy.builder()
        .maxRetries(1)
        .delayMs(0)
        .condition((response, error, attempt) ->
            response != null && response.getStatusCode() == 401)
        .build())
    .responseFilter((reqCtx, respCtx) -> {
        if (respCtx.getStatusCode() == 401) {
            currentToken = refreshToken();
        }
    })
    .build();

Request and response filters

// Request filter — add headers, modify request, or abort
RestConfig.builder()
    .requestFilter(ctx ->
        ctx.getHeaders().put("X-Custom-Header", "value"))
    .build();

// Abort request without hitting the server
RestConfig.builder()
    .requestFilter(ctx -> ctx.abortWith(
        new RestResponse(403, "Forbidden", "{}", Collections.emptyMap())))
    .build();

// Response filter — inspect response
RestConfig.builder()
    .responseFilter((reqCtx, respCtx) ->
        console.log("Status: " + respCtx.getStatusCode()))
    .build();

Exception mapper

Map HTTP error codes to typed exceptions:

RestConfig.builder()
    .exceptionMapper(response -> {
        if (response.getStatusCode() == 404) {
            return new NotFoundException(response);
        }
        if (response.getStatusCode() == 401) {
            return new UnauthorizedException(response);
        }
        return null; // fallback to RestException
    })
    .build();

Promise API

Generated _RestCaller class provides a promise-based alternative:

ItemService_RestCaller caller = (ItemService_RestCaller) itemService;

caller.promiseGetItem(1)
    .then(item -> console.log(item.getName()))
    .catchError(error -> console.error(error.getMessage()));

Keycloak OIDC integration

The combination of dynamic bearer token, retry policy, and response filter provides a clean
integration pattern with Keycloak (or any OIDC provider). The REST client handles the full
token lifecycle — initial authentication, automatic refresh on 401, and per-request token
injection — without any Keycloak-specific dependencies on the client side.

// Shared REST interface — used by both client (J2CL) and server (Quarkus)
@Path("/api/secure")
@Produces(MediaType.APPLICATION_JSON)
public interface SecureItemService {

    @GET @Path("/item")
    SecureItem getSecureItem();
}
// Server side — Quarkus resource with @RolesAllowed
@Path("/api/secure")
@Produces(MediaType.APPLICATION_JSON)
public class SecureItemResource {

    @GET @Path("/item")
    @RolesAllowed("user")
    public SecureItem getSecureItem() {
        return new SecureItem(1, "protected-data");
    }
}
// Client side — automatic token refresh on 401
String[] currentToken = { keycloakToken };

Caller<SecureItemService> caller = new SecureItemService_RestCaller(
    RestConfig.builder()
        .baseUrl(baseUrl)
        .bearerToken(() -> currentToken[0])
        .retryPolicy(RetryPolicy.builder()
            .maxRetries(1)
            .delayMs(0)
            .condition((response, error, attempt) ->
                response != null && response.getStatusCode() == 401)
            .build())
        .responseFilter((reqCtx, respCtx) -> {
            if (respCtx.getStatusCode() == 401) {
                currentToken[0] = refreshTokenFromKeycloak();
            }
        })
        .build());

caller.call(result -> {
    SecureItem item = (SecureItem) result;
    console.log(item.getName());   // "protected-data"
}).getSecureItem();

This pattern is fully tested with Keycloak 26 in Docker (via Testcontainers), Quarkus OIDC,
and covers: valid token, missing token (401), expired token with automatic refresh, and
dynamic token rotation between requests.


5. Security Module

New module ui/security adds role-based security: SecurityContext API, navigation guards via
@RolesAllowed, and element-level visibility control via @IfRole.

// Inject SecurityContext to check auth state
@Inject
SecurityContext securityContext;

public void checkAccess() {
    if (securityContext.isLoggedIn()) {
        User user = securityContext.getUser();
        boolean isAdmin = securityContext.isUserInRole("admin");
    }
}
// Protect pages with @RolesAllowed
@Page(path = "admin")
@RolesAllowed("admin")
public class AdminPage implements IsElement<HTMLDivElement> { ... }
// Conditionally show elements based on roles
@Inject @DataField @IfRole("user")
HTMLDivElement userSection;

@Inject @DataField @IfRole("admin")
HTMLDivElement adminPanel;      // hidden if user lacks "admin" role

6. WebSocket Client

New module ui/websocket provides a type-safe WebSocket client using Jakarta WebSocket annotations.
Define a @ClientEndpoint with lifecycle callbacks, inject WebSocketConnector<T>, and connect —
the framework generates the browser-side wiring at compile time.

// Define a client endpoint with Jakarta WebSocket annotations
@ClientEndpoint
@Singleton
public class EchoEndpoint {

    @OnOpen
    public void onOpen(Session session) {
        session.getBasicRemote().sendText("hello");
    }

    @OnMessage
    public void onMessage(String message) {
        DomGlobal.console.log("Received: " + message);
    }

    @OnClose
    public void onClose(CloseReason reason) {
        DomGlobal.console.log("Closed: " + reason.getCloseCode().getCode());
    }

    @OnError
    public void onError(Throwable error) {
        DomGlobal.console.error("Error: " + error.getMessage());
    }
}
// Inject the connector and connect
@Singleton
public class ChatPanel implements IsElement<HTMLDivElement> {

    @Inject
    WebSocketConnector<EchoEndpoint> connector;

    @PostConstruct
    public void init() {
        String wsUrl = "ws://" + DomGlobal.window.location.hostname + ":"
                + DomGlobal.window.location.port + "/echo";
        connector.baseUri(wsUrl).connect();
    }
}

7. DataBinder Module

New module ui/databinding provides two-way data binding between model objects and DOM elements.
Models are annotated with @Bindable, and fields can be bound declaratively via @Bound or
programmatically via DataBinder API.

@Bindable
@Dependent
public class UserModel {
    private String name;
    private String email;
    // getters + setters
}

Declarative binding with @Bound:

@Inject
DataBinder<UserModel> binder;

@Bound(property = "name")
HTMLInputElement nameField;

@Bound(property = "email")
HTMLInputElement emailField;

Programmatic binding:

DataBinder<UserModel> binder = DataBinder.forType(UserModel.class);
binder.bind(nameInput, "name");

UserModel model = binder.getModel();
model.setName("John");   // UI updates automatically

Templates & UI

8. PatternFly Java (PFJ) Support in @Templated

@Templated beans now support PatternFly Java components
as @DataField fields. This was achieved by adding support for org.jboss.elemento.IsElement interface
in the template engine — any Elemento-based component (including all PFJ components) can now be used
as a @DataField.

import org.patternfly.component.button.Button;
import org.patternfly.component.card.Card;

import static org.patternfly.component.button.Button.button;
import static org.patternfly.component.card.Card.card;
import static org.patternfly.component.card.CardBody.cardBody;
import static org.patternfly.component.card.CardTitle.cardTitle;

@Singleton
@Templated("Dashboard.html")
public class Dashboard implements IsElement<HTMLDivElement> {

    @DataField
    Button primaryBtn = button("Primary").primary();

    @DataField
    Button dangerBtn = button("Danger").danger();

    @DataField
    Card statusCard = card()
        .addTitle(cardTitle("Status"))
        .addBody(cardBody().text("All systems operational"));

    @EventHandler("primaryBtn")
    public void onPrimaryClick(@ForEvent("click") MouseEvent e) {
        primaryBtn.text("Clicked!");
    }
}
<!-- Dashboard.html -->
<div class="pf-v6-c-page__main-section">
  <div class="pf-v6-l-flex pf-m-gap-sm">
    <button data-field="primaryBtn"></button>
    <button data-field="dangerBtn"></button>
  </div>
  <div data-field="statusCard"></div>
</div>

9. Inline Templates

@Templated now supports inline HTML via the inline attribute, eliminating the need for a
separate .html file for simple components.

@Templated(inline = """
    <div data-field="root" id="inline-page">
      <h2>Inline Template</h2>
      <span data-field="content">Content here</span>
    </div>
    """)
public class InlinePage implements IsElement<HTMLDivElement> {

    @Inject @DataField
    HTMLDivElement root;

    @Inject @DataField @Named("span")
    HTMLElement content;
}

10. ConflictStrategy for @DataField

@DataField now supports a strategy attribute to control what happens when both the template
and the bean provide an element for the same data field.

public enum ConflictStrategy {
    USE_TEMPLATE,   // default — template element wins
    USE_BEAN        // bean's programmatic element wins
}
@Templated("MyComponent.html")
public class MyComponent implements IsElement<HTMLDivElement> {

    // Template HTML is used (default)
    @Inject @DataField(strategy = ConflictStrategy.USE_TEMPLATE)
    HTMLDivElement templateContent;

    // Bean-created element replaces template placeholder
    @DataField(strategy = ConflictStrategy.USE_BEAN)
    HTMLDivElement customWidget = createWidget();
}

11. Trusted Types Support

Crysknife now supports the Trusted Types API
for CSP-compliant DOM manipulation. SafeHtmlUtils, StyleInjector, and ScriptInjector
route all innerHTML/src assignments through a configurable Trusted Types policy.

The policy name is configurable via goog.define (Closure Compiler --define):

--define=crysknife.trustedtype.policy.name=myapp

Default policy name: crysknife.


Navigation & i18n

12. Navigation: @PageState and PageNotFound

@PageState binds page fields to URL query parameters with support for renaming, defaults,
and multi-valued parameters. PageNotFound role enables custom 404 pages.

@Page(path = "users")
public class UsersPage implements IsElement<HTMLDivElement> {

    @PageState
    String userId;

    @PageState(value = "q")
    String searchQuery;

    @PageState(defaultValue = "1")
    int page;

    @PageState
    List<String> tags;     // multi-valued: ?tags=a&tags=b
}
// Custom 404 page
@Page(path = "NotFound", role = PageNotFound.class)
@Templated("notfound.html")
public class NotFoundPage implements IsElement<HTMLDivElement> {
    @Inject @DataField
    HTMLDivElement root;
}

13. Internationalization (@TranslationBundle)

Complete rewrite of the i18n system. The new approach uses @TranslationBundle interfaces with
@TranslationKey methods, generates .native.js files with goog.getMsg() calls, and produces
XTB files per locale for Closure Compiler integration.

// Define a translation bundle interface
@TranslationBundle
public interface AppMessages {

    @TranslationKey(defaultValue = "Welcome")
    String welcome();

    @TranslationKey(defaultValue = "Hello {$name}, you have {$count} messages")
    String greeting(String name, String count);

    @TranslationKey(defaultValue = "Delete", key = "btn_delete")
    String deleteButton();
}

Provide translations in .properties files alongside the interface:

# AppMessages_ru.properties
welcome=Добро пожаловать
greeting=Привет {$name}, у тебя {$count} сообщений
btn_delete=Удалить

Use data-i18n-key in HTML templates for declarative translation:

<span data-field="welcomeLabel" data-i18n-key="AppMessages.welcome">Welcome</span>
<button data-field="deleteBtn" data-i18n-key="AppMessages.deleteButton">Delete</button>

Stability & Platform

14. Memory Leak Fixes

A series of fixes addressing memory leaks across the framework:

  • Instance.destroy() — now properly implemented (previously threw UnsupportedOperationException)
  • BeanFactorydependentBeans HashMap no longer grows unbounded; entries are cleaned up on destroy
  • MutationObserver — fixed index bug and added listener cleanup on destroy
  • Navigation — fixed listener accumulation on reinit; references cleaned up on destroy
  • DataBinding — property change handlers cleared on unbind() to prevent leaks
  • ManagedInstanceImpl — all tracked instances cleared on destroy()
  • Events — fixed ConcurrentModificationException in ObserverRegistry.fire()

15. Java 21 and Maven 3.9

Minimum build requirements updated to Java 21 and Maven 3.9. CI pipelines updated accordingly.
This enables the use of modern Java features such as text blocks, pattern matching, and sealed classes
in application code compiled with crysknife.