Class FreemarkerMixin

java.lang.Object
org.apache.juneau.rest.server.view.freemarker.FreemarkerMixin
Direct Known Subclasses:
ConsoleFreemarkerMixin

public class FreemarkerMixin extends Object
Mixin that wires Apache FreeMarker view-rendering onto any Juneau REST resource.

Compose into a host resource via @Rest(mixins=FreemarkerMixin.class); the host then:

  1. Gains a default mount at /freemarker/* that serves raw FreeMarker templates from the importer's classpath by asking the configured Configuration for the named template and rendering it with an empty data model (raw render path; callers who want attributes use FreemarkerView from a typed handler instead).
  2. Registers FreemarkerViewRenderer for the mixin's own endpoints (e.g. the /freemarker/* route above) via the mixin's own @Rest(responseProcessors=...) declaration.
Auto-wiring the renderer into the host's own endpoints:

A bare @Rest(mixins=FreemarkerMixin.class) is enough — a host whose own @RestOp method returns FreemarkerView renders it through the FreeMarker engine with no extra wiring. That works because FreemarkerMixin declares @Rest(mergeResponseProcessorsIntoHost=true) on its own class, so the framework folds FreemarkerMixin's FreemarkerViewRenderer into the host's own response-processor chain automatically.

This fold is per-mixin, opt-in, and response-processor-scoped: it happens only because FreemarkerMixin declares the opt-in (a mixin that does not is still scoped to its own endpoints, the standard @Rest(mixins=...) rule), and it folds only the renderer — never guards, serializers, or any other list-shaped attribute.

Two explicit wirings remain supported — useful when you want to be explicit, or to fold more than the renderer:

  1. @Mixin(mergeIntoHost=true) via the rich mixinDefs form — folds all of FreemarkerMixin's list-shaped @Rest attributes into the host's own chain, not just the renderer.
  2. List FreemarkerViewRenderer.class explicitly in the host's own @Rest(responseProcessors=...) — the fully manual equivalent.

The ResponseProcessorList partition pass repositions FreemarkerViewRenderer (a ViewRenderer) ahead of SerializedPojoProcessor in every case, so the FreemarkerView bean is dispatched to the FreeMarker engine rather than bean-serialized.

Composition example (microservice):

@Rest(path="/app", mixins=FreemarkerMixin.class) public class AppResource extends RestServlet { @Bean FreemarkerMixin freemarker() { return FreemarkerMixin.create() .basePath("/templates/") .templateSuffix(".ftlh") .build(); } @RestGet("/hello/{name}") public View hello(@Path String name) { return FreemarkerView.of("hello").attr("name", name); } }

Configurable mount path:

The default mount /freemarker/* can be overridden via the SVL variable ${juneau.freemarker.path:freemarker} — set via system property (-Djuneau.freemarker.path=views), environment variable (JUNEAU_FREEMARKER_PATH=views), or Config key (juneau.freemarker.path = views) to change the runtime mount without subclassing. Resolution happens once at RestContext construction time (SVL resolution in @RestOp(path)).

Mixin-only deployment:

This resource is designed for composition via @Rest(mixins=...). The mount path is pinned at the op level by @RestGet(path="/{juneau.freemarker.path:freemarker}/*") on render(java.lang.String, org.apache.juneau.rest.server.RestRequest, org.apache.juneau.rest.server.RestResponse); a class-level @Rest(paths=...) declaration would be silently ignored under the mixin pattern (see @Rest(paths) Javadoc).

Choosing a Configuration:

The juneau-rest-server-view-freemarker module ships only org.freemarker:freemarker in provided scope. No configuration bean is bundled. The bridge resolves a Configuration at first use from three sources, in order:

  • User-supplied bean: register a @Bean freemarker.template.Configuration (Spring) or BasicBeanStore.put(freemarker.template.Configuration.class, cfg) (microservice). Spring Boot's spring-boot-starter-freemarker autoconfigures one out of the box; the bridge picks it up via BeanStore.getBean(Configuration.class).
  • Bridge default: when nothing is registered, the bridge constructs a default Configuration anchored on a classpath resource root derived from basePath ("/templates/" becomes ClassLoaderTemplateResolver-equivalent prefix "/templates"). The default pins IncompatibleImprovements to the bridge-tested minor version (Configuration.VERSION_2_3_34), sets DefaultEncoding to UTF-8, uses HTMLOutputFormat so HTML escaping is the natural target, applies TemplateUpdateDelayMilliseconds per the cacheTemplates flag (production-safe by default; users opt into hot-reload via cacheTemplates(false)), and sets an ObjectWrapper with exposeFields=true (see below) unless overridden.

When no FreeMarker engine is on the classpath, the renderer surfaces FreemarkerViewRenderer.NO_ENGINE_DIAGNOSTIC naming the missing dependency.

Public-field DTOs (getter-only default trap):

FreeMarker's own version-default ObjectWrapper exposes only JavaBean getters — a view-model bean written as a simple DTO with public fields and no getters resolves every ${bean.field} reference to null/missing, silently (a template using !'default' fallbacks renders the default with no error at all; a bare ${bean.field} with no fallback throws at render time). This bridge avoids that trap by default: the bridge-built Configuration sets a DefaultObjectWrapper with exposeFields=true (DEFAULT_EXPOSE_FIELDS), so public-field DTOs render their field values out of the box — matching how Juneau's own marshalling is comfortable with public-field beans elsewhere. Use exposeFields(false) to restore FreeMarker's getter-only behavior, or objectWrapper(...) for full control (e.g. a BeansWrapper with custom exposure rules). Both knobs only affect the bridge-default Configuration; a user-supplied @Bean Configuration is used as-is.

Template suffix:

FreeMarker's API does not expose a configuration-level resolver suffix — the literal template name is what cfg.getTemplate(...) sees. The templateSuffix(...) builder knob fills the gap: when non-blank, the bridge appends the configured suffix to template names that don't already end with it (idempotent), so callers can write FreemarkerView.of("hello") or request /freemarker/hello and have it resolve to hello.ftlh on the classpath.

.ftl vs .ftlh:

FreeMarker auto-selects HTML escaping by file extension: .ftlh templates emit HTML-escaped output; .ftl templates emit raw output. For HTML responses, prefer .ftlh so a future attribute-binding change can't introduce an XSS regression.

Multiple base paths:

A host with /templates/ and /admin/templates/ template trees registers two FreemarkerMixin beans, each mounted via a subclass declaring its own op-level @RestGet(path=...) override and its own basePath. See the FreemarkerMixin_MockRest_Test test in juneau-integration-tests for the canonical pattern.

OpenAPI surface:

The greedy /* handler is not API-meaningful and is excluded from generated Swagger / OpenAPI specs via @OpSwagger(ignore=true).

See Also:
Since:
10.0.0
  • Field Details

  • Constructor Details

  • Method Details

    • registerSubtype

      public static void registerSubtype(Class<? extends FreemarkerMixin> type)
      Registers a subclass so FreemarkerViewRenderer also finds a bean declared with that subclass as its @Bean return type.

      The bean store is keyed by declared type, so without this a @Bean public ConsoleFreemarkerMixin freemarker() was not found and the renderer silently used a plain mixin. Subclasses call this from a static initializer.

      Example:

      public class MyMixin extends FreemarkerMixin { static { FreemarkerMixin.registerSubtype(MyMixin.class); } }

      Parameters:
      type - The subclass. Must not be null.
    • registeredSubtypes

      public static Set<Class<? extends FreemarkerMixin>> registeredSubtypes()
      Returns:
      The registered subclasses, in registration order.
    • create

      Creates a new builder.
      Returns:
      A new builder.
    • getBasePath

      public String getBasePath()
      Returns the base path under which template resources are resolved.
      Returns:
      The base path. Never null.
    • getTemplateSuffix

      Returns the configured template-name suffix.
      Returns:
      The template suffix. Never null.
    • isCacheTemplates

      public boolean isCacheTemplates()
      Returns the template-cache flag.
      Returns:
      The cache flag.
    • isExposeFields

      public boolean isExposeFields()
      Returns the field-exposure flag applied to the bridge-default object wrapper.
      Returns:
      The field-exposure flag. Ignored once getObjectWrapper() returns non-null.
    • getObjectWrapper

      public freemarker.template.ObjectWrapper getObjectWrapper()
      Returns the user-supplied ObjectWrapper override, if any.
      Returns:
      The object wrapper override, or null if the bridge builds its own default.
    • applyTemplateSuffix

      Appends getTemplateSuffix() to name if not already present (idempotent).
      Parameters:
      name - The template name. Must not be null.
      Returns:
      The template name with the configured suffix appended (if applicable).
    • resolveConfiguration

      public freemarker.template.Configuration resolveConfiguration(RestRequest req)
      Resolves the active Configuration via the shared FreemarkerDispatcher worker.
      Parameters:
      req - The current REST request.
      Returns:
      The active FreeMarker configuration. Never null.
    • render

      public void render(@Path("/*") String path, RestRequest req, RestResponse res) throws IOException, BasicHttpException
      [GET /freemarker/*] — render a raw template through the FreeMarker engine.

      The @Path("/*") String path captures the multi-segment trailing remainder (e.g. a request for /freemarker/about.ftlh matches the mount with path = "about.ftlh"). Delegates to the shared FreemarkerDispatcher worker (path-traversal hardening, suffix application, configuration resolution, and render).

      Missing template surfaces as the engine's own TemplateNotFoundException (HTTP 500 from the renderer's catch-all); missing engine surfaces as FreemarkerViewRenderer.NO_ENGINE_DIAGNOSTIC.

      Parameters:
      path - The trailing path segment after /freemarker/ (the template name relative to the configured base path; configured suffix appended if missing).
      req - The current REST request.
      res - The current REST response.
      Throws:
      IOException - If the underlying response writer fails.
      BasicHttpException - On boundary violation (403), missing engine (500), or render failure (500).