Core Framework User's Guide
Draft

This guide is a work in progress and will be revised regularly until the official launch of Oorian.

1. Introduction

What is Oorian?

Oorian is a server-side Java web framework that lets you build interactive web applications entirely in Java. There is no JavaScript to write, no HTML templates to maintain, and no frontend build tools to configure. You write Java, and Oorian handles everything else.

With Oorian, HTML elements are Java objects. A <div> is a Div, a <button> is a Button, and CSS properties are set through type-safe methods like setDisplay(Display.FLEX). Your IDE's autocomplete, refactoring, and debugging tools work naturally with every part of your UI.

Each page can be independently configured for AJAX, Server-Sent Events (SSE), or WebSocket communication. A simple contact form might use AJAX, while a real-time dashboard uses SSE, and a collaborative editor uses WebSocket — all in the same application.

The Wrapper Approach

Most server-side Java frameworks try to build their own UI components from scratch. They end up with mediocre grids, basic charts, and limited editors that can't compete with the specialized JavaScript libraries the rest of the industry uses.

Oorian takes a different approach: wrap the best, don't reinvent the rest. Need a data grid? Use AG Grid, the industry standard. Need charts? Use ECharts or Chart.js. Need a rich text editor? Use CKEditor, TinyMCE, or Quill. Each of these libraries is maintained by a dedicated team that does nothing but perfect that one component.

Oorian provides Java wrapper libraries for over 50 JavaScript UI libraries. Every wrapper follows the same conventions for configuration, events, and data binding. You learn the Oorian way once, and every library feels familiar.

No Vendor Lock-In

With frameworks like Vaadin or ZK, you're locked into their proprietary component library. If their grid doesn't meet your needs, you're stuck. If they discontinue a component, you have to rewrite your code.

Oorian gives you freedom. Start with one charting library, switch to another later. Use different grid libraries for different use cases in the same application. Your code stays clean because every wrapper follows the same patterns, and switching libraries is a matter of changing imports, not rewriting your application.

Production Proven

Oorian is not a theoretical framework or a weekend project. It has powered iGradePlus, a commercial SaaS application, in production for over 10 years. iGradePlus serves schools and institutions with hundreds of interactive pages — all built with Oorian, all in pure Java, with zero custom JavaScript.

This decade of real-world use has shaped every design decision in the framework. Features exist because they solved real problems. APIs are designed because they proved themselves in production. Oorian is built by practitioners, for practitioners.

Who is Oorian For?

Oorian is designed for Java developers who want to:

  • Build web applications without learning JavaScript frameworks — If you know Java, you know enough to build with Oorian
  • Use enterprise-grade UI components — Access the best JavaScript libraries through clean Java APIs
  • Leverage their existing Java skills — Object-oriented design, inheritance, polymorphism, and standard debugging all apply directly
  • Avoid frontend build complexity — No webpack, no npm, no node_modules, no transpilers
  • Choose real-time communication per page — AJAX, SSE, or WebSocket, configured with a single line of code
New to Oorian?

If you're just getting started, we recommend the Quick Start Guide first. It walks you through creating your first Oorian application in minutes. Then come back here for the full picture.

2. Platform Independence

The Java servlet ecosystem is split between two package namespaces: the legacy javax.servlet API (Java EE / Servlet 4.0 and earlier) and the modern jakarta.servlet API (Jakarta EE / Servlet 5.0+). Frameworks that import servlet classes directly are locked to one namespace or the other, forcing painful migration when upgrading servers.

Oorian avoids this problem entirely. The framework's core library — OorianCommonLib — contains all the framework logic but has zero servlet imports. Instead, it defines its own set of interfaces that mirror the servlet and WebSocket APIs. Thin adapter libraries implement these interfaces for each platform:

Adapter Library Target Platform
OorianJakartaLib Jakarta EE (Tomcat 10+, Jetty 12+, WildFly 27+)
OorianJ2eeLib Java EE (Tomcat 9, Jetty 11, older servers)

You include the adapter library that matches your servlet container. Your application code works with Oorian's interfaces and is unaffected by the underlying servlet namespace.

Oorian Interfaces

The following interfaces and classes replace direct use of servlet and WebSocket types throughout the framework:

Oorian Type Replaces Purpose
OorianHttpRequest HttpServletRequest Access request headers, parameters, cookies, and upload parts
OorianHttpResponse HttpServletResponse Set response headers, status codes, content type, and write output
OorianHttpSession HttpSession Session attributes, ID, creation time, and lifecycle management
AppContext ServletContext Application-wide context, resource loading, and MIME types
OorianCookie Cookie Cookie properties (name, value, path, domain, max-age, flags)
OorianUploadPart Part Multipart file upload data (content type, input stream, headers)
WebsocketConnection Session (WebSocket) Send text messages and close WebSocket connections
SseConnection SSE output stream Send Server-Sent Events data and manage connection state
OorianSessionListener HttpSessionListener Respond to session creation and destruction events

What This Means for Your Code

As an application developer, you rarely interact with these interfaces directly. Oorian's higher-level APIs — HtmlPage, OorianSession, event listeners — handle everything internally. When you do need low-level access, you use Oorian's types rather than servlet types:

Java
@Override
protected boolean initializePage()
{
    // getHttpResponse() returns OorianHttpResponse, not HttpServletResponse
    getHttpResponse().setHeader("X-Custom-Header", "value");

    // getCookie() returns OorianCookie, not javax/jakarta Cookie
    OorianCookie pref = getCookie("user-pref");

    // OorianSession wraps OorianHttpSession with typed accessors
    OorianSession session = getSession();
    String username = session.getAttributeAsString("username");

    return true;
}
Servlet Container Migration

To move an Oorian application from Tomcat 9 (javax) to Tomcat 10 (jakarta), swap OorianJ2eeLib for OorianJakartaLib in your project dependencies. No application code changes are required.

3. Getting Started

Prerequisites

Before building with Oorian, ensure you have the following:

  • Java 17 or later — Oorian requires JDK 17+
  • A servlet container — Apache Tomcat 10+, Jetty 11+, or use Oorian's built-in LaunchPad embedded server
  • An IDE — Any Java IDE works. NetBeans, IntelliJ IDEA, and Eclipse all provide full autocomplete and debugging support

Project Structure

An Oorian web application follows the standard Java web application structure:

BASH
YourApp/
├── src/
│   └── com/yourcompany/
│       ├── MyApplication.java        # Application entry point
│       └── pages/
│           ├── HomePage.java          # @Page("/")
│           └── AboutPage.java         # @Page("/about")
├── web/
│   ├── WEB-INF/
│   │   └── web.xml
│   └── oorian.min.js                  # Oorian client-side runtime
└── build.xml

The src/ directory contains your Java source code, organized by package. The web/ directory contains static resources and the deployment descriptor. Oorian discovers your pages automatically through package scanning — no additional configuration files are needed.

oorian.min.js

The oorian.min.js file is Oorian's client-side runtime. It handles all communication between the browser and the server — AJAX requests, SSE connections, WebSocket management, DOM updates, event dispatching, and file uploads. Every Oorian application must include this file in the web/ directory.

The file is included with the Oorian library download and in all Quick Start projects. Copy it into your project's web/ directory. Oorian automatically adds a <script> tag referencing it when rendering pages — you do not need to include it manually.

Static Web Resources

Any HTML, CSS, JavaScript, image, or other static files your application needs should be placed in the web/ directory or its subdirectories. The servlet container serves these files directly to the browser without any processing by Oorian. Organize them into subdirectories to keep your project tidy:

BASH
web/
├── css/
│   └── custom-styles.css
├── js/
│   └── third-party-library.js
├── images/
│   ├── logo.png
│   └── banner.jpg
├── WEB-INF/
│   └── web.xml
└── oorian.min.js

The Application Class

Every Oorian application starts with an Application subclass annotated with @WebListener. The Application class is the entry point for your application — it manages initialization, page discovery, and application-wide configuration.

When the servlet container starts, it calls your Application's initialize() method. When it shuts down, it calls destroy():

Java
@WebListener
public class MyApplication extends Application
{
    @Override
    public void initialize(ServletContext context)
    {
        // Register packages for page discovery
        registerPackage("com.myapp");

        // Set application-wide communication mode
        setDefaultCommunicationMode(CommunicationMode.WEBSOCKET);

        // Enable CSRF protection
        setCsrfProtectionEnabled(true);

        // Configure upload directory
        setTempFilePath("/tmp/myapp/uploads");
    }

    @Override
    public void destroy(ServletContext context)
    {
        // Release database connections, close resources
    }
}

Page Discovery

You must call registerPackage() for the top-level package of any packages that contain your page classes. Oorian scans the registered package and all of its sub-packages for classes annotated with @Page. This is a requirement — your application will not discover or serve any pages without it. You can register multiple packages if your application is spread across several.

Application-Wide Configuration

The Application class provides several configuration options that apply to all pages:

Method Description
setDefaultCommunicationMode(mode) Sets the default communication mode for all pages (AJAX_ONLY, AJAX_WITH_SSE, or WEBSOCKET)
setDefaultPollInterval(ms) Sets the polling interval for AJAX_ONLY mode (in milliseconds)
setCsrfProtectionEnabled(boolean) Enables or disables CSRF token validation application-wide
setTempFilePath(path) Sets the directory for temporary file uploads
Default Communication Mode

The default communication mode is WEBSOCKET. Individual pages can override this with setCommunicationMode(). See Communication Modes for details.

Your First Page

Create a page by extending HtmlPage and annotating it with @Page:

Java
@Page("/")
public class HomePage extends HtmlPage
{
    @Override
    protected void createHead(Head head)
    {
        head.setTitle("My First Oorian App");
    }

    @Override
    protected void createBody(Body body)
    {
        H1 heading = new H1();
        heading.setText("Hello, Oorian!");
        body.addElement(heading);

        P paragraph = new P();
        paragraph.setText("Welcome to your first Oorian application.");
        body.addElement(paragraph);
    }
}

That's it. No HTML templates, no JavaScript, no build tools. Deploy to your servlet container and visit http://localhost:8080/ to see your page.

Quick Start Guide

For a more detailed walkthrough of creating your first application, including IDE setup and deployment, see the Quick Start Guide.

4. Configuration

Oorian provides a lightweight configuration system based on classpath property files. Load application settings from oorian.properties, access them through typed getters, and use environment profiles to manage different configurations for development, staging, and production.

Property File Loading

Place a file named oorian.properties on the classpath (typically in src/resources/ or WEB-INF/classes/). The framework loads it automatically before your Application.initialize() method is called.

oorian.properties
app.name=My Application
app.version=1.0.0
db.host=localhost
db.port=5432
db.pool.size=10
feature.notifications=true

Access configuration values through OorianConfig.get(), which provides typed getters with default values:

Java
OorianConfig config = OorianConfig.get();

// String values
String appName = config.getString("app.name");
String dbHost = config.getString("db.host", "localhost");  // with default

// Numeric values
int port = config.getInt("db.port", 5432);
long timeout = config.getLong("request.timeout", 30000);
double rate = config.getDouble("billing.rate", 0.05);

// Boolean values
boolean notifications = config.getBoolean("feature.notifications", false);

// Check if a key exists
if (config.containsKey("smtp.host"))
{
    // configure email...
}

The configuration is also available through your Application class via Application.getConfig(). If the property file is missing, the framework logs a warning and continues normally — all getters return their default values. Invalid numeric formats also log a warning and return the default rather than throwing.

Environment Profiles

Environment profiles let you maintain different configurations for development, staging, and production. Each profile has its own property file that overrides values from the base oorian.properties.

The active profile is resolved in this order:

  1. Programmatic: setProfile("dev") in initialize()
  2. System property: -Doorian.profile=dev
  3. Environment variable: OORIAN_PROFILE=dev
  4. Default: "prod"
Prefer System Properties or Environment Variables

Using -Doorian.profile=dev or the OORIAN_PROFILE environment variable is the recommended approach for setting the active profile. A setProfile("dev") call in initialize() can easily be overlooked during code review and accidentally deployed to production.

Create profile-specific files alongside the base configuration:

File Structure
src/resources/
    oorian.properties           # Base settings (always loaded)
    oorian-dev.properties       # Development overrides
    oorian-staging.properties   # Staging overrides
    oorian-prod.properties      # Production overrides

Loading happens in two phases: the base file loads before initialize(), and the profile-specific file loads after the profile is resolved. Profile values override base values with the same key.

oorian-dev.properties
db.host=localhost
db.port=5432
db.pool.size=2
oorian.mode=dev
oorian-prod.properties
db.host=db.mycompany.com
db.port=5432
db.pool.size=20

Complete Example

Using configuration and profiles together in your application:

Java
@WebListener
public class MyApp extends Application
{
    @Override
    protected void initialize(ServletContext context)
    {
        registerPackage("com.myapp");

        // Optionally set the profile programmatically
        // (otherwise resolved from system property or env var)
        setProfile("dev");

        // Access configuration values
        OorianConfig config = getConfig();

        // Check the active profile
        String profile = config.getActiveProfile();  // "dev"

        if (config.isProfile("dev"))
        {
            setDevMode(true);
        }

        // Configure services using config values
        int poolSize = config.getInt("db.pool.size", 10);
        String dbHost = config.getString("db.host", "localhost");
    }
}

5. Pages and Routing

Pages are the fundamental building blocks of an Oorian application. Each page is a Java class that generates a complete HTML document, and the @Page annotation maps it to a URL.

The @Page Annotation

The @Page annotation defines the URL path for your page. Oorian supports static paths, dynamic parameters, and multiple parameters in a single path:

Java
// Static path
@Page("/about")
public class AboutPage extends HtmlPage { ... }

// Dynamic parameter
@Page("/users/{id}")
public class UserProfilePage extends HtmlPage { ... }

// Multiple parameters
@Page("/blog/{year}/{slug}")
public class BlogPostPage extends HtmlPage { ... }

// Nested path
@Page("/admin/settings")
public class AdminSettingsPage extends HtmlPage { ... }

Dynamic parameters are enclosed in curly braces ({id}, {slug}) and can be retrieved in your page code using the Parameters object.

Page Structure

Every Oorian page extends HtmlPage and overrides two methods to build the HTML document:

Java
@Page("/products")
public class ProductsPage extends HtmlPage
{
    @Override
    protected void createHead(Head head)
    {
        // Configure the <head> element
        head.setTitle("Our Products");
        head.setDescription("Browse our product catalog.");
        head.addCssLink("/css/products.css");
    }

    @Override
    protected void createBody(Body body)
    {
        // Build the page content
        H1 title = new H1();
        title.setText("Products");
        body.addElement(title);

        // Add product cards, tables, etc.
    }
}

Including Static Resources

Use createHead() to link external CSS and JavaScript files that are stored in your project's web/ directory. The Head class provides addCssLink() for stylesheets and addJavaScript() for script files:

Java
@Override
protected void createHead(Head head)
{
    head.setTitle("My Page");
    head.addCssLink("/css/custom-styles.css");
    head.addJavaScript("/js/third-party-library.js");
}

Page Lifecycle

When a user requests a page, Oorian processes it through a defined lifecycle:

  1. initializePage() — Called first. Perform authentication checks, load data, or redirect. Return false to stop page rendering.
  2. createHead() — Build the <head> section: title, meta tags, stylesheets, and scripts.
  3. createBody() — Build the <body> section: your page content.
  4. Page renders — Oorian serializes the element tree to HTML and sends it to the browser.

The initializePage() method is optional but powerful. Use it for authentication checks, data loading, or conditional redirects:

Java
@Page("/dashboard")
public class DashboardPage extends HtmlPage
{
    @Override
    protected boolean initializePage()
    {
        User user = (User) getSession().getAttribute("user");

        if (user == null)
        {
            navigateTo("/login");
            return false;  // Stop rendering
        }

        return true;  // Continue to createHead/createBody
    }

    @Override
    protected void createHead(Head head) { ... }

    @Override
    protected void createBody(Body body) { ... }
}

Updating the UI

In SSE and WebSocket communication modes, changes you make to elements are not automatically sent to the browser. Call sendUpdate() to push all pending changes as a single batched response:

Java
@Override
public void onEvent(MouseClickedEvent event)
{
    statusLabel.setText("Processing...");
    progressBar.setWidth("50%");
    submitBtn.setDisabled(true);

    sendUpdate();
}
When is sendUpdate() required?

In AJAX mode, updates are sent automatically at the end of each request-response cycle. In SSE and WebSocket modes, you must call sendUpdate() explicitly to push changes to the browser. It is safe to call in AJAX mode — it simply has no effect.

URL Path Parameters

For dynamic paths like /users/{id}, use getUrlParameters() to access the path segments by name or by index:

Java
@Page("/users/{id}/orders/{orderId}")
public class OrderDetailPage extends HtmlPage
{
    @Override
    protected void createBody(Body body)
    {
        UrlParameters urlParams = getUrlParameters();

        // By name: /users/42/orders/99
        Integer userId = urlParams.getParameterAsInt("id");        // 42
        Long orderId = urlParams.getParameterAsLong("orderId");  // 99

        // By index (order of appearance)
        String first = urlParams.getParameter(0);   // "42"
        String second = urlParams.getParameter(1);  // "99"
    }
}

UrlParameters provides the following typed accessors, each available by name or by index:

Method Return Type
getParameter(name) / getParameter(index) String
getParameterAsInt(name) / getParameterAsInt(index) Integer
getParameterAsLong(name) / getParameterAsLong(index) Long
getParameterAsDouble(name) / getParameterAsDouble(index) Double

Query Parameters

Query string parameters (e.g., /products?page=2&sort=name) are accessed directly on the page using methods inherited from HttpFile:

Java
@Page("/products")
public class ProductsPage extends HtmlPage
{
    @Override
    protected void createBody(Body body)
    {
        // /products?page=2&sort=name&featured=true
        String sort = getParameter("sort");              // "name"
        Integer page = getParameterAsInt("page");       // 2
        Boolean featured = getParameterAsBoolean("featured"); // true (from RequestParameters)

        // Check if a parameter exists
        if (hasParameter("sort"))
        {
            // apply sorting
        }
    }
}

These methods return null when the parameter is missing or cannot be parsed. For multi-valued parameters (e.g., ?ids=1&ids=2&ids=3), use getParameterValues(name) to get all values as a String[].

The full set of query parameter accessors:

Method Return Type Description
getParameter(name) String Returns the raw string value
getParameterAsInt(name) Integer Parses the value as an integer
getParameterAsLong(name) Long Parses the value as a long
getParameterAsFloat(name) Float Parses the value as a float
getParameterValues(name) String[] Returns all values for multi-valued parameters
hasParameter(name) boolean Checks if the parameter exists
Query Parameters vs. URL Path Parameters

URL path parameters (getUrlParameters()) come from @Page placeholders like {id} and are always present when the route matches. Query parameters (getParameter()) come from the ?key=value portion of the URL and are optional. Both are available in initializePage(), createHead(), and createBody().

Navigation

Oorian provides several methods for navigating between pages:

Java
// Navigate to another page
navigateTo("/dashboard");

// Navigate with parameters
navigateTo("/users/42?tab=settings");

// Create a URL for use in links
String url = createUrl("/products");

// Go back to the previous page
navigateBack();

// Open a URL in a new browser tab
openInNewWindow("/reports/annual");
Navigation and Page Lifecycle

When you call navigateTo() inside initializePage(), remember to return false to prevent the current page from rendering. In event handlers, navigateTo() triggers the navigation after the current handler completes.

Cache Control

Oorian's CacheControl class provides a fluent builder for constructing HTTP Cache-Control headers. Use it to control how browsers and CDNs cache your application's responses.

Static Factory Methods

For common caching strategies, use the built-in factory methods:

Java
// Long-lived static resources (CSS, JS, images with versioned URLs)
// Produces: "public, max-age=31536000, immutable"
CacheControl cache = CacheControl.staticResources();

// Prevent caching entirely (sensitive data)
// Produces: "no-store"
CacheControl cache = CacheControl.preventCaching();

// Allow caching but require revalidation before each use
// Produces: "no-cache"
CacheControl cache = CacheControl.revalidate();

Custom Cache Policies

Build custom policies with the fluent API. All methods return this for method chaining:

Java
// Public cache with 1-hour max age, must revalidate when stale
CacheControl cache = new CacheControl()
    .setPublic()
    .setMaxAge(3600)
    .mustRevalidate();
// Produces: "public, max-age=3600, must-revalidate"

// Private cache for user-specific content
CacheControl cache = new CacheControl()
    .setPrivate()
    .setMaxAge(300)
    .noTransform();

// CDN-friendly: short browser cache, longer CDN cache
CacheControl cache = new CacheControl()
    .setPublic()
    .setMaxAge(60)
    .setSMaxAge(3600)
    .setStaleWhileRevalidate(86400);

The available directives:

Method Directive Description
setPublic() public Response may be cached by any cache, including shared caches (CDNs, proxies)
setPrivate() private Response is for a single user; must not be stored by shared caches
noCache() no-cache Caches must revalidate with origin server before each use
noStore() no-store No cache should store the response (strongest directive)
noTransform() no-transform Proxies must not modify the response body or headers
mustRevalidate() must-revalidate Must not use stale responses without revalidation
proxyRevalidate() proxy-revalidate Like must-revalidate but only for shared caches
immutable() immutable Response body will not change; prevents conditional revalidation
setMaxAge(int) max-age Maximum seconds before response is stale
setSMaxAge(int) s-maxage Maximum age for shared caches, overrides max-age
setStaleWhileRevalidate(int) stale-while-revalidate Serve stale while revalidating in background
setStaleIfError(int) stale-if-error Serve stale if origin returns a 5xx error

Using CacheControl with CssFile

CssFile subclasses have built-in cache control support. Cached-mode CssFiles (created with a name) default to long-lived caching, while dynamic-mode CssFiles default to no-cache. You can override the default with setCacheControl():

Java
@Css("/css/app.css")
public class AppStyles extends CssFile
{
    public AppStyles()
    {
        super("app-styles");  // Cached mode: defaults to public, max-age=31536000, immutable

        // Override with a custom cache policy if needed
        setCacheControl(new CacheControl().setPublic().setMaxAge(86400).mustRevalidate());
    }

    @Override
    protected CssStyleSheet createStyleSheet()
    {
        // ...
    }
}

Setting Cache Headers on Pages

For page responses, set the Cache-Control header on the OorianHttpResponse in initializePage():

Java
@Override
protected boolean initializePage()
{
    // Prevent caching of sensitive pages
    getHttpResponse().setHeader("Cache-Control",
        CacheControl.preventCaching().toString());

    return true;
}
CssFile Cache Defaults

Cached-mode CssFile instances (those created with a name in the constructor) automatically use public, max-age=31536000, immutable with ETag support. Dynamic-mode instances (no-arg constructor) default to no-cache. Use setCacheControl() only when you need to override these defaults.

6. HTML Elements

Oorian provides Java classes for every standard HTML element. Instead of writing HTML markup, you construct a tree of Java objects that Oorian renders to HTML.

Element Hierarchy

All Oorian elements inherit from a common base:

  • Element — The abstract base class for all HTML elements. Provides methods for setting attributes, adding child elements, and inline styling.
  • HtmlElement — Extends Element with convenience methods for content building: addText(), addLineOfText(), addLineBreak(), addParagraph(), addSpacer(), and setFocus().
  • StyledElement — Extends HtmlElement with CSS class management (addClass(), removeClass()) and type-safe setter methods for virtually every CSS property (setBackgroundColor(), setDisplay(), setPadding(), setBorder(), etc.). Use this as your base when building custom components.

Head and Body

Every page receives a Head and Body instance through the createHead() and createBody() lifecycle methods. These are the two top-level elements of every HTML document.

Head

The Head element manages document metadata, external resources, and SEO tags. It provides convenience methods so you never need to create Meta, Title, or Link elements manually:

Category Method Description
Title setTitle(String) Sets the document title (browser tab, bookmarks, search results)
SEO setDescription(String) Sets the meta description (shown in search results)
SEO setKeywords(String) Sets the meta keywords (comma-separated)
SEO setRobots(String) Controls crawler behavior (e.g., "index,follow", "noindex")
Resources addCssLink(String) Links an external CSS stylesheet
Resources addCssLink(String, boolean) Links a stylesheet with optional cache-busting timestamp
Resources addJavaScript(String) Links an external JavaScript file
Resources addJavaScript(String, boolean) Links a script with optional cache-busting timestamp
Resources addCssStyleSheet(CssStyleSheet) Embeds an inline <style> block
Meta addMeta(String, String) Adds a custom meta tag (attribute, value)
Social setOpenGraph(title, description, imageUrl, pageUrl) Sets Open Graph tags for Facebook and LinkedIn previews
Social setTwitterCard(card, title, description, imageUrl) Sets Twitter Card tags for Twitter/X previews
Security setContentSecurityPolicy(ContentSecurityPolicy) Sets a Content Security Policy via meta tag
Java
@Override
protected void createHead(Head head)
{
    // Document metadata
    head.setTitle("Products | My Store");
    head.setDescription("Browse our full product catalog.");
    head.setRobots("index,follow");

    // External resources
    head.addCssLink("/css/products.css");
    head.addJavaScript("/js/charts.js");

    // Cache-busting: appends a timestamp query parameter
    head.addCssLink("/css/theme.css", true);

    // Social media previews
    head.setOpenGraph("Products", "Browse our catalog", "/images/og.png", "/products");

    // Custom meta tag
    head.addMeta("viewport", "width=device-width, initial-scale=1.0");
}

Body

The Body element is the container for all visible page content. It extends StyledElement, so it supports CSS class management with addClass() and removeClass() in addition to all standard inline styling methods. Use createBody() to add your page's element tree:

Java
@Override
protected void createBody(Body body)
{
    body.addClass("dark-theme");
    body.setPadding(0);
    body.setMargin(0);

    Div content = new Div();
    body.addElement(content);
}

Common Elements

Class HTML Tag Description
Div <div> Generic block container
Span <span> Generic inline container
P <p> Paragraph
H1H6 <h1><h6> Heading levels 1 through 6
Anchor <a> Hyperlink
Button <button> Clickable button
Image <img> Image
Table <table> Data table
Ul <ul> Unordered list
Ol <ol> Ordered list
Li <li> List item
Section <section> Thematic section
Nav <nav> Navigation section
Header <header> Header section
Footer <footer> Footer section

Creating and Configuring Elements

Create an element by instantiating its class, configure it with setter methods, and add it to a parent:

Java
// Create elements
Div container = new Div();
H2 title = new H2();
P description = new P();

// Configure text content
title.setText("Welcome");
description.setText("Thanks for visiting our site.");

// Configure styling
container.setMaxWidth(800);
container.setMargin("0 auto");
container.setPadding(40);

// Build the tree
container.addElement(title);
container.addElement(description);
body.addElement(container);

Attributes

Set standard and custom HTML attributes using addAttribute() and setId():

Java
Div card = new Div();
card.setId("main-card");
card.addAttribute("role", "article");
card.addAttribute("data-category", "featured");

Anchor link = new Anchor();
link.setHref("/about");
link.setText("About Us");
link.addAttribute("target", "_blank");

Image logo = new Image();
logo.setSrc("/images/logo.png");
logo.setAlt("Company Logo");
logo.setWidth(200);

Input Elements

Oorian provides type-specific subclasses for each HTML input type:

Class Input Type Description
TextInput text Single-line text field
PasswordInput password Password field (masked input)
EmailInput email Email address field
Checkbox checkbox Checkbox toggle
RadioButton radio Radio button (one of many)
Select <select> Dropdown selection
TextArea <textarea> Multi-line text field
FileInput file File upload field
HiddenInput hidden Hidden form value
Java
// Text input with placeholder
TextInput nameField = new TextInput();
nameField.setName("fullName");
nameField.setPlaceholder("Enter your name");

// Select dropdown
Select countrySelect = new Select();
countrySelect.setName("country");
countrySelect.addOption(new Option("us", "United States"));
countrySelect.addOption(new Option("ca", "Canada"));
countrySelect.addOption(new Option("uk", "United Kingdom"));

// Checkbox
Checkbox agreeBox = new Checkbox();
agreeBox.setName("agree");

Child Element Management

Oorian provides methods for managing an element's children:

Java
Div container = new Div();

// Add child elements
container.addElement(header);
container.addElement(content);
container.addElement(footer);

// Remove a specific child
container.removeElement(content);

// Remove all children
container.removeAllElements();

// Check for children
boolean hasChildren = container.hasElements();
int count = container.getElementCount();

// Insert raw HTML content
RawHtml html = new RawHtml("<em>Emphasized text</em>");
container.addElement(html);

7. CSS Styling

Oorian works with standard CSS files just like any web application. You can link to your own stylesheets or external ones from the page head using addCssLink():

Java
@Override
protected void createHead(Head head)
{
    // Link to a local CSS file
    head.addCssLink("/css/common.css");

    // Link to an external stylesheet
    head.addCssLink("https://fonts.googleapis.com/css2?family=Inter");
}

Beyond standard CSS files, Oorian also provides powerful tools for working with CSS directly in Java — inline style methods, CSS class management, programmatic stylesheets, and dynamic server-generated CSS files.

Internal CSS

Internal CSS is embedded directly in the page using an HTML <style> element. In Oorian, the Style class represents this element. You can pass it a raw CSS string for page-specific rules that need selectors, pseudo-classes, or pseudo-elements that inline styles cannot express:

Java
@Override
protected void createHead(Head head)
{
    super.createHead(head);

    Style style = new Style();
    style.setContent("background-color: #fffbcc; font-weight: bold;");
    head.addElement(style);
}

For more complex styles, you can use CssStyle to build the CSS string programmatically instead of writing it by hand:

Java
CssStyle defaultStyle = new CssStyle();
defaultStyle.setFontFamily("'Inter', sans-serif");
defaultStyle.setColor("#1f2937");
defaultStyle.setMargin(0);
defaultStyle.setPadding(0);
defaultStyle.setBackgroundColor("#f5f5f5");

Style style = new Style();
style.setContent(defaultStyle.getAttributesString());
head.addElement(style);
Keep internal styles small

Internal styles are ideal for page-specific CSS. For more complicated internal styles, use the CssStyleSheet class described later in this chapter.

Inline Style Methods

Every element has type-safe methods for the most common CSS properties. These methods translate directly to inline style attributes on the rendered HTML:

Java
Div card = new Div();

// Layout
card.setDisplay(Display.FLEX);
card.setFlexDirection(FlexDirection.COLUMN);
card.setAlignItems(AlignItems.CENTER);
card.setGap(16);

// Sizing
card.setWidth(300);
card.setMaxWidth("100%");
card.setPadding(24);
card.setMargin("0 auto");

// Appearance
card.setBackgroundColor(Color.WHITE);
card.setColor("#1f2937");
card.setBorderRadius(12);
card.setBoxShadow("0 2px 8px rgba(0, 0, 0, 0.1)");

// Position
card.setPosition(Position.RELATIVE);
card.setOverflow(Overflow.HIDDEN);

For CSS properties that don't have a dedicated method, use addStyleAttribute():

Java
// For properties without a dedicated method
card.addStyleAttribute("grid-template-columns", "repeat(3, 1fr)");
card.addStyleAttribute("aspect-ratio", "16 / 9");

Reusable Styles with CssStyle

The CssStyle class lets you define a set of CSS properties once and apply them to multiple elements. It has the same type-safe setter methods available on elements, but packaged as a standalone, reusable object:

Java
// Define a reusable style
CssStyle cardStyle = new CssStyle();
cardStyle.setBackgroundColor(Color.WHITE);
cardStyle.setPadding(24);
cardStyle.setBorderRadius(12);
cardStyle.setBoxShadow("0 2px 8px rgba(0, 0, 0, 0.1)");

// Apply to multiple elements
panel1.setStyle(cardStyle);
panel2.setStyle(cardStyle);
panel3.setStyle(cardStyle);

You can also merge additional properties into an element's existing style without replacing it:

Java
// Add properties on top of existing styles
CssStyle highlight = new CssStyle();
highlight.setBorder(2, BorderStyle.SOLID, "#4285f4");
highlight.setBackgroundColor("#e8f0fe");

panel1.addStyle(highlight);  // merges with existing style
CssStyle values are copied, not referenced

When a CssStyle is applied to an element via setStyle() or addStyle(), the element copies the attributes into its own internal style state. It does not hold a reference to the original CssStyle object. Changing the CssStyle after it has been applied will not affect the element.

CSS Units

The com.oorian.css.units package provides type-safe unit classes for CSS dimensions. When CSS values are computed dynamically, building strings like width + "px" or fontSize + "rem" gets tedious fast. Unit classes eliminate that boilerplate:

Java — without units
int columns = getColumnCount();
sidebar.setWidth((100 / columns) + "%");
sidebar.setMinWidth(sidebarMinPx + "px");
sidebar.setPadding(basePadding + "rem");
Java — with units
int columns = getColumnCount();
sidebar.setWidth(new Percent(100 / columns));
sidebar.setMinWidth(new Px(sidebarMinPx));
sidebar.setPadding(new Rem(basePadding));

All unit classes extend an abstract Units base class whose toString() method produces properly formatted CSS output. They work anywhere a CSS length value is accepted — on elements, in CssRule declarations, and in CssStyle objects:

Java
// On elements
card.setWidth(new Percent(100));
card.setMaxWidth(new Px(600));
card.setPadding(new Rem(1.5f));
card.setBorderRadius(new Em(0.5f));

// In CssRule declarations
ClassRule hero = new ClassRule("hero");
hero.setHeight(new Vh(100));
hero.setMinHeight(new Px(600));

// Character-based width for readable text
ElementRule article = new ElementRule("article");
article.setMaxWidth(new Ch(70));
Class CSS Unit Example
Px px (pixels) new Px(16)
Em em new Em(1.5f)
Rem rem (root em) new Rem(2)
Percent % new Percent(50)
Vh / Vw vh / vw (viewport) new Vh(100)
Vmin / Vmax vmin / vmax new Vmin(50)
Pt pt (points) new Pt(12)
Ch ch (character width) new Ch(40)
Ex ex (x-height) new Ex(2)
Cm / Mm / In / Pc Print units new Cm(2.5f)

The Color Class

The com.oorian.css.color package includes a full-featured Color class with color space conversions, manipulation, blending, and all 140 standard HTML color names as type-safe constants:

Java — named colors
card.setBackgroundColor(Color.ALICE_BLUE);
heading.setColor(Color.DARK_SLATE_GRAY);
badge.setBackgroundColor(Color.CORAL);
border.setBorderColor(Color.CORNFLOWER_BLUE);

A fine-grained grayscale palette (GRAY_01 through GRAY_34) is also available for precise control over neutral tones.

Creating Colors

Java
// From CSS hex string
Color brand = new Color("#2563eb");

// From RGB components (0-255)
Color salmon = new Color(250, 128, 114);

// From a single hex integer
Color amber = new Color(0xFFBF00);

// From HSB (hue 0-360, saturation 0-100, brightness 0-100)
Color vibrant = new Color(new Hsb(210, 80, 90));

// Copy constructor
Color brandCopy = new Color(brand);

Color Manipulation

Java
Color primary = new Color("#2563eb");

// Darker shade (50% toward black)
Color darkPrimary = primary.getShadeOf(50);

// Lighter tint (30% toward white)
Color lightPrimary = primary.getTintOf(30);

// Use intensity to pick readable text color
int intensity = primary.getIntensity();
String textColor = intensity > 179 ? "#1f2937" : "#ffffff";

Color Blending

Java
// Blend two colors with alpha values
Color blended = Color.blend(Color.BLUE, 0.6f, Color.RED, 0.4f);

// Blend a list of colors
List<Color> palette = List.of(Color.RED, Color.GREEN, Color.BLUE);
Color mixed = Color.blend(palette, 0.5f);

Output Formats

Java
Color c = Color.DODGER_BLUE;
String css = c.getCssString();           // "#1E90FF"
String hex = c.getHexString();           // "1e90ff"
String rgba = c.getCssString(0.75f);     // "rgba(30, 144, 255, 0.750000)"
int rgb = c.getRgb();                    // integer RGB value

Palettes and Random Colors

Java
// Standard palette of distinct colors
List<Color> chartColors = Color.getStandardColors();

// Grayscale palette with a specific number of shades
List<Color> grays = Color.getGrayScale(10);

// Random color from the standard palette
Color highlight = Color.getRandomColor();

CSS Class Management

Elements that extend StyledElement can dynamically add and remove CSS classes:

Java
Div alert = new Div();

// Add one or more classes (space-separated)
alert.addClass("alert alert-success");

// Remove a class
alert.removeClass("alert-success");

// Add a different class
alert.addClass("alert-error");

This is particularly useful in event handlers where you need to change an element's appearance in response to user actions.

Programmatic Stylesheets

For page-level styles, use CssStyleSheet with CSS rule classes to build stylesheets programmatically. This gives you the same type-safe methods available on elements, but applied to CSS selectors.

Oorian provides specialized rule classes for common selector types, so you don't have to write raw selector strings:

Class Selector Type Example
ClassRule Class selector new ClassRule("card").card
ElementRule Tag selector new ElementRule("body")body
IdRule ID selector new IdRule("header")#header
CssRule Any selector new CssRule(".card:hover").card:hover

Use the specific subclass when the selector is a simple class, element, or ID. Use CssRule directly for compound selectors, pseudo-classes, and pseudo-elements:

Java
CssStyleSheet css = new CssStyleSheet();

// Class selector: .card
ClassRule cardRule = new ClassRule("card");
cardRule.setBackgroundColor(Color.WHITE);
cardRule.setPadding(24);
cardRule.setBorderRadius(12);
cardRule.setBoxShadow("0 2px 8px rgba(0, 0, 0, 0.1)");
css.addRule(cardRule);

// Pseudo-class: .card:hover (use CssRule for compound selectors)
CssRule cardHover = new CssRule(".card:hover");
cardHover.setBoxShadow("0 4px 16px rgba(0, 0, 0, 0.15)");
css.addRule(cardHover);

// Add to page
head.addCssStyleSheet(css);

ElementRule and IdRule also accept multiple selectors to create grouped rules:

Java
// Grouped element selector: h1, h2, h3
ElementRule headings = new ElementRule("h1", "h2", "h3");
headings.setColor("#1f2937");
headings.setFontFamily("'Inter', sans-serif");
css.addRule(headings);

CSS Combinators Without the Cryptic Syntax

Rather than memorizing CSS combinator symbols like >, +, and ~, Oorian provides descriptive methods on rule objects:

CSS Syntax Oorian Method Meaning
.parent .child addDescendant() Any nested element at any depth
.parent > .child addChild() Direct children only
.item + .item addAdjacentSibling() Immediately following sibling
.item ~ .item addGeneralSibling() All following siblings
Java — descendant selector
ClassRule navbar = new ClassRule("navbar");
navbar.setDisplay(Display.FLEX);
navbar.setAlignItems(AlignItems.CENTER);
navbar.setPadding(16);

ElementRule navLink = new ElementRule("a");
navLink.setColor(Color.WHITE);
navLink.setTextDecoration("none");
navLink.setPadding(8, 16);
navbar.addDescendant(navLink);

sheet.addRule(navbar);
Java — child selector
ClassRule menu = new ClassRule("menu");
ElementRule menuItem = new ElementRule("li");
menuItem.setListStyleType("none");
menuItem.setPadding(8);
menu.addChild(menuItem);

sheet.addRule(menu);
Java — adjacent sibling selector
ClassRule cardRule = new ClassRule("card");
cardRule.setBorderRadius(8);
ClassRule nextCard = new ClassRule("card");
nextCard.setMarginTop(16);
cardRule.addAdjacentSibling(nextCard);

sheet.addRule(cardRule);

Every combinator method returns the rule for chaining, so you can compose as deeply as you need.

Pseudo-Classes and Pseudo-Elements

CSS pseudo-classes like :hover, :focus, and :first-child are another area where raw CSS syntax is easy to get wrong. Oorian lets you attach pseudo-class styles directly to the rule object, so all styles for an element live together in one place:

Java — link pseudo-classes
ElementRule link = new ElementRule("a");
link.setColor("#2563eb");
link.setTextDecoration("none");

CssStyle hoverStyle = new CssStyle();
hoverStyle.setColor("#1d4ed8");
hoverStyle.setTextDecoration("underline");
link.setHoverStyle(hoverStyle);

CssStyle focusStyle = new CssStyle();
focusStyle.setOutline("2px solid #2563eb");
focusStyle.setOutlineOffset("2px");
link.setFocusStyle(focusStyle);

link.setVisitedStyle("color: #7c3aed;");
link.setActiveStyle("color: #dc2626;");

sheet.addRule(link);

This generates the corresponding CSS selectors (a, a:hover, a:focus, etc.) automatically.

Form Validation States

Java
ElementRule input = new ElementRule("input");
input.setBorder(1, BorderStyle.SOLID, Color.LIGHT_GRAY);
input.setBorderRadius(4);
input.setPadding(8, 12);

input.setFocusStyle("border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2);");
input.setInvalidStyle("border-color: #dc2626;");
input.setValidStyle("border-color: #16a34a;");
input.setDisabledStyle("background-color: #f3f4f6; cursor: not-allowed;");
input.setRequiredStyle("border-left: 3px solid #2563eb;");

sheet.addRule(input);

Structural Selectors

Java
ElementRule tableRow = new ElementRule("tr");
tableRow.setNthChild(2, "background-color: #f8fafc;");
tableRow.setFirstChildStyle("font-weight: bold;");
tableRow.setLastChildStyle("border-bottom: none;");

sheet.addRule(tableRow);

Pseudo-Elements

Oorian supports ::before, ::after, ::first-letter, ::first-line, ::marker, and ::selection through dedicated methods:

Java
CssRule label = new CssRule("label.required");
label.setFontWeight(FontWeight.WEIGHT_600);

CssStyle afterStyle = new CssStyle();
afterStyle.addStyleAttribute("content", "' *'");
afterStyle.setColor(Color.RED);
label.setAfterStyle(afterStyle);

sheet.addRule(label);

ElementRule listItem = new ElementRule("li");

CssStyle markerStyle = new CssStyle();
markerStyle.setColor(Color.CORNFLOWER_BLUE);
markerStyle.setFontWeight(FontWeight.WEIGHT_700);
listItem.setMarkerStyle(markerStyle);

CssStyle selStyle = new CssStyle();
selStyle.setBackgroundColor("#2563eb");
selStyle.setColor(Color.WHITE);
listItem.setSelectionStyle(selStyle);

sheet.addRule(listItem);

The key benefit is that all the styles for an element — its base styles, pseudo-classes, and pseudo-elements — live together on a single rule object rather than scattered across multiple CSS selectors.

Responsive Design with Media Queries

Use CssMediaQuery to add responsive styles:

Java
CssStyleSheet css = new CssStyleSheet();

// Default: 3-column grid
ClassRule grid = new ClassRule("product-grid");
grid.setDisplay(Display.GRID);
grid.addStyleAttribute("grid-template-columns", "repeat(3, 1fr)");
grid.setGap(24);
css.addRule(grid);

// Tablet: 2-column grid
CssMediaQuery tablet = new CssMediaQuery("(max-width: 1024px)");
ClassRule tabletGrid = new ClassRule("product-grid");
tabletGrid.addStyleAttribute("grid-template-columns", "repeat(2, 1fr)");
tablet.addRule(tabletGrid);
css.addMediaQuery(tablet);

// Mobile: 1-column
CssMediaQuery mobile = new CssMediaQuery("(max-width: 768px)");
ClassRule mobileGrid = new ClassRule("product-grid");
mobileGrid.addStyleAttribute("grid-template-columns", "1fr");
mobile.addRule(mobileGrid);
css.addMediaQuery(mobile);

CSS Transitions

Oorian provides a high-level API for CSS transitions through the com.oorian.css.transitions package. Instead of writing raw CSS transition strings, you use pre-built Transition subclasses that handle property names, timing functions, and state management automatically.

Every transition supports a speed scale from 0 (slowest, 1200ms) to 10 (fastest, 50ms), with 5 (300ms) as the default. You can also override with an exact duration in seconds.

Adding Transitions to Elements

Transitions are added to any StyledElement with addTransition(), then triggered with transitionForward() and reversed with transitionReverse():

Java
Div panel = new Div();
panel.setText("Hello, world!");

// Add a fade transition
panel.addTransition(new Fade().setSpeed(7));

// Later, in an event handler:
panel.transitionForward();   // Fades out
panel.transitionReverse();   // Fades back in

You can add multiple transitions to the same element. When the transition plays forward, Oorian snapshots the element's current CSS values so that transitionReverse() can restore them exactly.

Transition Presets

Class Effect Key Options
Fade Animates opacity (fades to transparent by default) setTo(float) — target opacity (0.0–1.0)
Slide Slides element in a direction using CSS transform setDirection(Direction), setDistance(String), setWithFade(boolean)
Grow Scales element up or down using CSS transform setScale(float), setOrigin(Origin, Origin), setWithFade(boolean)
Collapse Collapses element to zero height or width (accordion effect) setAxis(Axis) — VERTICAL or HORIZONTAL
ColorShift Animates background color to a target color setTo(String) — any CSS color value

Transition Examples

Slide
// Slide left with fade
Slide slide = new Slide();
slide.setDirection(Direction.LEFT);
slide.setDistance("50px");
panel.addTransition(slide);

// Slide without fading
Slide slideOnly = new Slide();
slideOnly.setDirection(Direction.UP);
slideOnly.setWithFade(false);
panel.addTransition(slideOnly);
Grow
// Scale up from top-left corner
Grow grow = new Grow();
grow.setScale(2.0f);
grow.setOrigin(Origin.LEFT, Origin.TOP);
panel.addTransition(grow);
Collapse (Accordion)
// Vertical collapse — great for accordion panels
panel.addTransition(new Collapse());
panel.transitionForward();   // Collapses to zero height
panel.transitionReverse();   // Expands back

// Horizontal collapse for sidebars
sidebar.addTransition(new Collapse().setAxis(Axis.HORIZONTAL));
ColorShift
// Flash a warning color, then return to original
panel.addTransition(new ColorShift().setTo("#ff4444"));
panel.transitionForward();
panel.transitionReverse();

Speed and Duration

All transitions use a 0–10 speed scale. You can also set an exact duration:

Java
// Using the speed scale (0 = slowest, 10 = fastest)
new Fade().setSpeed(3);   // 500ms
new Fade().setSpeed(7);   // 150ms

// Using exact duration (overrides speed scale)
new Fade().setDuration(0.5f);  // Exactly 500ms
Speed Duration Speed Duration
01200ms6200ms
11000ms7150ms
2800ms8100ms
3500ms975ms
4400ms1050ms
5 (default)300ms

Fluent Chaining

All transition configuration methods return this, enabling fluent chaining:

Java
panel.addTransition(new Fade().setSpeed(7).setTo(0.5f))
     .addTransition(new ColorShift().setTo("#e0e0e0"))
     .transitionForward();
Transform conflict

Slide and Grow both use the CSS transform property. They cannot be combined on the same element — the last one applied will overwrite the other. Use them on separate elements instead.

Transition Sequences

For choreographed multi-element transitions, use TransitionSequence and TransitionGroup. A sequence plays steps one after another with automatic delay calculation. A group plays multiple element/transition pairs simultaneously.

Sequential transitions
// Each step plays after the previous one finishes
TransitionSequence seq = new TransitionSequence();
seq.add(header, new Fade());
seq.add(content, new Slide().setDirection(Direction.DOWN));
seq.add(footer, new Fade().setSpeed(8));

seq.play();      // Plays header → content → footer
seq.reverse();   // Plays footer → content → header
Grouped transitions (simultaneous)
// Group plays all entries at the same time
TransitionGroup group = new TransitionGroup();
group.add(header, new Fade());
group.add(sidebar, new Slide().setDirection(Direction.LEFT));

// Mix groups and individual steps in a sequence
TransitionSequence seq = new TransitionSequence();
seq.add(group);                                     // Step 1: header + sidebar together
seq.add(content, new Fade().setSpeed(8));           // Step 2: content fades after group
seq.play();

StyledElement Transition Methods

Method Description
addTransition(Transition) Adds a transition to the element (can add multiple)
clearTransitions() Removes all transitions and clears saved state
transitionForward() Snapshots current state and plays all transitions forward
transitionForward(long delayMs) Plays forward after the specified delay
transitionReverse() Restores original CSS values (plays transitions in reverse)
transitionReverse(long delayMs) Plays reverse after the specified delay
isTransitionForwardActive() Returns true if the forward transition is currently active

CSS Animations

For looping or fire-and-forget effects, Oorian provides CSS keyframe animations through the com.oorian.css.animations package. Unlike transitions (which interpolate between two states), animations use @keyframes to define multi-step sequences that can loop indefinitely.

Applying an Animation

Call animate() on any StyledElement. Oorian automatically injects the required @keyframes into the page's <head> (deduplicated by keyframe ID) and sets all animation CSS properties on the element:

Java
// Spin a loading icon
Span spinner = new Span();
spinner.addClass("fa-solid fa-spinner");
spinner.animate(new Spin());

// Stop the animation later
spinner.stopAnimation();

Animation Presets

Class Effect Default Speed
Spin Continuous 360° rotation (loading spinners) 0 (1200ms)
SpinPulse Stepped 360° rotation in discrete 45° jumps 1 (1000ms)
Beat Scales up and back down (heartbeat effect) 1 (1000ms)
BeatFade Combines scaling and opacity for a pulsing effect 1 (1000ms)
Bounce Squash-and-stretch bouncing with secondary bounces 1 (1000ms)
Fade Fades opacity in and out (looping, unlike transitions.Fade) 1 (1000ms)
Flip Flips element around an axis (X, Y, or Z) 1 (1000ms)
Shake Rapid back-and-forth rotation with decreasing intensity 1 (1000ms)

Animation Configuration

All animations share common configuration options inherited from the Animation base class:

Java
// Speed scale (same 0–10 scale as transitions)
new Spin().setSpeed(5);

// Exact duration override
new Beat().setDuration(0.8f);

// Iteration count (-1 = infinite, which is the default)
new Shake().setIterationCount(3);   // Shake 3 times then stop

// Animation direction
new Spin().setDirection(AnimationDirection.REVERSE);
new Bounce().setDirection(AnimationDirection.ALTERNATE);

// Fill mode (what state the element rests in after animation)
new Fade().setFillMode(AnimationFillMode.FORWARDS);

// Delay before animation starts
new Beat().setDelay(500);  // 500ms delay

Animation Preset Examples

Beat and BeatFade
// Heartbeat effect on a notification icon
icon.animate(new Beat());
icon.animate(new Beat().setScale(1.5f));  // Bigger pulse

// Pulsing effect combining scale and opacity
icon.animate(new BeatFade().setScale(1.3f).setMinOpacity(0.2f));
Bounce and Shake
// Bouncing effect (great for attention-grabbing)
arrow.animate(new Bounce());
arrow.animate(new Bounce().setHeight("-1em"));  // Higher bounce

// Shake 3 times (e.g., on validation error)
field.animate(new Shake().setIterationCount(3));
Flip
// Flip around Y axis (default)
card.animate(new Flip());

// Flip around X axis with custom angle
card.animate(new Flip().setFlipAxis("x").setAngle("-90deg"));

Animations on Unattached Elements

The animate() method injects @keyframes into the page's <head> at call time. If the element is not yet attached to the page tree, you must inject the keyframes manually in createHead():

Java
@Override
protected void createHead(Head head)
{
    // Pre-inject keyframes for animations used on elements built later
    Spin spin = new Spin();
    head.addCssStyleSheet(spin.getStyleSheet());
}

StyledElement Animation Methods

Method Description
animate(Animation) Injects @keyframes into <head> and applies all animation properties
stopAnimation() Removes all animation CSS properties, stopping the animation immediately
Transitions vs. Animations

Use transitions for one-shot state changes that you want to reverse (e.g., showing/hiding panels, accordion collapse, hover effects). Use animations for looping or fire-and-forget effects (e.g., loading spinners, attention pulses, notification badges). Both share the same speed scale and fluent API style.

CSS Transform Builder

The Transform class provides a fluent, type-safe API for composing CSS transform property values. Multiple transform functions are accumulated and rendered in order. Each function name is unique — calling the same function again replaces the previous value rather than duplicating it.

Individual Transform Methods

Every StyledElement has convenience methods for common transform functions. These methods compose with each other — calling setTranslateX() followed by setRotate() produces transform: translateX(...) rotate(...):

Java
Div box = new Div();

// Individual transform functions — these compose automatically
box.setTranslateX(50);          // pixels
box.setTranslateY(20);          // pixels
box.setRotate(45);              // degrees
box.setScale(1.5f);
box.setSkewX(10f);              // degrees
box.setSkewY(5f);               // degrees
// Result: transform: translateX(50px) translateY(20px) rotate(45deg) scale(1.5) skewX(10deg) skewY(5deg)

// Reset all transforms
box.setTransform("none");

The Transform Builder

For one-shot composition, use the Transform builder class. This is useful when you want to set the entire transform value at once, such as in a CssRule:

Java
// Fluent builder — produces a single transform string
box.setTransform(new Transform()
    .translateX(50)
    .rotate(45)
    .scale(1.2f));
// Result: transform: translateX(50.0px) rotate(45.0deg) scale(1.2)

// Works on CssRule too
ClassRule rule = new ClassRule("tilted-card");
rule.setTransform(new Transform()
    .rotate(-3)
    .scale(1.05f));

Transform API Reference

Builder Method CSS Output
translateX(int), translateX(Units), translateX(String) translateX(50px)
translateY(int), translateY(Units), translateY(String) translateY(20px)
translate(int, int), translate(Units, Units), translate(String, String) translate(50px, 20px)
rotate(float), rotate(String) rotate(45deg)
rotateX(float), rotateX(String) rotateX(45deg)
scale(float), scale(float, float) scale(1.5), scale(2.0, 0.5)
skewX(float), skewX(String) skewX(10deg)
perspective(int), perspective(Units), perspective(String) perspective(500px)

CSS Filter Builder

The Filter class provides a fluent API for composing CSS filter property values. Like Transform, multiple filter functions are accumulated and each function name is unique.

Individual Filter Methods

Java
Div photo = new Div();

// Individual filter functions — these compose automatically
photo.setBlur("5px");
photo.setBrightness(1.2f);
photo.setContrast(1.1f);
photo.setGrayscale(0.5f);
photo.setHueRotate(90);       // degrees
photo.setInvert(1.0f);
photo.setSepia(0.8f);
photo.setSaturate(2.0f);

// Reset all filters
photo.setFilter("none");

The Filter Builder

Java
// Fluent builder
photo.setFilter(new Filter()
    .blur(3)
    .brightness(1.2f)
    .contrast(1.1f));
// Result: filter: blur(3px) brightness(1.2) contrast(1.1)

// On a CssRule
ClassRule muted = new ClassRule("muted-image");
muted.setFilter(new Filter()
    .grayscale(0.7f)
    .brightness(0.9f));

Backdrop Filter

The backdrop-filter property applies graphical effects to the area behind an element. This is commonly used for frosted-glass overlays. It uses the same Filter builder:

Java
// Frosted glass overlay
Div overlay = new Div();
overlay.setBackgroundColor("rgba(255, 255, 255, 0.2)");
overlay.setBackdropFilter(new Filter().blur(10));

// String form
overlay.setBackdropFilter("blur(10px) saturate(1.5)");

// On a CssRule
ClassRule glass = new ClassRule("glass-panel");
glass.setBackdropFilter(new Filter()
    .blur(12)
    .brightness(1.1f)
    .saturate(1.3f));

Filter API Reference

Builder Method CSS Output
blur(int), blur(Units), blur(String) blur(5px)
brightness(float) brightness(1.2)
contrast(float) contrast(1.1)
dropShadow(int, int, int, Color), dropShadow(Units, Units, Units, Color), dropShadow(String...) drop-shadow(2px 2px 4px #000000)
grayscale(float) grayscale(0.5) — 0.0 (none) to 1.0 (full)
hueRotate(float), hueRotate(String) hue-rotate(90deg)
invert(float) invert(1.0)
sepia(float) sepia(0.8)
saturate(float) saturate(2.0)

Gradient Builder

The Gradient class provides a fluent API for composing CSS gradient values. Gradients are used as CSS image values for properties like background-image. The builder supports linear, radial, and conic gradients with color stops.

Java
// Linear gradient (horizontal)
box.setBackgroundImage(Gradient.linear("to right")
    .addStop("#3b82f6", 0)
    .addStop("#8b5cf6", 100));
// Result: background-image: linear-gradient(to right, #3b82f6 0%, #8b5cf6 100%)

// Linear gradient with angle
box.setBackgroundImage(Gradient.linear(45)
    .addStop("#000")
    .addStop("#fff"));

// Radial gradient
box.setBackgroundImage(Gradient.radial("circle")
    .addStop("#fbbf24")
    .addStop("#ef4444"));

// Conic gradient (color wheel)
box.setBackgroundImage(Gradient.conic()
    .addStop("red")
    .addStop("yellow")
    .addStop("green")
    .addStop("blue")
    .addStop("red"));

// Repeating gradient (striped pattern)
box.setBackgroundImage(Gradient.linear(45)
    .addStop("#e0e7ff", "0px")
    .addStop("#e0e7ff", "10px")
    .addStop("#c7d2fe", "10px")
    .addStop("#c7d2fe", "20px")
    .repeating());

Color Object Stops

Color stops accept both CSS strings and Oorian Color objects:

Java
Gradient.linear()
    .addStop(Color.RED)
    .addStop(Color.BLUE, 100);

Gradient API Reference

Factory Method CSS Output
Gradient.linear() linear-gradient(...) — default direction (top to bottom)
Gradient.linear("to right") linear-gradient(to right, ...)
Gradient.linear(45) linear-gradient(45deg, ...)
Gradient.radial() radial-gradient(...)
Gradient.radial("circle") radial-gradient(circle, ...)
Gradient.conic() conic-gradient(...)
Gradient.conic(int), Gradient.conic(String) conic-gradient(from 45deg, ...)
Instance Method Description
addStop(String color), addStop(Color color) Add a color stop at auto position
addStop(String, int), addStop(Color, int) Add a color stop at a percentage position (e.g., 50 for 50%)
addStop(String, String), addStop(Color, String) Add a color stop at a specific position (e.g., "10px")
repeating() Convert to repeating variant (e.g., repeating-linear-gradient)

BoxShadow Builder

The BoxShadow class provides a fluent API for composing CSS box-shadow and text-shadow values. CSS allows multiple comma-separated shadows, and this builder makes it easy to layer them.

Java
// Single drop shadow with int pixels and Color
card.setBoxShadow(new BoxShadow()
    .add(0, 4, 6, -1, new Color("rgba(0,0,0,0.1)")));

// Multiple layered shadows (Material Design elevation)
card.setBoxShadow(new BoxShadow()
    .add(0, 10, 15, -3, new Color("rgba(0,0,0,0.1)"))
    .add(0, 4, 6, -4, new Color("rgba(0,0,0,0.1)")));
// Result: box-shadow: 0px 10px 15px -3px rgba(0,0,0,0.1), 0px 4px 6px -4px rgba(0,0,0,0.1)

// Inset shadow (inner shadow)
input.setBoxShadow(new BoxShadow()
    .addInset(0, 2, 4, 0, new Color("rgba(0,0,0,0.1)")));

// Combining outer and inset shadows (String form for rgba colors)
card.setBoxShadow(new BoxShadow()
    .add("0", "4px", "12px", "rgba(0,0,0,0.15)")
    .addInset("0", "1px", "0", "0", "rgba(255,255,255,0.1)"));

Text Shadow

The same BoxShadow class works for text-shadow. The only difference is that text-shadow does not support the spread parameter or inset keyword:

Java
// Neon glow text effect
heading.setTextShadow(new BoxShadow()
    .add(0, 0, 10, new Color("rgba(59,130,246,0.8)"))
    .add(0, 0, 20, new Color("rgba(59,130,246,0.4)")));

// Simple text shadow
heading.setTextShadow(new BoxShadow()
    .add(1, 1, 2, new Color("rgba(0,0,0,0.3)")));

Color Object Overloads

Integer overloads accept pixel values and Color objects:

Java
// Using int values (pixels) and Color objects
card.setBoxShadow(new BoxShadow()
    .add(2, 2, 8, 0, Color.BLACK));
// Result: box-shadow: 2px 2px 8px 0px #000000

card.setBoxShadow(new BoxShadow()
    .addInset(0, 0, 10, 5, Color.BLACK));

BoxShadow API Reference

Method Description
add(int, int), add(String, String) Shadow with offsets only
add(int, int, int), add(String, String, String) Shadow with blur radius
add(int, int, int, Color), add(String, String, String, String) Shadow with blur and color
add(int, int, int, int, Color), add(Units, Units, Units, Units, Color), add(String...) Full shadow specification
addInset(int, int, int, int, Color), addInset(Units...), addInset(String...) Same overloads as add(), prefixed with inset

ClipPath Builder

The ClipPath class provides a fluent API for the CSS clip-path property, which clips an element to a shape. Unlike transforms and filters (which compose multiple functions), a clip-path has a single active shape — each method replaces the previous value.

Java
// Circle
avatar.setClipPath(new ClipPath().circle("50%"));

// Circle at a specific position
box.setClipPath(new ClipPath().circleAt("50%", "25% 25%"));

// Ellipse
banner.setClipPath(new ClipPath().ellipse("50%", "40%"));

// Triangle (polygon)
arrow.setClipPath(new ClipPath().polygon("50% 0%", "100% 100%", "0% 100%"));

// Hexagon
badge.setClipPath(new ClipPath().polygon(
    "25% 0%", "75% 0%", "100% 50%",
    "75% 100%", "25% 100%", "0% 50%"));

// Rounded inset (like border-radius but as a clip)
card.setClipPath(new ClipPath().insetRound(10, 15));

// SVG path
shape.setClipPath(new ClipPath().path("M0,0 L100,0 L100,100 Z"));

// Reference an SVG clipPath element
box.setClipPath(new ClipPath().url("#myClipShape"));

// String form for any clip-path value
box.setClipPath("circle(75%)");

ClipPath API Reference

Method CSS Output
circle(int), circle(Units), circle(String) circle(50%)
circleAt(int, String), circleAt(Units, String), circleAt(String, String) circle(50% at 25% 25%)
ellipse(int, int), ellipse(Units, Units), ellipse(String, String) ellipse(50% 40%)
ellipseAt(int, int, String), ellipseAt(Units, Units, String), ellipseAt(String, String, String) ellipse(50% 40% at center)
inset(int), inset(int, int, int, int), inset(Units...), inset(String) inset(10px 20px 30px 40px)
insetRound(int, int), insetRound(String, String) inset(10px round 15px)
polygon(points...) polygon(50% 0%, 100% 100%, 0% 100%)
path(svgPath) path('M0,0 L100,0 ...')
url(url) url(#clipId)
Builder availability and overloads

All CSS builder classes (Transform, Filter, Gradient, BoxShadow, ClipPath) work on elements, CssRule, and CssStyleSheet. Each builder also has a toString() method, so you can use them anywhere a CSS string is expected.

Methods that accept CSS length values provide three-tier overloads: int (pixels), Units (e.g., new Px(50), new Percent(25), new Em(1.5f)), and String (raw CSS). Methods that accept angles provide float (degrees) and String overloads. Shadow methods additionally accept Color objects.

Reusable Stylesheets with CssFile

For styles shared across multiple pages, extend CssFile to serve CSS as a proper file with browser caching support.

Unlike static CSS files, a CssFile builds its stylesheet programmatically in Java, so it can generate CSS dynamically from any data source — database records, user preferences, configuration, or session state. The result is served as a standard CSS file with proper Content-Type headers and configurable caching. This combination of type-safe programmatic CSS building, per-user dynamic generation, and built-in cache management is unique to Oorian — no other Java framework or CSS-in-JS library offers all of these capabilities as a single, cohesive feature.

Java
@Css("/css/common.css")
public class CommonStyles extends CssFile
{
    public CommonStyles()
    {
        super("common-styles");  // Cache key
    }

    @Override
    protected CssStyleSheet createStyleSheet()
    {
        CssStyleSheet css = new CssStyleSheet();

        ElementRule body = new ElementRule("body");
        body.setFontFamily("'Inter', sans-serif");
        body.setColor("#1f2937");
        body.setMargin(0);
        css.addRule(body);

        // Add more shared rules...

        return css;
    }
}

Then reference it from any page:

Java
@Override
protected void createHead(Head head)
{
    head.addCssLink("/css/common.css");
}

Dynamic Stylesheets

Because CssFile builds its CSS programmatically in Java, it can generate stylesheets dynamically based on any data source — database records, user preferences, configuration files, or session state. This is a powerful capability that static CSS files cannot match.

For example, you can build a theme stylesheet that reads each user's color preferences from a database:

Java
@Css("/css/user-theme.css")
public class UserThemeStyles extends CssFile
{
    public UserThemeStyles()
    {
        super();  // Dynamic mode — no caching (content varies per user)
    }

    @Override
    protected CssStyleSheet createStyleSheet()
    {
        // Load the current user's theme settings from the database
        UserPreferences prefs = UserPreferences.loadForCurrentUser();

        CssStyleSheet css = new CssStyleSheet();

        // Apply user's chosen colors
        CssRule root = new CssRule(":root");
        root.addStyleAttribute("--primary-color", prefs.getPrimaryColor());
        root.addStyleAttribute("--accent-color", prefs.getAccentColor());
        root.addStyleAttribute("--font-family", prefs.getFontFamily());
        css.addRule(root);

        // Apply user's font size preference
        ElementRule body = new ElementRule("body");
        body.setFontSize(prefs.getFontSize() + "px");
        css.addRule(body);

        // Dark mode if user has it enabled
        if (prefs.isDarkMode())
        {
            ElementRule darkBody = new ElementRule("body");
            darkBody.setBackgroundColor("#1a1a2e");
            darkBody.setColor("#e0e0e0");
            css.addRule(darkBody);
        }

        return css;
    }
}
Cached vs. Dynamic CssFile

Use the cached constructor (super("name")) for shared styles that are the same for all users — these are built once and served from memory. Use the dynamic constructor (super()) for user-specific styles that need to be rebuilt on each request, such as themes driven by database settings.

Prefer built-in style methods

Always use dedicated methods like setDisplay(), setPosition(), and setBackgroundColor() when available. Only fall back to addStyleAttribute() for CSS properties that don't have a dedicated method. Built-in methods provide type safety through enums and prevent property name typos.

8. Container and Layout Components

Oorian provides a set of layout components that handle common layout patterns without requiring manual CSS. These components use CSS Flexbox and Grid internally but expose simple, semantic Java APIs.

Container

Container provides centered, width-constrained content. It applies a maximum width and horizontal auto-margins to keep content from stretching too wide on large screens:

Java
// Default container (LG - 1024px max-width)
Container container = new Container();
container.addElement(content);

// Small container for narrow content like articles
Container narrow = new Container(Container.Size.SM);

// Extra large container for wide layouts
Container wide = new Container(Container.Size.XL);

// Custom max-width
Container custom = new Container("900px");

Preset sizes follow common responsive breakpoints:

Size Max Width
Container.Size.SM 640px
Container.Size.MD 768px
Container.Size.LG 1024px (default)
Container.Size.XL 1280px
Container.Size.XXL 1536px
Container.Size.FLUID 100% (no constraint)

RegionLayout

RegionLayout provides a five-region layout familiar to Java Swing developers. It divides the page into north, south, east, west, and center regions. The center region expands to fill all available space:

STRUCTURE
+--------------------------------------+
|               NORTH                  |
+--------+-----------------+-----------+
|        |                 |           |
|  WEST  |     CENTER      |   EAST    |
|        |                 |           |
+--------+-----------------+-----------+
|               SOUTH                  |
+--------------------------------------+
Java
// Classic application layout
RegionLayout layout = new RegionLayout();
layout.north(header)
      .west(navigation)
      .center(mainContent)
      .east(sidebar)
      .south(footer);

// Configure region sizes
layout.setWestWidth(280);
layout.setEastWidth(300);
layout.setNorthHeight(64);
layout.setSouthHeight(48);

// Make the layout fill the entire viewport
layout.fillViewport();

body.addElement(layout);

All regions are optional. A simple header-content-footer layout only uses three:

Java
RegionLayout layout = new RegionLayout();
layout.north(header)
      .center(content)
      .south(footer);

HStack and VStack

HStack arranges children horizontally (left to right), and VStack arranges children vertically (top to bottom). They use CSS Flexbox internally and provide simple controls for spacing and alignment:

Java
// Horizontal button group with 8px gap
HStack buttonGroup = new HStack(8);
buttonGroup.addElement(new Button("Save"));
buttonGroup.addElement(new Button("Cancel"));

// Vertical form layout with 16px spacing
VStack formLayout = new VStack(16);
formLayout.addElement(nameField);
formLayout.addElement(emailField);
formLayout.addElement(buttonGroup);

// Navigation bar with space between items
HStack navbar = new HStack();
navbar.spaceBetween();
navbar.addElement(logo);
navbar.addElement(menuItems);
navbar.addElement(userProfile);

FlowStack

FlowStack arranges child elements in a wrapping flow layout, similar to how words wrap in a paragraph. Elements flow left to right and wrap to the next line when there isn't enough horizontal space:

Java
// Default flow with uniform spacing
FlowStack tags = new FlowStack(8);
tags.addElement(createTag("Java"));
tags.addElement(createTag("HTML"));
tags.addElement(createTag("CSS"));
tags.addElement(createTag("WebSocket"));

// Separate horizontal and vertical spacing
FlowStack gallery = new FlowStack(16, 12);

HPanel and VPanel

HPanel arranges elements into a single-row table with multiple cells. VPanel arranges elements into a single-column table with multiple rows. Both provide fine-grained control over cell sizing and alignment:

Java
// Horizontal panel with three columns
HPanel toolbar = new HPanel(3);
toolbar.setCellWidth(0, "200px");
toolbar.setCellWidth(2, "200px");
toolbar.addElement(0, logo);
toolbar.addElement(1, searchBar);
toolbar.addElement(2, userMenu);
toolbar.setSpacing(10);

// Vertical panel with sized rows
VPanel sidebar = new VPanel(3);
sidebar.setCellHeight(0, "auto");
sidebar.setCellHeight(2, "40px");
sidebar.addElement(0, menuHeader);
sidebar.addElement(1, menuItems);
sidebar.addElement(2, footer);

Deck

Deck displays one child element at a time, hiding all others. It is ideal for tabbed interfaces, wizards, and any UI that switches between views:

Java
Deck deck = new Deck();
deck.addElement(overviewPanel);
deck.addElement(detailsPanel);
deck.addElement(settingsPanel);

// Show a specific panel
deck.show(0);              // By index
deck.show("details");     // By child ID
deck.show(settingsPanel); // By element reference

// Navigate sequentially
deck.showNext();
deck.showPrev();

// Query current state
int index = deck.getDisplayedIndex();
Element current = deck.getDisplayedElement();

GlassPane

GlassPane creates a layered container with a background tint layer and a foreground content layer. Use it for overlay effects, dimmed backgrounds behind modals, or hero sections with tinted background images:

Java
// Glass pane with 50% opacity black tint
GlassPane hero = new GlassPane(50, Color.BLACK);
hero.setBackgroundImage("/images/hero-bg.jpg");
hero.setBackgroundSize(BackgroundSize.COVER);
hero.addElement(heroContent);

AsyncPanel

AsyncPanel performs content building in a background thread, preventing slow operations from blocking page rendering. Extend it and override the modify() method to build content asynchronously:

Java
AsyncPanel report = new AsyncPanel()
{
    @Override
    protected void modify()
    {
        // Runs in a background thread
        List<Record> data = database.fetchReport();

        for (Record record : data)
        {
            addElement(new ReportRow(record));
        }

        sendUpdate();
    }
};

Grid and GridItem

Grid provides a simplified API for CSS Grid Layout. Use it for two-dimensional layouts with rows and columns. GridItem wraps content to control spanning and placement within the grid:

Java
// Three equal columns with 24px gap
Grid grid = new Grid(3, 24);
grid.addElement(card1);
grid.addElement(card2);
grid.addElement(card3);

// Responsive: auto-fit columns with 250px minimum width
Grid responsive = new Grid();
responsive.autoFit(250);
responsive.setGap(16);

// GridItem spanning multiple columns
GridItem wide = new GridItem(banner);
wide.spanColumns(2);
grid.addElement(wide);

AutoLayout

AutoLayout is a content-aware layout that adjusts based on the selected mode: FLOW (items wrap naturally), EQUAL (items sized equally), STACK (vertical stacking), or INLINE (inline without wrapping):

Java
AutoLayout layout = new AutoLayout(AutoLayout.Mode.EQUAL, 16);
layout.addElement(card1);
layout.addElement(card2);
layout.addElement(card3);

Center

Center uses flexbox to center all child elements both horizontally and vertically within its bounds:

Java
// Center content in the viewport
Center centered = new Center(loginForm);
centered.fillViewport();

// Horizontal-only or vertical-only centering
Center hCenter = Center.horizontal();
Center vCenter = Center.vertical();

SplitLayout

SplitLayout creates a two-pane layout with a fixed panel and a flexible main area. The fixed panel can be on any side:

Java
// Left sidebar (250px) with flexible main area
SplitLayout layout = new SplitLayout(250);
layout.setSidebar(navigation);
layout.setMain(content);

// Right sidebar or top/bottom panels
SplitLayout right = SplitLayout.rightSidebar(300);
SplitLayout top = SplitLayout.topPanel(64);

TabbedLayout

TabbedLayout provides a tabbed interface where clicking a tab header switches the visible content panel. Tabs can be horizontal or vertical:

Java
TabbedLayout tabs = new TabbedLayout();
tabs.addTab("Overview", overviewPanel);
tabs.addTab("Details", detailsPanel);
tabs.addTab("Settings", settingsPanel);
tabs.selectTab(0);

// Vertical tabs
TabbedLayout vertical = TabbedLayout.vertical();

Accordion

Accordion provides vertically stacked expandable/collapsible panels. By default only one panel can be open at a time:

Java
Accordion faq = new Accordion();
faq.addItem("What is Oorian?", answerElement1);
faq.addItem("How does it work?", answerElement2);
faq.addItem("Is it free?", answerElement3);

// Allow multiple panels open simultaneously
faq.setMultiple(true);

WizardLayout

WizardLayout guides users through multi-step processes such as checkout flows, registration, or onboarding. It features a progress indicator, step content area, and navigation controls:

Java
WizardLayout wizard = new WizardLayout();
wizard.addStep("Account", accountForm);
wizard.addStep("Profile", profileForm);
wizard.addStep("Confirm", confirmPanel);

// Navigate between steps
wizard.nextStep();
wizard.previousStep();
wizard.showStep(0);

Drawer

Drawer is a slide-out panel for navigation or side content. It slides in from the left or right edge of the screen with a backdrop overlay:

Java
Drawer menu = new Drawer();
menu.addElement(navigationList);

// Open/close/toggle
menu.open();
menu.close();
menu.toggle();

// Right-side drawer
Drawer rightDrawer = Drawer.right();

Overlay

Overlay creates a full-screen layer that covers the viewport and centers its content. Use it for modals, dialogs, lightboxes, and loading screens:

Java
Overlay modal = new Overlay();
modal.setBackdrop("rgba(0,0,0,0.5)");
modal.addElement(dialogBox);

// Backdrop blur effect
modal.setBackdropBlur(4);

Utility Layout Elements

These small utility elements are used within other layouts:

Class Description
Spacer Expands to fill available space in a flex container, or provides a fixed-size gap
Divider A horizontal or vertical line for separating content (Divider.vertical())
Sticky Sticks its content to a position when scrolling (Sticky.top(64))
ScrollBox A scrollable container with configurable overflow (ScrollBox.horizontal())
AspectRatio Maintains a specific aspect ratio (new AspectRatio(16, 9))
PageSection An HTML <section> element with configurable vertical spacing

Content Layout Components

These components handle common content arrangement patterns:

Class Description
NavbarLayout A navigation bar with brand, menu, and action areas. Supports themes, sticky positioning, and responsive collapse.
MediaLayout The classic media object: an image or icon alongside descriptive content. Media can be positioned left or right.
CenteredContentLayout A narrow centered column optimized for reading, with preset widths like PROSE for comfortable line lengths.
SplitNavLayout A persistent navigation sidebar with switchable content panels. Commonly used for settings and admin pages.
MasterDetailLayout A two-pane master list/detail view for CRUD screens, email clients, and record browsers.

Page Layout Templates

Oorian includes a library of pre-built page layout templates for common application patterns. All extend the PageLayout base class and provide fillViewport() for full-screen layouts:

Class Use Case
AppShellLayout Application shell with header, collapsible sidebar, content, and footer. The backbone of admin panels and internal tools.
DashboardLayout Analytics dashboards with a stats bar and configurable widget grid (2, 3, or 4 columns).
DocsLayout Documentation pages with left navigation, main content, and right-side table of contents.
FormLayout Centered form card with title and action buttons. Includes presets: loginStyle(), registrationStyle().
SettingsLayout Settings pages with a category sidebar and content panel.
HeroLayout Landing pages with a prominent hero section (background image, overlay, gradient) and content sections below.
StoryLayout Product storytelling with alternating image/text sections for marketing pages.
FeatureGridLayout Product feature showcase with headline, feature card grid, and call-to-action section.
ErrorLayout Error pages with status code, message, and recovery actions. Includes presets: error404(), error500().
EmptyStateLayout Empty state displays (no data, no results) with icon, message, and action. Includes presets: noResults(), welcome().
LoadingLayout Loading states with spinner, message, and optional progress indicator. Can be used as an overlay.
ReferenceLayout API/class reference documentation with a summary index sidebar and scrollable detail area.
Java
// Application shell example
AppShellLayout shell = new AppShellLayout();
shell.header(navbar)
     .sidebar(menu)
     .content(mainContent)
     .footer(statusBar);
shell.setSidebarWidth(260);
shell.fillViewport();

// Error page example
ErrorLayout error = new ErrorLayout();
error.error404()
     .message("The page you're looking for doesn't exist.")
     .action(homeButton);

ComposableLayout

ComposableLayout is a slot-based CSS Grid layout for custom arrangements. You define named slots and assign content to them, then control positioning using CSS Grid template syntax:

Java
// Custom dashboard layout with named slots
ComposableLayout layout = new ComposableLayout();
layout.setGridTemplate(
    "'header header' auto " +
    "'sidebar main' 1fr " +
    "'footer footer' auto / 250px 1fr"
);

// Assign content to named slots
layout.slot("header", headerContent);
layout.slot("sidebar", sidebarContent);
layout.slot("main", mainContent);
layout.slot("footer", footerContent);

// Simple equal-column layout
ComposableLayout twoCol = new ComposableLayout(2);
twoCol.slot("col1", leftContent);
twoCol.slot("col2", rightContent);

CSS Flexbox and Grid

For layouts that don't fit the pre-built components, you can use Oorian's type-safe CSS methods to build Flexbox and Grid layouts directly on any element:

Java
// Flexbox layout
Div flexContainer = new Div();
flexContainer.setDisplay(Display.FLEX);
flexContainer.setFlexDirection(FlexDirection.ROW);
flexContainer.setJustifyContent(JustifyContent.SPACE_BETWEEN);
flexContainer.setAlignItems(AlignItems.CENTER);
flexContainer.setGap(16);

// Grid layout
Div gridContainer = new Div();
gridContainer.setDisplay(Display.GRID);
gridContainer.addStyleAttribute("grid-template-columns", "repeat(3, 1fr)");
gridContainer.setGap(24);

// Child element flex properties
Div sidebar = new Div();
sidebar.setFlex("0 0 250px");  // Fixed width sidebar

Div main = new Div();
main.setFlex("1");  // Flexible main content
Which Layout to Use?

Use HStack/VStack for simple horizontal or vertical arrangements. Use Grid for two-dimensional grid layouts. Use SplitLayout for a fixed sidebar with flexible main area. Use TabbedLayout for tabbed interfaces. Use AppShellLayout for full application shells with header, sidebar, and footer. Use the other Page Layout Templates for specific page types (dashboards, forms, errors, etc.). Use ComposableLayout for fully custom grid-based layouts. Fall back to direct CSS Flexbox/Grid for anything more specialized.

9. Communication Modes

One of Oorian's most distinctive features is its flexible communication model. Each page in your application can independently choose how it communicates with the browser. A simple form page can use basic AJAX, a dashboard can use Server-Sent Events for live updates, and a collaborative editor can use WebSocket for real-time bidirectional communication.

The Three Modes

Mode Direction Best For
AJAX_ONLY Client → Server (request/response) Simple forms, static content, pages with minimal interactivity
AJAX_WITH_SSE Client → Server (AJAX) + Server → Client (SSE) Dashboards, status monitors, live feeds, progress indicators
WEBSOCKET Bidirectional Real-time collaboration, chat, interactive applications

AJAX_ONLY

AJAX_ONLY is the simplest communication mode. The browser sends AJAX requests to the server when events occur (button clicks, form submissions, etc.), and the server responds with updates. The server cannot push updates to the browser on its own.

This mode is ideal for pages where all interaction is user-initiated: contact forms, login pages, settings panels, and pages that don't need live updates.

Java
@Page("/contact")
public class ContactPage extends HtmlPage
{
    public ContactPage()
    {
        setCommunicationMode(CommunicationMode.AJAX_ONLY);
    }
}

AJAX_WITH_SSE

AJAX_WITH_SSE combines AJAX for client-to-server communication with Server-Sent Events (SSE) for server-to-client push. The browser still sends events via AJAX, but the server can push updates at any time through an SSE channel.

This mode is perfect for dashboards and monitoring pages where data changes on the server and needs to be reflected in the browser without the user taking action. It's more efficient than WebSocket for one-way server push because SSE uses a simple HTTP connection.

Java
@Page("/dashboard")
public class DashboardPage extends HtmlPage
{
    public DashboardPage()
    {
        setCommunicationMode(CommunicationMode.AJAX_WITH_SSE);
    }
}

WEBSOCKET

WEBSOCKET provides full bidirectional communication through a persistent connection. Both the client and server can send messages at any time with minimal latency. This is the default mode and the most powerful option.

WebSocket is ideal for highly interactive applications, real-time collaboration, chat interfaces, and any page where low-latency bidirectional communication matters. The persistent connection eliminates the overhead of repeated HTTP requests.

Java
@Page("/editor")
public class EditorPage extends HtmlPage
{
    public EditorPage()
    {
        // WEBSOCKET is the default, but you can set it explicitly
        setCommunicationMode(CommunicationMode.WEBSOCKET);
    }
}

Configuring Per Page

Set the communication mode in your page's constructor using setCommunicationMode(). Each page operates independently, so you can mix modes freely within the same application:

Java
// Simple form - AJAX is sufficient
@Page("/settings")
public class SettingsPage extends HtmlPage
{
    public SettingsPage()
    {
        setCommunicationMode(CommunicationMode.AJAX_ONLY);
    }
}

// Live dashboard - needs server push
@Page("/metrics")
public class MetricsPage extends HtmlPage
{
    public MetricsPage()
    {
        setCommunicationMode(CommunicationMode.AJAX_WITH_SSE);
    }
}

// Collaborative workspace - full bidirectional
@Page("/workspace")
public class WorkspacePage extends HtmlPage
{
    public WorkspacePage()
    {
        setCommunicationMode(CommunicationMode.WEBSOCKET);
    }
}

You can also set a default for all pages in your Application class, and then override it on specific pages that need a different mode:

Java
@WebListener
public class MyApplication extends Application
{
    @Override
    public void initialize(ServletContext context)
    {
        registerPackage("com.myapp");

        // Default all pages to AJAX_WITH_SSE
        setDefaultCommunicationMode(CommunicationMode.AJAX_WITH_SSE);
    }
}
Default Mode

If you don't set a communication mode, Oorian defaults to WEBSOCKET. This is the most capable mode and works well for most interactive applications. Only switch to AJAX_ONLY or AJAX_WITH_SSE when you have a specific reason, such as simplifying infrastructure (no WebSocket proxy needed) or reducing resource usage on pages that don't need bidirectional communication.

Pushing Updates with sendUpdate()

When you modify elements in an event handler — changing text, toggling visibility, updating styles — the changes need to reach the browser. How this works depends on the communication mode:

Mode Behavior
AJAX_ONLY Updates are sent automatically at the end of the request-response cycle. No explicit call needed.
AJAX_WITH_SSE AJAX responses are automatic. For server-initiated pushes (e.g., from a worker thread), call sendUpdate().
WEBSOCKET You must call sendUpdate() to push changes to the browser.

The sendUpdate() method batches all pending element changes into a single message, minimizing network round-trips:

Java
@Override
public void onEvent(MouseClickedEvent event)
{
    statusLabel.setText("Processing...");
    progressBar.setWidth("50%");
    submitBtn.setDisabled(true);

    sendUpdate();  // Push all three changes in one batch
}
Tip

It is safe to call sendUpdate() in any communication mode. In AJAX_ONLY mode it simply has no effect, so you can include it unconditionally if your page might later switch modes.

10. Event Handling

Oorian uses a JDK-style event model that will feel immediately familiar if you have experience with Swing, JavaFX, or SWT. Elements fire events, and listeners handle them. There are no custom annotations, no magic strings, and no reflection tricks — just standard Java interfaces and method calls.

The event system supports six categories of events, each scoped to a different level of the application:

Event Type Scope Use Case
ClientEvent Element User interactions from the browser (clicks, keystrokes, form submits)
ServerEvent Element Server-side component communication with event bubbling
ExtServerEvent Element Server-side events with custom handler methods
PageEvent Page Communication between components on the same page
SessionEvent Session Communication across pages within a user's session
ApplicationEvent Application Broadcasting to all active sessions (system-wide notifications)

The Event Architecture

All events extend from a common Event<T> base class that uses generics to bind each event to its specific listener type. Every event type has a corresponding listener interface with an onEvent() method. This is the same observer pattern used throughout the Java ecosystem.

The general pattern is always the same:

  1. Implement the listener interface for the events you want to handle
  2. Register your listener with registerListener()
  3. Handle events in the onEvent() callback

Client Events

Client events originate in the browser and are dispatched to listeners on the server. When a user clicks a button, types in a field, or submits a form, Oorian sends the event to the server and routes it to the registered listener on the appropriate element.

Every client event carries a source reference (the element the listener was registered on) and a target reference (the element the user actually interacted with). Use getSource() to route events when multiple elements share a listener.

Category Events Listener
Mouse (click only) MouseClickedEvent MouseClickListener
Mouse (all) MouseClickedEvent, MouseDblClickedEvent, MouseDownEvent, MouseUpEvent, MouseMoveEvent, MouseOverEvent, MouseOutEvent MouseListener
Keyboard KeyDownEvent, KeyUpEvent KeyListener
Input InputChangeEvent, InputCompleteEvent InputListener
Form FormEvent FormListener
Focus FocusInEvent, FocusOutEvent FocusListener
Scroll ScrollEvent, ScrollEndEvent ScrollListener
Touch TapEvent, TapHoldEvent, SwipeEvent, SwipeLeftEvent, SwipeRightEvent TouchListener
Drag & Drop DragStartEvent, DropEvent, DragEnterEvent, DragLeaveEvent, etc. DragDropListener
File ClientFileSelectEvent, ClientFileUploadEvent (see File Uploads)
Custom UserEvent UserEventListener

All client events and listeners are in the com.oorian.messaging.events.client package.

Here is a complete example. The page implements MouseClickListener and registers itself on two buttons. When either button is clicked, the onEvent() method is called and getSource() identifies which button was clicked:

Java
@Page("/counter")
public class CounterPage extends HtmlPage implements MouseClickListener
{
    private int count = 0;
    private Span countLabel;
    private Button incrementBtn;
    private Button resetBtn;

    @Override
    protected void createBody(Body body)
    {
        countLabel = new Span();
        countLabel.setText("0");
        body.addElement(countLabel);

        incrementBtn = new Button("Increment");
        incrementBtn.registerListener(this, MouseClickedEvent.class);
        body.addElement(incrementBtn);

        resetBtn = new Button("Reset");
        resetBtn.registerListener(this, MouseClickedEvent.class);
        body.addElement(resetBtn);
    }

    @Override
    public void onEvent(MouseClickedEvent event)
    {
        if (event.getSource() == incrementBtn)
        {
            count++;
        }
        else if (event.getSource() == resetBtn)
        {
            count = 0;
        }

        countLabel.setText(String.valueOf(count));
        sendUpdate();
    }
}
MouseClickListener vs. MouseListener

If you only need click events, implement MouseClickListener — it has a single onEvent(MouseClickedEvent) method. If you need the full range of mouse interactions (double-click, mouse down/up, move, over/out), implement MouseListener instead, which defines handler methods for all seven mouse event types.

Server Events

Server events are created and dispatched entirely on the server. They are scoped to the element hierarchy and support event bubbling — when dispatched on an element, they propagate up through parent elements, allowing ancestors to observe events from their descendants.

This makes server events ideal for communication between components. A child component can dispatch an event, and a parent container can listen for it without the child knowing who is listening.

To create a custom server event, extend ServerEvent and parameterize it with your listener interface:

Java — Define the event and listener
public class ItemSelectedEvent extends ServerEvent<ItemSelectedListener>
{
    private final String itemId;

    public ItemSelectedEvent(String itemId)
    {
        this.itemId = itemId;
    }

    public String getItemId()
    {
        return itemId;
    }
}

public interface ItemSelectedListener extends ServerEventListener<ItemSelectedEvent>
{
    // onEvent(ItemSelectedEvent) is inherited from ServerEventListener
}
Java — Dispatch and handle the event
// In a child component — dispatch when something is selected
dispatchEvent(new ItemSelectedEvent("product-42"));

// In a parent component — register to listen for the event
itemList.registerListener(this, ItemSelectedEvent.class);

// Handle the event
@Override
public void onEvent(ItemSelectedEvent event)
{
    String id = event.getItemId();
    // update detail panel, etc.
}

Because server events bubble by default, you can register the listener on a parent element and it will receive events dispatched by any descendant. To create a non-bubbling server event, pass false to the superclass constructor:

Java
public ItemSelectedEvent(String itemId)
{
    super(false);  // this event will not bubble
    this.itemId = itemId;
}

Extended Server Events (ExtServerEvent)

Extended server events work like server events — they are element-scoped and support bubbling — but with one key difference: the ExtServerEventListener interface is empty. Instead of inheriting a generic onEvent() method, each listener subinterface defines its own specifically-named handler methods.

This is useful when a component fires multiple related events and you want distinct handler method names instead of overloaded onEvent() methods:

Java — Define extended events and listener
public class TabOpenedEvent extends ExtServerEvent<TabEventListener>
{
    private final int tabIndex;

    public TabOpenedEvent(int tabIndex) { this.tabIndex = tabIndex; }
    public int getTabIndex() { return tabIndex; }

    @Override
    public void dispatchTo(TabEventListener listener)
    {
        listener.onTabOpened(this);
    }
}

public class TabClosedEvent extends ExtServerEvent<TabEventListener>
{
    private final int tabIndex;

    public TabClosedEvent(int tabIndex) { this.tabIndex = tabIndex; }
    public int getTabIndex() { return tabIndex; }

    @Override
    public void dispatchTo(TabEventListener listener)
    {
        listener.onTabClosed(this);
    }
}

public interface TabEventListener extends ExtServerEventListener<ExtServerEvent>
{
    void onTabOpened(TabOpenedEvent event);
    void onTabClosed(TabClosedEvent event);
}
Java — Register and handle
// Register for both events through the shared listener
tabPanel.registerListener(this, TabOpenedEvent.class, TabClosedEvent.class);

// Each event dispatches to its own handler method
@Override
public void onTabOpened(TabOpenedEvent event)
{
    loadTabContent(event.getTabIndex());
}

@Override
public void onTabClosed(TabClosedEvent event)
{
    cleanupTab(event.getTabIndex());
}
ServerEvent vs. ExtServerEvent

Use ServerEvent when you have a single event type with one handler. Use ExtServerEvent when a component fires multiple related events and you want descriptive handler method names like onTabOpened() and onTabClosed() instead of multiple onEvent() overloads. Both support event bubbling through the element hierarchy.

Page Events

Page events are scoped to a single HtmlPage instance. Unlike server events, they are not tied to any element and do not bubble. Any component on the page can dispatch a page event, and any component that has registered a listener on the page will receive it.

This makes page events ideal for decoupled communication between sibling components that don't share a parent-child relationship in the element tree. A sidebar component can notify a content panel that the user selected a new category, without either component holding a direct reference to the other.

Java — Define a page event
public class CategoryChangedEvent extends PageEvent
{
    private final String category;

    public CategoryChangedEvent(String category)
    {
        this.category = category;
    }

    public String getCategory()
    {
        return category;
    }
}

Register and dispatch page events through the page or through any element (which delegates to the page automatically):

Java — In the sidebar component
// Dispatch a page event from any element
dispatchEvent(new CategoryChangedEvent("electronics"));
Java — In the content panel
// Register for page events (can register on the page or on any element)
registerListener(this, CategoryChangedEvent.class);

@Override
public void onEvent(CategoryChangedEvent event)
{
    loadProducts(event.getCategory());
}

Page events also support a stop() mechanism. A listener can call event.stop() to signal that the event has been fully handled. Subsequent listeners can check event.isStopped() to decide whether to act.

Session Events

Session events are scoped to the current user's OorianSession. They are delivered to all listeners registered on that session, regardless of which page the listener belongs to. This makes session events the right choice when you need to communicate across pages within a single user's session.

A common use case is notifying all open pages when a user's preferences change, or when background processing completes:

Java — Define a session event
public class ThemeChangedEvent extends SessionEvent<ThemeChangedListener>
{
    private final String theme;

    public ThemeChangedEvent(String theme)
    {
        this.theme = theme;
    }

    public String getTheme()
    {
        return theme;
    }
}

public interface ThemeChangedListener extends SessionEventListener<ThemeChangedEvent>
{
    // onEvent(ThemeChangedEvent) is inherited from SessionEventListener
}
Java — Register on the session and dispatch
// Any page can register to receive session events
OorianSession.get().registerListener(this, ThemeChangedEvent.class);

// Dispatch to all listeners in this session
new ThemeChangedEvent("dark").dispatch();

// Handle the event
@Override
public void onEvent(ThemeChangedEvent event)
{
    applyTheme(event.getTheme());
    sendUpdate();
}

Session events provide a convenience dispatch() method that automatically dispatches the event to the current session. You can also dispatch explicitly with OorianSession.get().dispatchEvent(event).

Application Events

Application events are the broadest scope in the event system. When dispatched, they are delivered to every active session in the application. This makes them ideal for system-wide broadcasts such as maintenance notifications, configuration changes, or real-time alerts that should reach all connected users.

Java — Define an application event
public class MaintenanceAlert extends ApplicationEvent<MaintenanceAlertListener>
{
    private final String message;
    private final int minutesUntilShutdown;

    public MaintenanceAlert(String message, int minutesUntilShutdown)
    {
        this.message = message;
        this.minutesUntilShutdown = minutesUntilShutdown;
    }

    public String getMessage() { return message; }
    public int getMinutesUntilShutdown() { return minutesUntilShutdown; }
}

public interface MaintenanceAlertListener extends ApplicationEventListener<MaintenanceAlert>
{
    // onEvent(MaintenanceAlert) is inherited from ApplicationEventListener
}
Java — Register and broadcast
// Each page registers to receive application events via its session
OorianSession.get().registerListener(this, MaintenanceAlert.class);

// Broadcast to every active session in the application
new MaintenanceAlert("Server restarting for updates", 5).dispatch();

// Every registered listener across all sessions receives the event
@Override
public void onEvent(MaintenanceAlert event)
{
    showBanner(event.getMessage());
    sendUpdate();
}
Application Events Reach All Users

Application events are dispatched to every active session. Use them only for truly global notifications. For user-specific communication, use session events instead.

Choosing the Right Event Type

Scenario Event Type
Responding to a button click ClientEvent (MouseClickedEvent)
Child component notifying its parent ServerEvent (bubbles up)
Component with multiple related events ExtServerEvent (custom handler names)
Sidebar notifying content panel on the same page PageEvent
Updating all open tabs when user preferences change SessionEvent
Announcing server maintenance to all connected users ApplicationEvent

The sendUpdate() Pattern

After modifying elements in an event handler, call sendUpdate() to push the changes to the browser. This is Oorian's core interaction pattern:

  1. An event is dispatched to your listener
  2. Your handler modifies element properties (text, visibility, styles, etc.)
  3. You call sendUpdate() to push all pending changes to the browser

Oorian automatically detects which elements have changed and sends only the minimal updates needed. If you change the text of one label, only that label's update is sent — not the entire page.

When to Call sendUpdate()

Call sendUpdate() once at the end of your event handler, after all modifications are complete. Oorian batches all changes into a single response, so calling it multiple times is unnecessary. For server-side events (ServerEvent, PageEvent, etc.) that modify the UI, remember to call sendUpdate() just as you would for client events.

11. Session Management

Oorian provides the OorianSession class for managing server-side session data. It wraps the standard HTTP session with typed accessors and integrates with Oorian's page lifecycle.

Accessing the Session

From within an HtmlPage, use the inherited getSession() method. Outside of a page context, use the static OorianSession.get() method:

Java
// From within an HtmlPage
OorianSession session = getSession();
session.setAttribute("user", currentUser);

// From anywhere in the application (static access)
OorianSession session = OorianSession.get();
User user = (User) session.getAttribute("user");
Always Use OorianSession

Do not access HttpSession directly. Always use OorianSession for session management. It provides thread-safe access, integrates with Oorian's page caching, and offers typed attribute accessors.

Storing and Retrieving Attributes

OorianSession stores attributes as Object values and provides typed getters for common types. Each typed getter has an overload that accepts a default value:

Java
OorianSession session = getSession();

// Store attributes
session.setAttribute("username", "john.doe");
session.setAttribute("userId", 42);
session.setAttribute("isAdmin", true);
session.setAttribute("balance", 1250.75);

// Retrieve with typed accessors
String username = session.getAttributeAsString("username");
Integer userId = session.getAttributeAsInt("userId");
Boolean isAdmin = session.getAttributeAsBoolean("isAdmin");
Double balance = session.getAttributeAsDouble("balance");

// Retrieve with default values (returned if attribute is null)
String theme = session.getAttributeAsString("theme", "light");
Integer pageSize = session.getAttributeAsInt("pageSize", 25);
Boolean darkMode = session.getAttributeAsBoolean("darkMode", false);

// Retrieve as raw Object (for custom types)
User user = (User) session.getAttribute("user");

// Remove an attribute
session.removeAttribute("tempData");

Session Info

OorianSession exposes standard session metadata and lifecycle controls:

Method Description
getId() Returns the unique session identifier
getCreationTime() Returns when the session was created (milliseconds since epoch)
getLastAccessedTime() Returns when the session was last accessed (milliseconds since epoch)
isNew() Returns true if the session was just created and the client has not yet acknowledged it
setMaxInactiveInterval(seconds) Sets the maximum idle time before the session expires
getMaxInactiveInterval() Returns the maximum inactive interval in seconds
invalidate() Destroys the session and unbinds all attributes
Java
OorianSession session = getSession();

// Set session timeout to 30 minutes
session.setMaxInactiveInterval(1800);

// Logout: clear data and destroy the session
session.removeAttribute("user");
session.invalidate();
navigateTo("/login");

Cookies

Oorian provides cookie management through HtmlPage methods. Use cookies for data that needs to persist across sessions, such as user preferences or "remember me" tokens:

Java
// Read a cookie
OorianCookie themeCookie = getCookie("theme");

if (themeCookie != null)
{
    applyTheme(themeCookie.getValue());
}

// Read with a default value
OorianCookie langCookie = getCookie("language", "en");
String language = langCookie.getValue();

// Create and add a cookie
OorianCookie cookie = addCookie("theme", "dark");

// Create a cookie with custom settings
OorianCookie rememberMe = new OorianCookie("remember", "token123");
rememberMe.setMaxAge(60 * 60 * 24 * 30);  // 30 days
rememberMe.setPath("/");
rememberMe.setHttpOnly(true);
rememberMe.setSecure(true);
addCookie(rememberMe);
Never Store Sensitive Data in Cookies

Cookies are stored on the client and can be read or modified by the user. Never store passwords, session tokens, or other sensitive information in cookies. Use server-side session attributes for sensitive data, and cookies only for non-sensitive preferences. Always set HttpOnly and Secure flags on cookies that carry any kind of token.

12. Forms and Validation

Oorian provides two approaches to form handling: OorianForm for basic forms with server-side processing, and ValidatedForm for forms that need built-in validation with automatic error display.

Basic Forms with OorianForm

Use OorianForm with FormListener to handle form submissions. Give each input a name with setName(), and access submitted values through the Parameters object:

Java
@Page("/contact")
public class ContactPage extends HtmlPage implements FormListener
{
    private TextInput nameInput;
    private TextInput emailInput;
    private TextArea messageInput;
    private Span statusLabel;

    @Override
    protected void createBody(Body body)
    {
        OorianForm form = new OorianForm();
        form.registerListener(this, FormEvent.class);

        nameInput = new TextInput();
        nameInput.setName("name");
        form.addElement(nameInput);

        emailInput = new TextInput();
        emailInput.setName("email");
        form.addElement(emailInput);

        messageInput = new TextArea();
        messageInput.setName("message");
        form.addElement(messageInput);

        form.addElement(new Button("Send Message"));

        statusLabel = new Span();
        form.addElement(statusLabel);

        body.addElement(form);
    }

    @Override
    public void onEvent(FormEvent event)
    {
        Parameters params = event.getParameters();

        String name = params.getParameterValue("name");
        String email = params.getParameterValue("email");
        String message = params.getParameterValue("message");

        // Process the form data
        sendContactEmail(name, email, message);

        statusLabel.setText("Thank you! Your message has been sent.");
        sendUpdate();
    }
}

The Parameters Class

The Parameters class provides typed access to form values. All values arrive as strings from the browser, and Parameters handles type conversion for you:

Method Returns
getParameterValue(name) String — the first value, or null
getParameterValueAsInt(name) Integer — parsed integer, or null
getParameterValueAsLong(name) Long — parsed long, or null
getParameterValueAsDouble(name) Double — parsed double, or null
getParameterValueAsFloat(name) Float — parsed float, or null
getParameterValueAsBoolean(name) Boolean — parsed boolean, or false
getParameterValues(name) List<String> — all values (for multi-select, checkboxes)
containsParameter(name) boolean — true if the parameter exists
Java
Parameters params = event.getParameters();

// String value
String name = params.getParameterValue("name");

// Numeric values with automatic parsing
Integer age = params.getParameterValueAsInt("age");
Double salary = params.getParameterValueAsDouble("salary");

// Boolean (checkbox)
Boolean subscribe = params.getParameterValueAsBoolean("subscribe");

// Multi-value (checkboxes, multi-select)
List<String> selectedRoles = params.getParameterValues("roles");

// Check if parameter was submitted
if (params.containsParameter("agreeToTerms"))
{
    // User checked the terms checkbox
}

Validated Forms

ValidatedForm extends OorianForm with built-in validation support. Each input field is wrapped in a ValidatedInput that defines validation rules through a fluent API:

Java
// Create a validated form
ValidatedForm form = new ValidatedForm();
form.registerListener(this, FormEvent.class);

// Create input elements
TextInput emailInput = new TextInput();
emailInput.setName("email");
Span emailError = new Span();

PasswordInput passwordInput = new PasswordInput();
passwordInput.setName("password");
Span passwordError = new Span();

// Register validated inputs with rules
form.addValidatedInput(
    new ValidatedInput<>(emailInput, emailError)
        .required("Email is required")
        .email("Please enter a valid email address")
);

form.addValidatedInput(
    new ValidatedInput<>(passwordInput, passwordError)
        .required("Password is required")
        .minLength(8, "Password must be at least 8 characters")
);

// Add elements to the form
form.addElement(emailInput);
form.addElement(emailError);
form.addElement(passwordInput);
form.addElement(passwordError);
form.addElement(new Button("Sign In"));

Each ValidatedInput takes the input element and an error display element (typically a Span). When validation fails, the error message is automatically shown in the error display element, and CSS classes are applied for visual feedback.

Built-in Validators

Oorian includes a comprehensive set of validators that cover common form validation scenarios:

Validator Method Description
required(message) Field must not be empty
email(message) Must be a valid email address
minLength(n, message) Minimum character length
maxLength(n, message) Maximum character length
length(min, max, message) Character length must be within range
pattern(regex, message) Must match a regular expression pattern
range(min, max, message) Numeric value must be within range
url(message) Must be a valid URL
phone(format, message) Must match a phone number format
numeric(message) Must be a numeric value
alphanumeric(message) Must contain only letters and numbers
creditCard(message) Must be a valid credit card number
date(format, message) Must match a date format

Validators are chained using the fluent API. They are evaluated in the order they are added, and validation stops at the first failure for each field:

Java
new ValidatedInput<>(usernameInput, usernameError)
    .required("Username is required")
    .minLength(3, "Username must be at least 3 characters")
    .maxLength(20, "Username must be 20 characters or fewer")
    .alphanumeric("Username must contain only letters and numbers");

Handling Validation in onEvent

In your FormListener, call form.validate(event) to validate all fields at once. The returned ValidationResult tells you whether the form is valid, and error messages are automatically displayed next to each invalid field:

Java
@Override
public void onEvent(FormEvent event)
{
    ValidationResult result = form.validate(event);

    if (result.isValid())
    {
        // All fields passed validation - process the form
        Parameters params = event.getParameters();
        String email = params.getParameterValue("email");
        String password = params.getParameterValue("password");
        createAccount(email, password);
    }

    // sendUpdate() pushes validation error messages to the browser
    sendUpdate();
}

Cross-Field Validation

For validation rules that compare two or more fields, use CompareValidator as a form-level validator. This is commonly used for password confirmation and date range validation:

Java
// Password confirmation
form.addFormValidator(
    new CompareValidator("password", "confirmPassword",
        CompareValidator.Operation.EQUALS)
        .withMessage("Passwords do not match")
);

// Date range validation
form.addFormValidator(
    new CompareValidator("startDate", "endDate",
        CompareValidator.Operation.LESS_THAN)
        .withMessage("Start date must be before end date")
);

The CompareValidator.Operation enum supports: EQUALS, NOT_EQUALS, LESS_THAN, LESS_THAN_OR_EQUALS, GREATER_THAN, and GREATER_THAN_OR_EQUALS.

13. Data Binding

Oorian's data binding framework connects Java bean properties to form fields, eliminating the manual work of reading values from forms and writing them to objects. The Binder class handles type conversion, validation, and bidirectional synchronization between your model and the UI.

Type Converters

Type converters transform values between their UI presentation type (typically String) and their Java model type. Every converter implements the Converter<P, M> interface with two methods:

  • convertToModel(P value) — converts the presentation value to the model type. Throws ConversionException on invalid input.
  • convertToPresentation(M value) — converts the model value to the presentation type. Never throws.

Oorian includes built-in converters for common types:

Converter From To
StringToIntegerConverter String Integer
StringToLongConverter String Long
StringToDoubleConverter String Double
StringToBigDecimalConverter String BigDecimal
StringToBooleanConverter String Boolean
StringToDateConverter String Date
StringToLocalDateConverter String LocalDate
StringToLocalDateTimeConverter String LocalDateTime
StringToEnumConverter String Enum

Built-in converters are registered in the ConverterRegistry by default. You can register custom converters for application-specific types:

Java
ConverterRegistry.getInstance().register(
    String.class, Money.class, new StringToMoneyConverter()
);

The Binder

The Binder<BEAN> class manages the bindings between form fields and bean properties. Use the fluent BindingBuilder API to configure each binding with converters, validators, and required-field checks:

Java
Binder<User> binder = new Binder<>(User.class);

// Bind a text input to a String property
binder.forField(nameInput)
    .asRequired("Name is required")
    .bind("name");

// Bind with a type converter
binder.forField(ageInput)
    .withConverter(new StringToIntegerConverter("Must be a number"))
    .bind("age");

// Bind with converter and validator
binder.forField(emailInput)
    .asRequired("Email is required")
    .withValidator(v -> v.contains("@"), "Invalid email")
    .bind("email");

// Bind a checkbox to a boolean property
binder.forCheckbox(activeCheckbox)
    .bind("active");

// Bind a select to an enum property
binder.forField(roleSelect)
    .withConverter(new StringToEnumConverter<>(Role.class))
    .bind("role");

Read values from a bean into the form, and write form values back to a bean:

Java
// Populate the form from a bean
User user = loadUser(userId);
binder.readBean(user);

// Write form values back to the bean
// Throws BindingValidationException if any binding fails
binder.writeBean(user);

// Or write only if all bindings are valid (returns boolean)
if (binder.writeBeanIfValid(user))
{
    saveUser(user);
}

// Check if any field has been modified
boolean dirty = binder.isModified();

Auto-Binding

For forms where field names match bean property names, use bindInstanceFields() to automatically bind fields by name. The binder scans the target object for fields whose names match bean properties and creates bindings with automatic converter lookup:

Java
public class UserForm extends OorianForm
{
    private TextInput name;       // matches User.name
    private TextInput email;      // matches User.email
    private Checkbox active;     // matches User.active

    public void setupBindings(Binder<User> binder)
    {
        // Automatically binds name, email, and active
        binder.bindInstanceFields(this);
    }
}

Fields that don't match any bean property are silently skipped.

Validation Pipeline

When writing form values to a bean, the binder runs each binding through a three-stage validation pipeline:

  1. Required check — if asRequired() was called and the field is empty, validation stops with the required message
  2. Type conversion — the converter's convertToModel() is called; a ConversionException stops validation
  3. Validators — each registered validator runs in order; the first failure stops the chain

Writes are atomic: if any binding fails validation, no values are written to the bean. This prevents partial updates where some fields are written and others are not.

Complete Example

Java
public class EditUserPage extends HtmlPage implements FormListener
{
    private Binder<User> binder;
    private User user;

    @Override
    protected void createBody(Body body)
    {
        user = loadUser(getUrlParameters().getValue("id"));
        binder = new Binder<>(User.class);

        OorianForm form = new OorianForm();

        TextInput nameInput = new TextInput();
        binder.forField(nameInput)
            .asRequired("Name is required")
            .bind("name");
        form.addElement(nameInput);

        TextInput emailInput = new TextInput();
        binder.forField(emailInput)
            .asRequired("Email is required")
            .withValidator(v -> v.contains("@"), "Invalid email")
            .bind("email");
        form.addElement(emailInput);

        TextInput ageInput = new TextInput();
        binder.forField(ageInput)
            .withConverter(new StringToIntegerConverter("Must be a number"))
            .withValidator(v -> v >= 0 && v <= 150, "Invalid age")
            .bind("age");
        form.addElement(ageInput);

        SubmitButton submit = new SubmitButton("Save");
        form.addElement(submit);
        form.registerListener(this, FormEvent.class);

        // Populate the form
        binder.readBean(user);

        body.addElement(form);
    }

    @Override
    public void onEvent(FormEvent event)
    {
        if (binder.writeBeanIfValid(user))
        {
            saveUser(user);
            navigateTo("/users");
        }
    }
}

14. Data Providers

Data providers offer a unified abstraction for loading data into grids, lists, tables, and trees. Whether your data is in memory, fetched from a database, or organized as a tree hierarchy, the DataProvider interface provides consistent pagination, sorting, and filtering.

DataProvider Interface

All data providers implement the DataProvider<T> interface:

Java
public interface DataProvider<T>
{
    DataResult<T> fetch(Query query);
    int size();
    void refresh();
    void addDataChangeListener(DataChangeListener listener);
}

The fetch() method receives a Query containing the offset, limit, sort descriptors, and filter descriptors, and returns a DataResult with the items and total count.

ListDataProvider

ListDataProvider is an in-memory implementation backed by a List. It supports sorting and filtering via reflection, and automatically notifies listeners when the underlying data changes:

Java
List<User> users = loadAllUsers();
ListDataProvider<User> provider = new ListDataProvider<>(users);

// Mutate the data — listeners are notified automatically
provider.addItem(newUser);
provider.removeItem(oldUser);
provider.replaceItem(oldUser, updatedUser);
provider.clear();

// Manual refresh after external changes
provider.refresh();

CallbackDataProvider

CallbackDataProvider is designed for lazy loading from databases or remote APIs. You provide two callbacks: one to fetch items and one to count the total:

Java
CallbackDataProvider<User> provider = new CallbackDataProvider<>(
    // FetchCallback: load a page of data
    query -> userRepository.find(
        query.getOffset(),
        query.getLimit(),
        query.getSorts(),
        query.getFilters()
    ),
    // CountCallback: return total count
    () -> userRepository.count()
);

HierarchicalDataProvider

HierarchicalDataProvider supports tree-structured data. Implement the methods to fetch children, count children, and check for child existence:

Java
HierarchicalDataProvider<Department> provider = new HierarchicalDataProvider<>()
{
    @Override
    public List<Department> fetchChildren(Department parent)
    {
        return departmentService.getChildren(parent.getId());
    }

    @Override
    public boolean hasChildren(Department item)
    {
        return departmentService.hasSubDepartments(item.getId());
    }
};

Query, Sorting, and Filtering

The Query class encapsulates pagination, sorting, and filtering parameters that are passed to DataProvider.fetch():

Java
// Sort descriptors
SortDescriptor byName = SortDescriptor.asc("name");
SortDescriptor byDate = SortDescriptor.desc("createdDate");

// Filter descriptors
FilterDescriptor active = FilterDescriptor.equals("status", "ACTIVE");
FilterDescriptor search = FilterDescriptor.contains("name", "John");
FilterDescriptor recent = FilterDescriptor.gte("createdDate", startDate);

The FilterOperator enum provides the full set of comparison operators:

Operator Description
EQUALS Exact match
NOT_EQUALS Not equal
CONTAINS String contains (case-insensitive)
STARTS_WITH String starts with
ENDS_WITH String ends with
GT / GTE Greater than / Greater than or equal
LT / LTE Less than / Less than or equal
IS_NULL Value is null
IS_NOT_NULL Value is not null

DataResult

DataResult wraps the fetched items and the total count. Use the static factory method to create results:

Java
// From a database query
List<User> page = userRepository.findPage(offset, limit);
int total = userRepository.count();

return DataResult.of(page, total);

15. Dependency Injection

Oorian provides a lightweight service locator abstraction that lets you resolve services from anywhere in your application. Use the built-in ServiceRegistry for simple applications, or plug in Spring, CDI, or any other DI framework through the ServiceLocator interface.

ServiceLocator Interface

The ServiceLocator interface defines the contract for service resolution:

Java
public interface ServiceLocator
{
    <T> T get(Class<T> serviceClass);
    boolean has(Class<T> serviceClass);
}

Built-in ServiceRegistry

The ServiceRegistry provides a ready-to-use implementation with two registration modes:

  • Singleton — returns the same instance for every lookup
  • Factory — creates a new instance for every lookup using a Supplier
Java
ServiceRegistry registry = new ServiceRegistry();

// Register a singleton — same instance every time
registry.register(UserService.class, new UserServiceImpl());
registry.register(EmailService.class, new SmtpEmailService());

// Register a factory — new instance per lookup
registry.registerFactory(ReportGenerator.class, () -> new ReportGenerator());

// Remove a registration
registry.unregister(EmailService.class);

// Clear all registrations
registry.clear();

Static Access via Services

The Services class provides static access to the configured service locator from anywhere in your application — pages, components, worker threads, and utility classes:

Java
// Resolve a service
UserService userService = Services.get(UserService.class);

// Check if a service is registered
if (Services.has(EmailService.class))
{
    Services.get(EmailService.class).sendWelcome(user);
}

// Throws ServiceNotFoundException if not registered
Services.get(UnregisteredService.class);  // throws

Spring Integration

To use Spring as your service locator, implement the ServiceLocator interface and delegate to the Spring ApplicationContext:

Java
public class SpringServiceLocator implements ServiceLocator
{
    private final ApplicationContext context;

    public SpringServiceLocator(ApplicationContext context)
    {
        this.context = context;
    }

    @Override
    public <T> T get(Class<T> serviceClass)
    {
        return context.getBean(serviceClass);
    }

    @Override
    public <T> boolean has(Class<T> serviceClass)
    {
        try
        {
            context.getBean(serviceClass);
            return true;
        }
        catch (NoSuchBeanDefinitionException e)
        {
            return false;
        }
    }
}

Application Setup

Wire everything together in your Application.initialize() method. Set the service locator first, then register your services:

Java
@WebListener
public class MyApp extends Application
{
    @Override
    protected void initialize(ServletContext context)
    {
        registerPackage("com.myapp");

        // Option 1: Built-in registry
        ServiceRegistry registry = new ServiceRegistry();
        registry.register(UserService.class, new UserServiceImpl());
        registry.register(EmailService.class, new SmtpEmailService());
        Services.setServiceLocator(registry);

        // Option 2: Spring integration
        // ApplicationContext springCtx = new AnnotationConfigApplicationContext(AppConfig.class);
        // Services.setServiceLocator(new SpringServiceLocator(springCtx));
    }
}

Pages and components can then resolve services without knowing which DI framework is being used:

Java
public class UserListPage extends HtmlPage
{
    @Override
    protected void createBody(Body body)
    {
        UserService userService = Services.get(UserService.class);
        List<User> users = userService.findAll();

        for (User user : users)
        {
            body.addElement(new Paragraph(user.getName()));
        }
    }
}
Testing Tip

For unit tests, register mock implementations in a ServiceRegistry and set it as the service locator before running tests. This lets you test pages and components in isolation without real service dependencies.

16. Worker Threads

Sometimes you need to perform long-running operations — database queries, API calls, file processing, report generation — without blocking the user's request. Oorian's OorianWorkerThread lets you offload work to a background thread and update the UI when it completes.

Creating a Worker Thread

Call OorianWorkerThread.create() from an event handler, passing a lambda that contains the work to perform. The worker thread has full access to the page's elements, so you can update the UI directly and push changes to the browser with sendUpdate().

Java
@Override
public void onEvent(MouseClickedEvent event)
{
    statusLabel.setText("Processing...");
    progressBar.setDisplay(Display.BLOCK);
    sendUpdate();

    OorianWorkerThread.create(() ->
    {
        // This runs in a background thread
        List<Report> reports = generateReports();

        // Update the UI when done
        statusLabel.setText("Complete! Generated " + reports.size() + " reports.");
        progressBar.setDisplay(Display.NONE);
        sendUpdate();  // Push changes to browser
    });
}

How It Works

Worker threads created with OorianWorkerThread.create() are queued and started after the current request processing completes. This design prevents race conditions by ensuring your event handler finishes before the background thread begins modifying page state.

Key characteristics of worker threads:

  • Queued execution — The worker starts after the current request completes, not immediately
  • Full page access — The background thread can read and modify any element on the page
  • UI push via sendUpdate() — Call sendUpdate() to push changes to the browser at any point during execution
  • No concurrent access — Only one worker thread runs per page at a time, preventing race conditions on shared page state
Communication Mode Matters

Worker threads that call sendUpdate() require a persistent connection to the browser. Use WEBSOCKET or AJAX_WITH_SSE mode for pages that use worker threads. With AJAX_ONLY, the browser won't receive updates until its next poll.

17. Drag and Drop

Oorian provides a complete drag-and-drop system built on the HTML5 Drag and Drop API. Elements can be made draggable, containers can accept drops, and all events are handled server-side through standard Oorian listeners — no JavaScript required.

Making Elements Draggable

Call setDraggable(true) on any element to allow the user to drag it:

Java
Div card = new Div();
card.setText("Drag me");
card.setDraggable(true);

This sets the HTML draggable attribute and registers the necessary client-side handlers automatically.

Creating Drop Targets

Call setDropAllowed(true) on a container to make it accept dropped elements:

Java
Div dropZone = new Div();
dropZone.setDropAllowed(true);

Drag and Drop Events

Oorian fires a series of events during a drag-and-drop operation. Implement DragDropListener and register for the events you need:

Event Fired When
DragStartEvent The user begins dragging an element
DragEndEvent The drag operation ends (drop or cancel)
DragEnterEvent A dragged element enters a drop target
DragOverEvent A dragged element moves over a drop target
DragLeaveEvent A dragged element leaves a drop target
DragExitEvent A dragged element exits the browser window
DropEvent An element is dropped on a valid target

Handling Drops

Register listeners on both the draggable elements and the drop target, then handle the events on the server:

Java
public class TaskBoardPage extends HtmlPage implements DragDropListener
{
    private Div todoColumn;
    private Div doneColumn;

    @Override
    protected void createBody(Body body)
    {
        todoColumn = new Div();
        todoColumn.setDropAllowed(true);
        todoColumn.registerListener(this, DropEvent.class);

        doneColumn = new Div();
        doneColumn.setDropAllowed(true);
        doneColumn.registerListener(this, DropEvent.class);

        // Create draggable task cards
        for (String task : tasks)
        {
            Div card = createTaskCard(task);
            card.setDraggable(true);
            todoColumn.addElement(card);
        }

        body.addElement(todoColumn);
        body.addElement(doneColumn);
    }

    @Override
    public void onEvent(DropEvent event)
    {
        Element dropped = event.getDroppedElement();
        Element newParent = event.getNewParent();
        Element oldParent = event.getOldParent();

        // Update your data model based on the move
        updateTaskStatus(dropped, newParent);
        sendUpdate();
    }
}

The DropEvent

The DropEvent provides full context about the drop operation:

Method Description
getDroppedElement() The element that was dropped
getOldParent() The original parent container before the drag
getNewParent() The container the element was dropped into
getOldParentChildIds() Child element IDs of the old parent after the move
getNewParentChildIds() Child element IDs of the new parent after the move

The child ID lists reflect the new ordering after the drop, making it straightforward to persist the updated order to a database.

DropPanel

For common reordering scenarios, Oorian provides DropPanel — a container that automatically makes its children draggable and handles drop events. Extend it and implement three callback methods:

Java
public class SortableList extends DropPanel
{
    @Override
    protected void onDragStart(Element element)
    {
        element.setOpacity(50);  // Visual feedback
    }

    @Override
    protected void onDragEnd(Element element)
    {
        element.setOpacity(100);  // Restore
    }

    @Override
    protected void onDrop(Element dropped)
    {
        // Element has already been moved in the DOM
        // Persist the new ordering
        saveOrder();
        sendUpdate();
    }
}

DropPanel handles all the wiring automatically: children added to the panel become draggable, the panel itself is a drop target, and elements are visually repositioned during the drag. The client-side auto-scroll feature activates when the user drags near the edges of the browser window, making it easy to reorder long lists.

Automatic vs. Custom

Use DropPanel when you need sortable lists or reorderable grids within a single container. Use the low-level setDraggable() / setDropAllowed() API when you need to drag between separate containers or implement custom drag-and-drop behavior.

18. JavaScript and Browser APIs

Oorian provides type-safe Java access to browser capabilities through three layers: methods on HtmlPage, methods on Element, and 19 specialized API classes in the com.oorian.html.js package. You never need to write JavaScript—every browser feature is accessible from Java.

Adding Scripts to the Page

The JavaScript element represents an HTML <script> tag. Use it to include external JavaScript files or embed inline code. This is primarily used when integrating third-party libraries or adding bridge code for extension libraries.

External Script Files

Reference an external JavaScript file by passing the URL to the constructor or calling setSrc(). Absolute paths within the application are automatically resolved with the servlet context path:

Java
// Via constructor
JavaScript script = new JavaScript("/js/chart-config.js");
head.addElement(script);

// Via setSrc()
JavaScript analytics = new JavaScript();
analytics.setSrc("/js/analytics.js");
head.addElement(analytics);

Inline JavaScript Code

Embed JavaScript directly in the page using setSrcCode() to set the content or addSrcCode() to append incrementally:

Java
// Set inline code
JavaScript init = new JavaScript();
init.setSrcCode("document.addEventListener('DOMContentLoaded', initApp);");
head.addElement(init);

// Build code incrementally
JavaScript config = new JavaScript();
config.addSrcCode("var CONFIG = {\n");
config.addSrcCode("    apiUrl: '/api/v1',\n");
config.addSrcCode("    debug: false\n");
config.addSrcCode("};\n");
head.addElement(config);

Loading Source Code from a File

Use setSrcCodeFromFile() to load JavaScript source code from a file within the web application and embed it inline at render time. Unlike setSrc() which creates a <script src="..."> reference, this reads the file contents and inlines them directly into the page:

Java
// File contents are read at render time and embedded inline
JavaScript bridge = new JavaScript();
bridge.setSrcCodeFromFile("/js/oorian-bridge.js");
head.addElement(bridge);

Async and Deferred Loading

Control when external scripts execute using setAsync() and setDefer(). These only apply to external scripts (with a src attribute):

Java
// Async: download in parallel, execute as soon as available
JavaScript async = new JavaScript("/js/analytics.js");
async.setAsync(true);

// Defer: download in parallel, execute after document is parsed
JavaScript deferred = new JavaScript("/js/app.js");
deferred.setDefer(true);
Method Description
new JavaScript(src) Create a script referencing an external file
setSrc(url) Set or change the external file URL
setSrcCode(code) Set inline code (replaces any existing content)
addSrcCode(code) Append inline code incrementally
setSrcCodeFromFile(path) Load file contents and embed inline at render time
setAsync(true) Download in parallel, execute immediately when available
setDefer(true) Download in parallel, execute after document is parsed
setType(type) Set the MIME type (e.g., "module" for ES6 modules)

Executing JavaScript

When you need to call a browser API that Oorian doesn't wrap directly, use executeJs() on HtmlPage to run JavaScript on the client:

Java
// Execute a raw JavaScript statement
executeJs("console.log('Hello from the server')");

// Call a function with parameters (auto-serialized to JSON)
executeJs("showNotification", "Success", "Changes saved.");

// Load a JavaScript file (cached, duplicates ignored)
loadScript("/js/analytics.js");

// Load with cache-busting
loadScript("/js/analytics.js", true);

Navigation

HtmlPage provides a full navigation API:

Method Description
navigateTo(url) Navigate to the specified URL
navigateBack() / navigateBack(n) Go back one or n pages in browser history
navigateForward() / navigateForward(n) Go forward one or n pages in browser history
navigateToReferrer() Navigate to the referring page
reload() Reload the current page
navigateToAfterDelay(url, ms) Navigate after a delay in milliseconds
reloadAfterDelay(ms) Reload after a delay in milliseconds
navigateBackAfterDelay(ms) Go back after a delay in milliseconds
historyPushState(title, url) Add a history entry without navigating
historyReplaceState(title, url) Replace the current history entry without navigating
Java
navigateTo("/dashboard");

// Update the URL without a full navigation
historyPushState("User Profile", "/users/42");

// Redirect after showing a success message
navigateToAfterDelay("/dashboard", 2000);

Window Management

Method Description
openInNewWindow(url) Open a URL in a new browser tab or window
openInNewWindow(url, target) Open a URL in a named window
openInNewWindow(url, name, specs) Open with window features (width, height, etc.)
closeWindow(name) Close a named window
blockPageUnload() Show a confirmation dialog when the user tries to leave
unblockPageUnload() Remove the page unload confirmation
print() Open the browser's print dialog
print(url) Print the content at the specified URL

Client-Side Timers and Image Preloading

Method Description
setClientTimeout(script, ms) Execute JavaScript after a delay (like setTimeout)
setClientInterval(script, ms) Execute JavaScript repeatedly (like setInterval)
loadImage(url) Preload an image for smoother UI transitions
loadImages(urls) Preload multiple images

Element-Level Methods

Individual elements provide methods for scrolling, click simulation, DOM updates, and server callbacks:

Method Description
scrollTo(pos) Scroll the element to a pixel position
scrollToTop() Scroll the element to the top
scrollToBottom() Scroll the element to the bottom
click() Simulate a click on the element from the server
update() Re-render and push this element to the browser
updateAttributes() Push only attribute changes (more efficient)
recreate() Full re-render from scratch
refresh() Refresh this element and all its children
requestCallback() Schedule a server round-trip callback
requestCallback(id) Callback with a named identifier
requestCallback(id, delay) Delayed callback with a named identifier

The callback system enables server-initiated round-trips. Override onCallback(String callbackId) to handle the response:

Java
myElement.requestCallback("refresh", 5000);

@Override
public void onCallback(String callbackId)
{
    if ("refresh".equals(callbackId))
    {
        refreshData();
    }
}

Specialized JavaScript API Classes

Oorian provides 19 static API classes in com.oorian.html.js for accessing browser features. All follow the same pattern: static methods, event-driven results, and no instantiation required.

API Class Purpose
WindowApi Window management, alerts, scrolling, print, focus/blur
NavigationApi URL navigation, history, back/forward, hash manipulation
TimerApi Client-side timers and server-side scheduled tasks
ScreenApi Screen size, window size, device type, orientation, pixel ratio
SelectionApi Text selection and focus management
ClipboardApi Read and write the system clipboard
StorageApi Browser localStorage and sessionStorage
FullscreenApi Enter, exit, and toggle fullscreen mode
GeolocationApi GPS position requests and continuous tracking
NotificationApi Desktop notifications with permission management
SpeechApi Text-to-speech synthesis and speech recognition
ShareApi Native content sharing (mobile share sheets)
MediaDevicesApi Camera and microphone enumeration and access
PermissionsApi Query browser permission status
NetworkApi Online/offline status monitoring
VisibilityApi Page visibility changes (tab switching, minimization)
VibrationApi Haptic feedback patterns for mobile devices
WakeLockApi Prevent screen dimming for video players, presentations

Every API method has two forms: a convenience version that finds the current page automatically, and an explicit version that accepts an HtmlPage parameter.

The Listener Pattern

Every asynchronous browser API in Oorian follows the same pattern:

  1. Your page implements a listener interface (e.g., GeolocationListener)
  2. You register the listener on an element: element.registerListener(this, EventClass.class)
  3. You call a static API method: GeolocationApi.getCurrentPosition(element)
  4. Oorian sends a JavaScript command to the browser
  5. The browser executes the API call and sends the result back
  6. The framework delivers the result to your Java listener as a typed event

Each API provides convenience methods that resolve the current page from the thread context, plus explicit overloads that accept an HtmlPage parameter for use in worker threads or shared services. Fire-and-forget operations (like writing to the clipboard) do not require a listener.

Geolocation

GeolocationApi wraps the browser's navigator.geolocation API. Request the user's current position or track changes over time. The browser prompts for permission, retrieves the location, and delivers the result to your GeolocationListener as a typed GeolocationEvent.

Java
public class LocationPage extends HtmlPage implements GeolocationListener
{
    private P statusLabel;
    private P coordinatesLabel;

    @Override
    protected void createBody(Body body)
    {
        statusLabel = new P("Click the button to get your location.");
        body.addElement(statusLabel);

        coordinatesLabel = new P();
        body.addElement(coordinatesLabel);

        Button btn = new Button("Get My Location");
        btn.registerListener(this, MouseClickedEvent.class);
        btn.registerListener(this, GeolocationEvent.class);
        btn.registerListener(this, GeolocationErrorEvent.class);
        body.addElement(btn);
    }

    @Override
    public void onEvent(MouseClickedEvent event)
    {
        statusLabel.setText("Requesting location...");
        GeolocationApi.getCurrentPosition(event.getElement());
    }

    @Override
    public void onEvent(GeolocationEvent event)
    {
        double lat = event.getLatitude();
        double lng = event.getLongitude();
        double accuracy = event.getAccuracy();

        statusLabel.setText("Location received!");
        coordinatesLabel.setText(String.format(
            "Lat: %.6f, Lng: %.6f (accuracy: %.0fm)",
            lat, lng, accuracy));
    }

    @Override
    public void onEvent(GeolocationErrorEvent event)
    {
        statusLabel.setText("Error: " + event.getMessage());
    }
}

For continuous tracking, use watchPosition() to receive updates whenever the user moves. Call clearWatch() with the returned ID to stop:

Java
// Start tracking — events arrive via the same GeolocationListener
String watchId = GeolocationApi.watchPosition(element);

// Stop tracking
GeolocationApi.clearWatch(watchId);

// Request high-accuracy positioning
GeolocationOptions options = GeolocationOptions.highAccuracy();
GeolocationApi.getCurrentPosition(element, options);

Clipboard

ClipboardApi wraps the browser's Clipboard API. Writing text to the clipboard is a fire-and-forget operation that does not require a listener. Reading requires the browser to prompt for permission, so results arrive asynchronously via ClipboardListener.

Java — Writing (no listener needed)
// Copy text to clipboard — convenience method
ClipboardApi.writeText("Text to copy");

// Copy from a specific page context
ClipboardApi.writeText(page, "Text to copy");
Java — Reading (async, result via listener)
public class PastePage extends HtmlPage implements ClipboardListener
{
    private P resultLabel;

    @Override
    protected void createBody(Body body)
    {
        resultLabel = new P();
        body.addElement(resultLabel);

        Button pasteBtn = new Button("Paste from Clipboard");
        pasteBtn.registerListener(this, MouseClickedEvent.class);
        pasteBtn.registerListener(this, ClipboardReadEvent.class);
        pasteBtn.registerListener(this, ClipboardErrorEvent.class);
        body.addElement(pasteBtn);
    }

    @Override
    public void onEvent(MouseClickedEvent event)
    {
        ClipboardApi.readText(event.getElement());
    }

    @Override
    public void onEvent(ClipboardReadEvent event)
    {
        resultLabel.setText("Clipboard: " + event.getText());
    }

    @Override
    public void onEvent(ClipboardErrorEvent event)
    {
        resultLabel.setText("Error: " + event.getMessage());
    }
}

Web Storage

StorageApi provides access to both localStorage (persists across sessions) and sessionStorage (cleared when the tab closes). Write and remove operations are fire-and-forget. Read operations are asynchronous and deliver results to your StorageListener.

Java — Writing (no listener needed)
// localStorage — persists across browser sessions
StorageApi.setLocalItem("theme", "dark");
StorageApi.setLocalItem("language", "en");
StorageApi.removeLocalItem("theme");
StorageApi.clearLocalStorage();

// sessionStorage — cleared when the tab closes
StorageApi.setSessionItem("wizardStep", "3");
StorageApi.setSessionItem("formDraft", jsonData);
StorageApi.removeSessionItem("wizardStep");
StorageApi.clearSessionStorage();
Java — Reading (async, result via listener)
public class PreferencesPage extends HtmlPage implements StorageListener
{
    private P themeLabel;

    @Override
    protected void createBody(Body body)
    {
        themeLabel = new P("Loading preferences...");
        themeLabel.registerListener(this, StorageEvent.class);
        body.addElement(themeLabel);

        // Request the stored value — result arrives via onEvent(StorageEvent)
        StorageApi.getLocalItem("theme", themeLabel);
    }

    @Override
    public void onEvent(StorageEvent event)
    {
        String key = event.getKey();
        String value = event.getValue();

        if ("theme".equals(key))
        {
            if (event.hasValue())
            {
                themeLabel.setText("Theme: " + value);
            }
            else
            {
                themeLabel.setText("No theme preference saved.");
            }
        }
    }
}

The StorageEvent includes getStorageType() to distinguish between localStorage and sessionStorage results, and hasValue() to check whether a value was found for the requested key.

Notifications

NotificationApi shows native browser notifications from your Java code. Notifications require user permission, which you should request before attempting to show them. Implement NotificationListener to respond to click, close, and error events.

Java
public class AlertsPage extends HtmlPage implements NotificationListener
{
    private P statusLabel;

    @Override
    protected void createBody(Body body)
    {
        statusLabel = new P();
        body.addElement(statusLabel);

        Button permBtn = new Button("Enable Notifications");
        permBtn.registerListener(this, MouseClickedEvent.class);
        permBtn.registerListener(this, NotificationClickEvent.class);
        permBtn.registerListener(this, NotificationCloseEvent.class);
        body.addElement(permBtn);
    }

    @Override
    public void onEvent(MouseClickedEvent event)
    {
        // Request permission, then show a notification
        Element element = event.getElement();
        NotificationApi.requestPermission(element);

        NotificationOptions options = new NotificationOptions();
        options.setIcon("/images/icon.png");
        options.setTag("task-complete");

        NotificationApi.show(element, "Task Complete",
            "Your report has been generated.");
    }

    @Override
    public void onEvent(NotificationClickEvent event)
    {
        statusLabel.setText("Notification clicked: " + event.getTag());
        navigateTo("/reports");
    }

    @Override
    public void onEvent(NotificationCloseEvent event)
    {
        statusLabel.setText("Notification dismissed.");
    }

    @Override
    public void onEvent(NotificationErrorEvent event)
    {
        statusLabel.setText("Notification error: " + event.getMessage());
    }
}

Use NotificationOptions to customize the notification with an icon, badge, image, tag, and other properties. The tag property identifies the notification in click and close events, allowing you to distinguish between multiple active notifications. Call NotificationApi.closeAll() to dismiss all active notifications.

Other APIs

The remaining APIs follow the same pattern. Here are quick examples of the most commonly used ones:

Java — Screen Detection and Fullscreen
// Detect device type
if (ScreenApi.isMobile())
{
    useMobileLayout();
}

// Check screen orientation
boolean portrait = ScreenApi.isPortrait();

// Enter fullscreen mode
FullscreenApi.requestFullscreen(videoPlayer);

// Toggle fullscreen
FullscreenApi.toggleFullscreen(presentationDiv);
Java — Speech, Vibration, and Sharing
// Text-to-speech
SpeechApi.speak("Welcome to the application");
SpeechApi.speak("Bienvenue", "fr-FR", 1.0f, 1.0f);

// Haptic feedback on mobile
VibrationApi.success();
VibrationApi.error();

// Native sharing on mobile
ShareApi.shareUrl(body, "Check this out", "https://oorian.com");
Java — Network Monitoring and Wake Lock
// Monitor online/offline status
NetworkApi.startListening(body);

// Detect tab visibility changes
VisibilityApi.startListening(body);

// Keep the screen on during a presentation
WakeLockApi.request(body);

// Release when done
WakeLockApi.release(body);
Java — Server-Side Timers
// Execute a task on the server after a delay
TimerApi.setTimeout(() -> {
    statusLabel.setText("Operation complete");
    sendUpdate();
}, 5000);

// Repeat a task every 30 seconds
TimerApi.setInterval(() -> {
    refreshDashboard();
    sendUpdate();
}, 0, 30000);

19. JSON API

Oorian includes a built-in JSON library for parsing, generating, and manipulating JSON data. The core classes are JsonObject, JsonArray, and the Jsonable interface for creating typed data models.

JsonObject

Use JsonObject to build JSON objects with key-value pairs. The put() method accepts strings, numbers, booleans, nested objects, arrays, and any object that implements Jsonable:

Java
// Build a JSON object
JsonObject user = new JsonObject();
user.put("name", "Alice Johnson");
user.put("age", 32);
user.put("active", true);
user.put("balance", 1250.75);

// Nested objects
JsonObject address = new JsonObject();
address.put("city", "Portland");
address.put("state", "OR");
user.put("address", address);

// Convert to JSON string
String json = user.toJsonString();
// {"name":"Alice Johnson","age":32,"active":true,"balance":1250.75,"address":{"city":"Portland","state":"OR"}}

Reading Values

Parse a JSON string with JsonObject.parse(), then use typed getters to extract values. Each getter has an overload that accepts a default value:

Java
// Parse from a JSON string
JsonObject json = JsonObject.parse(jsonString);

// Typed getters (return null if key is missing)
String name = json.getAsString("name");
Integer age = json.getAsInt("age");
Boolean active = json.getAsBoolean("active");
Double balance = json.getAsDouble("balance");
Long timestamp = json.getAsLong("timestamp");

// Getters with default values
String email = json.getAsString("email", "unknown@example.com");
Integer count = json.getAsInt("count", 0);
Boolean verified = json.getAsBoolean("verified", false);

// Nested objects and arrays
JsonObject address = json.getAsJsonObject("address");
String city = address.getAsString("city");

JsonArray tags = json.getAsJsonArray("tags");

// Check if a key exists
if (json.containsKey("email"))
{
    // Key exists in the object
}

// Get all keys
List<String> keys = json.getKeys();

JsonArray

JsonArray represents an ordered collection of JSON values. It supports the fluent add() method and provides typed extraction of elements:

Java
// Build a JSON array
JsonArray colors = new JsonArray();
colors.add("red").add("green").add("blue");

// Add objects to an array
JsonArray items = new JsonArray();
items.add(new JsonObject("name", "Item 1"));
items.add(new JsonObject("name", "Item 2"));

// Parse a JSON array string
JsonArray numbers = JsonArray.parse("[1, 2, 3, 4, 5]");

// Read individual elements by index
String first = colors.get(0).asString().getValue();
int num = numbers.get(0).asNumber().getValue().intValue();

// Extract typed lists
List<String> colorList = colors.getStringArray();
List<Integer> numberList = numbers.getIntArray();

// Iterate over array elements
for (JsonValue value : items)
{
    JsonObject item = (JsonObject) value;
    String name = item.getAsString("name");
}

// Static convenience methods for parsing typed arrays
List<String> names = JsonArray.parseStrings("[\"Alice\", \"Bob\", \"Carol\"]");
List<Integer> ids = JsonArray.parseInts("[1, 2, 3]");
Reading Strings from JsonArray

When extracting string values from a JsonArray by index, use get(i).asString().getValue() to get the raw string. The getAsString(i) method returns the JSON representation which may include surrounding quotes. For bulk extraction, use getStringArray() which returns a properly unwrapped List<String>.

The Jsonable Interface

For structured data, implement the Jsonable interface to create typed Java classes that serialize to and from JSON. This is the recommended approach — use Jsonable classes instead of working with raw JsonObject instances for your application data.

Java
public class Product implements Jsonable
{
    private String name;
    private double price;
    private int quantity;
    private boolean inStock;

    public Product()
    {

    }

    public Product(JsonValue jv)
    {
        initFromJson(jv);
    }

    @Override
    public final void initFromJson(JsonValue jv)
    {
        JsonObject json = (JsonObject) jv;
        this.name = json.getAsString("name");
        this.price = json.getAsDouble("price");
        this.quantity = json.getAsInt("quantity");
        this.inStock = json.getAsBoolean("inStock");
    }

    @Override
    public JsonValue toJsonValue()
    {
        JsonObject json = new JsonObject();
        json.put("name", name);
        json.put("price", price);
        json.put("quantity", quantity);
        json.put("inStock", inStock);
        return json;
    }

    // Setters and getters...
}

The Jsonable pattern requires two constructors and two methods:

  • Default constructor — for creating empty instances
  • JsonValue constructor — calls initFromJson(jv) for direct instantiation from JSON
  • initFromJson() — deserializes fields from a JsonValue (mark as final)
  • toJsonValue() — serializes fields to a JsonObject

Once a class implements Jsonable, it integrates seamlessly with JsonObject and JsonArray:

Java
// Add a Jsonable object to a JsonObject
Product product = new Product();
product.setName("Widget");
product.setPrice(29.99);

JsonObject order = new JsonObject();
order.put("product", product);  // Automatically calls toJsonValue()

// Add Jsonable objects to a JsonArray
JsonArray catalog = new JsonArray();
catalog.add(product1);  // Automatically calls toJsonValue()
catalog.add(product2);

// Deserialize from a JSON string
Product parsed = new Product(JsonObject.parse(jsonString));

// Convert to JSON string
String json = product.toJsonString();
Jsonable Best Practice

Always provide both constructors (default and JsonValue). Mark initFromJson as final to prevent subclasses from overriding the deserialization logic. If the default constructor has initialization logic needed by deserialization (e.g., instantiating nested objects), the JsonValue constructor should call this() first, then initFromJson(jv).

20. HTML Templates

While Oorian encourages building UIs programmatically in Java, it also supports HTML templates for cases where you want to start with existing HTML markup. HtmlTemplatePage loads an HTML file, parses it into a live Oorian element tree, and lets you modify anything from Java.

HtmlTemplatePage

Extend HtmlTemplatePage instead of HtmlPage and pass the path to your HTML file. Oorian parses the template's <head> and <body> into real Oorian elements, then calls createHead() and createBody() so you can modify them:

Java
@Page("/dashboard")
public class DashboardPage extends HtmlTemplatePage
{
    public DashboardPage()
    {
        super("/templates/dashboard.html");
    }

    @Override
    protected void createBody(Body body)
    {
        // Find elements from the template and modify them
        Element title = getElementById("page-title");
        title.setText("User Dashboard");

        // Add Oorian components into template placeholders
        Element content = getElementById("content");
        content.addElement(createStatsPanel());
        content.addElement(createRecentActivity());
    }
}

The corresponding template file (web/templates/dashboard.html):

HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Dashboard</title>
    <link rel="stylesheet" href="/css/dashboard.css">
</head>
<body>
    <div class="layout">
        <header>
            <h1 id="page-title">Placeholder</h1>
        </header>
        <main id="content">
            <!-- Oorian components will be added here -->
        </main>
        <footer>Copyright 2026</footer>
    </div>
</body>
</html>

How It Works

When the page is requested, HtmlTemplatePage automatically:

  1. Loads the HTML file from the servlet context
  2. Parses it with Oorian's Html5Parser into a full element tree
  3. Copies attributes from the template's <html> element (e.g., lang, dir) to the page
  4. Extracts the <head> and <body> and sets them as the page's head and body
  5. Calls createHead() and createBody() for your modifications

Every element in the parsed tree is a real Oorian element — you can register event listeners, apply styles, add child components, and use sendUpdate() to push changes, just like elements created in Java.

Finding and Modifying Elements

Use getElementById() to access specific elements by their id attribute, or getElementsByTagName() to find elements by tag name:

Java
@Override
protected void createBody(Body body)
{
    // Find a single element by ID
    Element sidebar = getElementById("sidebar");
    sidebar.addElement(createNavMenu());

    // Find all elements by tag name
    Elements buttons = body.getElementsByTagName("button", true);

    for (Element button : buttons)
    {
        button.registerListener(this, MouseClickedEvent.class);
    }
}

Modifying the Head

Override createHead() to add resources or meta tags to the template's existing head content:

Java
@Override
protected void createHead(Head head)
{
    // The head already contains everything from the template.
    // Add additional resources as needed.
    head.addCssLink("/css/extra-styles.css");
    head.addElement(new JavaScript("/js/analytics.js"));
}

Character Encoding

Templates are read as UTF-8 by default. To use a different encoding, call setCharset() in your constructor:

Java
public LegacyPage()
{
    super("/templates/legacy.html");
    setCharset("ISO-8859-1");
}

Parsing HTML Directly

For cases where you need to parse an HTML string rather than load a file, use Html5Parser directly. The parser supports all standard HTML5 elements, void elements, CDATA sections, entity references, and nested content:

Java
Html5Parser parser = new Html5Parser();

Element content = parser.parse(
    "<div class='card'>" +
    "  <h2>Welcome</h2>" +
    "  <p>Hello, world!</p>" +
    "</div>"
);

body.addElement(content);
When to Use Templates

Templates are useful when migrating existing HTML to Oorian, working with designers who provide HTML mockups, or when a layout is complex enough that HTML is more readable than programmatic construction. Since parsed elements are fully live Oorian elements, you get the best of both worlds — HTML for structure, Java for behavior.

21. Standalone HTML Generation

Oorian can be used as a standalone library to generate HTML without running a web server. This is ideal for generating emails, newsletters, reports, invoices, and any other content that needs properly structured HTML.

Generating HTML for Emails

Create Oorian elements, style them, and call getInnerHtml() to produce an HTML string that you can pass to your email sending library:

Java
Div container = new Div();
container.setMaxWidth(600);
container.setMargin("0 auto");
container.setFontFamily("Arial, sans-serif");

H1 title = new H1();
title.setText("Welcome to Our Service!");
title.setColor("#1f2937");
container.addElement(title);

P greeting = new P();
greeting.setText("Hello " + userName + ",");
container.addElement(greeting);

P body = new P();
body.setText("Thank you for signing up. Click the link below to verify your account.");
container.addElement(body);

A link = new A(verificationUrl);
link.setText("Verify Your Account");
link.setBackgroundColor("#2563eb");
link.setColor(Color.WHITE);
link.setPadding(12, 24);
link.setBorderRadius(6);
link.addStyleAttribute("text-decoration", "none");
link.setDisplay(Display.INLINE_BLOCK);
container.addElement(link);

// Get the HTML string
String html = container.getInnerHtml();

Benefits

Using Oorian for HTML generation gives you the same advantages as building web pages:

  • Type-safe — Compile-time checking prevents HTML errors
  • Reusable components — Create email templates as reusable Java classes
  • Full IDE support — Autocomplete and refactoring for every element and style
  • No template syntax — No Thymeleaf, FreeMarker, or Velocity to learn
  • Testable — Unit test your HTML generation with standard Java testing frameworks
Minimal Dependencies

When using Oorian for standalone HTML generation, you only need the core Oorian library. No servlet container, no web server, and no frontend toolchain are required.

22. HttpFile and Custom Content Types

Every page in Oorian ultimately extends HttpFile, the abstract base class that manages the HTTP request/response lifecycle. While most applications work with HtmlPage for interactive HTML, you can extend HttpFile directly to serve any content type — JSON APIs, CSV downloads, XML feeds, binary files, or anything else that responds to an HTTP request.

Class Hierarchy

HttpFile sits at the root of all request-handling classes in Oorian:

Class Hierarchy
HttpFile                         // Abstract base — any HTTP content
├── HtmlPage                     // Interactive HTML pages (AJAX/SSE/WebSocket)
├── TextPage                     // Simple text responses (JSON, XML, CSV, etc.)
└── CssFile                      // Programmatic CSS stylesheets

All three subclasses inherit the same lifecycle, routing, parameter handling, and session integration. The difference is how they generate their output.

Extending HttpFile Directly

To serve custom content, extend HttpFile and implement three methods:

Method Purpose
initializeFile() Set content type, headers, and cache control. Return false to abort.
createFile() Build the response content. Access parameters, session, and URL parameters here.
toString(StringBuilder) Append the final output to the buffer.
Java
@Page("/api/users/{id}")
public class UserApiEndpoint extends HttpFile
{
    private String jsonOutput;

    @Override
    protected boolean initializeFile()
    {
        setContentType("application/json");
        setCharacterEncoding("UTF-8");
        return true;
    }

    @Override
    protected void createFile()
    {
        Integer userId = getParameterAsInt("id");
        User user = UserService.findById(userId);

        JsonObject json = new JsonObject();
        json.put("id", user.getId());
        json.put("name", user.getName());
        json.put("email", user.getEmail());
        jsonOutput = json.toString();
    }

    @Override
    protected void toString(StringBuilder sb)
    {
        sb.append(jsonOutput);
    }
}

Using TextPage for Simple Responses

TextPage simplifies the common case of returning a text string. It implements toString(StringBuilder) for you — just call setText() with your content:

Java
@Page("/api/status")
public class StatusEndpoint extends TextPage
{
    @Override
    protected boolean initializeFile()
    {
        setContentType("application/json");
        return true;
    }

    @Override
    protected void createFile()
    {
        JsonObject json = new JsonObject();
        json.put("status", "OK");
        json.put("uptime", System.currentTimeMillis() - startTime);
        setText(json.toString());
    }
}

Request Lifecycle

Every HttpFile subclass follows the same lifecycle when a request arrives:

initializeFile()
Apply headers
createFile()
onCreated()
toString(sb)
onWriteComplete()

If initializeFile() returns false, processing stops and no content is sent. Use this to enforce access control or validate preconditions.

Response Configuration

Configure the HTTP response from initializeFile() using these methods. Headers are collected and applied automatically after the method returns:

Method Description
setContentType(type) Set the MIME type (e.g., "application/json", "text/csv")
setCharacterEncoding(enc) Set the character encoding (e.g., "UTF-8")
setResponseHeader(name, value) Set a response header, replacing any existing value
addResponseHeader(name, value) Add a response header without replacing existing values
setCacheControl(policy) Set a CacheControl policy for the response

Accessing Request Data

HttpFile provides typed parameter accessors inherited by all subclasses, including HtmlPage:

Method Returns
getParameter(name) String
getParameterAsInt(name) Integer (null if not parseable)
getParameterAsLong(name) Long
getParameterAsShort(name) Short
getParameterAsFloat(name) Float
getParameterValues(name) String[] (multi-valued parameters)
getUrlParameters() UrlParameters (from @Page("/path/{id}") patterns)
hasParameter(name) boolean
hasUrlParams() boolean

For multi-valued parameters (checkbox groups, multi-select lists), use the plural forms: getParametersAsInt(name), getParametersAsLong(name), getParametersAsShort(name), and getParametersAsFloat(name). These return a List of the corresponding type.

URL Generation

Generate URLs for any HttpFile subclass using the static helper methods. These read the @Page annotation and substitute URL parameters:

Java
// Get the raw path from the annotation
String path = HttpFile.getPath(UserApiEndpoint.class);
// Returns: "/api/users/{id}"

// Substitute URL parameters
String url = HttpFile.getUrl(UserApiEndpoint.class, 42);
// Returns: "/api/users/42"
When to Use Each Base Class

Use HtmlPage for interactive pages with UI elements and event handling. Use TextPage for simple API responses where you just need to return a string. Use HttpFile directly when you need full control over the output buffer, such as streaming large content or building output incrementally.

23. UI Extensions

Oorian's UI extension libraries are Java wrappers for popular JavaScript UI libraries. Each wrapper gives you a clean Java API while preserving the full power of the underlying library. There are over 50 wrappers available across a wide range of categories.

Available Categories

  • Full UI Platforms — Webix, DHTMLX, Syncfusion, Kendo UI, Wijmo, DevExtreme
  • Web Components & CSS Frameworks — Shoelace, Web Awesome, Bootstrap, Tailwind CSS
  • Rich Text Editors — CKEditor, TinyMCE, Quill, ProseMirror, Monaco Editor, TipTap, CodeMirror, Froala
  • Data Grids — AG Grid, Handsontable, Tabulator, DataTables
  • Charts — Chart.js, Highcharts, ECharts, D3.js, ApexCharts, Plotly.js, AnyChart
  • Maps — Leaflet, Mapbox, OpenLayers, Google Maps
  • Scheduling — FullCalendar, vis.js
  • And more — Diagrams, document viewers, carousels, notifications, media players, image tools, tour/onboarding

Using an Extension Library

Each extension library provides a static addAllResources() method that loads the required CSS and JavaScript files. Then you use the wrapper classes just like any other Oorian element. Here is a complete example using Chart.js:

Java
@Page("/analytics")
public class AnalyticsPage extends HtmlPage
{
    @Override
    protected void createHead(Head head)
    {
        head.setTitle("Analytics");
        ChartJs.addAllResources(head);
    }

    @Override
    protected void createBody(Body body)
    {
        CjsChart chart = new CjsChart(CjsChart.BAR);
        chart.setWidth(600);
        chart.setHeight(400);

        CjsDataSet sales = chart.addDataSet("Monthly Sales");
        sales.setData(120, 190, 300, 500, 200, 300);

        chart.setLabels("Jan", "Feb", "Mar", "Apr", "May", "Jun");

        body.addElement(chart);
    }
}

Consistent Conventions

Every Oorian wrapper follows the same conventions, so once you learn the pattern with one library, every other library feels familiar:

  • Java-first configuration — All options are set through Java methods, not JavaScript objects or JSON
  • Event integration — Library-specific events are dispatched through Oorian's standard listener system
  • Resource management — A single addAllResources() call loads everything the library needs
  • Fluent API — Setters return this for method chaining where appropriate

For the full list of UI extension libraries and their documentation, see the Extensions page.

24. Add-Ons

In addition to UI extension libraries that wrap JavaScript components, Oorian provides add-on libraries — server-side integrations for analytics, payments, authentication, error tracking, and other services. Add-ons run entirely on the server and do not wrap a browser-side UI library. Instead they connect your application to third-party platforms and APIs through a clean Java interface.

How Add-Ons Differ from UI Extensions

UI Extensions Add-Ons
Purpose Wrap a client-side JavaScript UI library Integrate a server-side service or API
Runs where Browser (JavaScript) + Server (Java bridge) Server only (pure Java)
Examples Chart.js, AG Grid, Leaflet Google Analytics, Stripe, Sentry
Resource loading addAllResources(head) loads CSS/JS No client-side resources required

Add-On Categories

Add-ons are organized into the following categories:

Category Description Examples
Analytics & Tracking Track user behavior, page views, and application usage Google Analytics, Matomo, Plausible
Error Tracking & Monitoring Capture and report application errors in real time Sentry, Rollbar
Authentication & Security User authentication, bot protection, and security services reCAPTCHA, hCaptcha, Auth0
Content Processing Server-side content transformation and rendering Markdown processing, syntax highlighting
Payments & Commerce Process payments and manage subscriptions Stripe, PayPal
PWA & Push Notifications Progressive Web App support and push notification delivery Service worker integration, Web Push API
Privacy & Compliance Cookie consent, GDPR compliance, and privacy management Cookie consent banners, privacy policy tools
Third-Party Integrations Connect to external platforms and services Social login, email services, cloud storage

Using an Add-On

Because add-ons are server-side, there are no client resources to load. You work with them the same way you work with any Java library — create an instance, configure it, and call its methods.

Some add-ons inject a small script tag into the page (for example, an analytics tracking snippet). These add-ons provide a helper method you call from createHead():

Java
@Page("/dashboard")
public class DashboardPage extends HtmlPage
{
    @Override
    protected void createHead(Head head)
    {
        head.setTitle("Dashboard");

        // Add analytics tracking snippet to the page
        GoogleAnalytics.addTracking(head, "G-XXXXXXXXXX");
    }
}

Other add-ons are purely server-side and are used in event handlers or business logic:

Java
// Verify a reCAPTCHA token on form submission
@Override
public void onEvent(FormEvent event)
{
    String token = event.getParameters().getParameterValue("g-recaptcha-response");

    if (ReCaptcha.verify(token, SECRET_KEY))
    {
        processRegistration(event.getParameters());
    }
    else
    {
        showError("Captcha verification failed.");
    }
}

Consistent Conventions

Add-ons follow the same principles as UI extensions:

  • Java-first configuration — All settings are passed through Java methods, not configuration files or JavaScript
  • No vendor lock-in — Switch analytics providers, payment processors, or error trackers without rewriting your application
  • Fluent API — Setters return this for method chaining where appropriate
Extensions vs Add-Ons

Both UI extensions and add-ons are listed on the Extensions page. Use the filter to view just add-ons, just UI extensions, or browse everything together.

25. Logging

Oorian provides a lightweight logging facade through the OorianLog class. Rather than bundling a specific logging framework, Oorian delegates to the JDK's System.Logger platform logging SPI, introduced in Java 9. This means your application can use any logging backend — SLF4J, Log4j2, java.util.logging, or any other provider — without Oorian requiring additional dependencies.

Getting a Logger

Create a logger instance as a private static final field in each class that needs logging. Pass the class reference to OorianLog.getLogger():

Java
public class OrderService
{
    private static final OorianLog LOG = OorianLog.getLogger(OrderService.class);

    public void processOrder(String orderId)
    {
        LOG.info("Processing order {0}", orderId);

        try
        {
            // ... process the order
            LOG.debug("Order {0} completed successfully", orderId);
        }
        catch (Exception ex)
        {
            LOG.error("Failed to process order", ex);
        }
    }
}

Each logger is named after the fully qualified class name, so log output always identifies its source. The logger instance is lightweight and safe to store as a static field.

Log Levels

OorianLog provides five log levels, each mapped to a System.Logger.Level:

Level Method Use For
TRACE LOG.trace() Fine-grained detail: per-message operations, heartbeats, pings, filter pass-through
DEBUG LOG.debug() Diagnostic information: request routing, connections opening and closing, session lifecycle
INFO LOG.info() Lifecycle events: application startup and shutdown, license status, registration counts
WARNING LOG.warning() Recoverable issues: CSRF validation failures, missing resources, security events
ERROR LOG.error() Failures: exceptions that affect functionality, unrecoverable errors
Keep INFO Quiet

Reserve INFO for true lifecycle events. A healthy application running in steady state should produce little or no INFO output. Use DEBUG for request-level detail and TRACE for per-message granularity.

Method Overloads

Each level provides three overloads for different use cases:

Java
// Simple message
LOG.info("Application started");

// Message with exception (stack trace is included in the output)
LOG.error("Failed to load configuration", ex);

// Parameterized message using MessageFormat syntax: {0}, {1}, ...
LOG.debug("Processing page {0} for session {1}", pageId, sessionId);

Parameterized messages use java.text.MessageFormat syntax with numbered placeholders ({0}, {1}, etc.). This is more efficient than string concatenation because the message is only formatted if the level is enabled.

Guarding Expensive Construction

If building a log message is expensive (e.g., serializing objects or iterating over large collections), use isLoggable() to check whether the level is enabled before constructing the message:

Java
if (LOG.isLoggable(System.Logger.Level.DEBUG))
{
    String details = buildExpensiveDebugInfo(request);
    LOG.debug("Request details: {0}", details);
}

For simple parameterized messages, this guard is not necessary — the framework avoids formatting the message if the level is disabled.

Pluggable Backends

By default, System.Logger delegates to java.util.logging (JUL). To route Oorian log output through a different logging framework, add the appropriate bridge library to your classpath:

Backend Bridge Dependency
SLF4J (Logback, etc.) org.slf4j:slf4j-jdk-platform-logging
Log4j2 org.apache.logging.log4j:log4j-jpl
JUL (default) No additional dependency required

Once a bridge is on the classpath, all Oorian log output is automatically routed through your chosen backend. You configure log levels, appenders, and formatting using the backend's own configuration files (e.g., logback.xml for Logback, log4j2.xml for Log4j2).

No Configuration Required

Oorian does not require any logging configuration to run. Out of the box, log output goes through JUL with the JVM's default settings. Add a bridge library only if you want to unify Oorian's logging with your application's existing logging framework.

Configuring Log Levels

How you set the active log level depends on which backend is in use. With the default JUL backend, create or edit a logging.properties file:

logging.properties
# Default level for all loggers
.level = INFO

# Show all Oorian framework logging at DEBUG level
com.oorian.level = FINE

# Show a specific class at TRACE level
com.oorian.html.HtmlPage.level = FINER

# Console handler configuration
handlers = java.util.logging.ConsoleHandler
java.util.logging.ConsoleHandler.level = ALL

JUL uses different level names than System.Logger. The mapping is:

OorianLog Method System.Logger Level JUL Level
trace() TRACE FINER
debug() DEBUG FINE
info() INFO INFO
warning() WARNING WARNING
error() ERROR SEVERE

Point the JVM to your properties file using the system property:

JVM Argument
-Djava.util.logging.config.file=/path/to/logging.properties

If you are using SLF4J or Log4j2 as your backend, configure levels using that framework's own configuration file (logback.xml, log4j2.xml, etc.) with the com.oorian logger name hierarchy.

Complete API Reference

Method Description
OorianLog.getLogger(Class<?>) Creates a logger instance named after the given class
isLoggable(System.Logger.Level) Returns true if the given level is enabled
trace(String) Logs a TRACE-level message
trace(String, Throwable) Logs a TRACE-level message with an exception
trace(String, Object...) Logs a TRACE-level parameterized message
debug(String) Logs a DEBUG-level message
debug(String, Throwable) Logs a DEBUG-level message with an exception
debug(String, Object...) Logs a DEBUG-level parameterized message
info(String) Logs an INFO-level message
info(String, Throwable) Logs an INFO-level message with an exception
info(String, Object...) Logs an INFO-level parameterized message
warning(String) Logs a WARNING-level message
warning(String, Throwable) Logs a WARNING-level message with an exception
warning(String, Object...) Logs a WARNING-level parameterized message
error(String) Logs an ERROR-level message
error(String, Throwable) Logs an ERROR-level message with an exception
error(String, Object...) Logs an ERROR-level parameterized message

26. Error Handling

Oorian provides a layered error handling system that covers custom error pages, built-in defaults, centralized exception monitoring, and development-mode diagnostics. All error handling features are opt-in and configured in your Application.initialize() method.

Custom Error Pages

Custom error pages extend ErrorPage and implement a single method: createBody(Body body). Build your page using standard Oorian elements (H1, Paragraph, Div, etc.) — the base class handles the HTML document skeleton, charset, viewport, and a minimal CSS reset.

Java
public class NotFoundPage extends ErrorPage
{
    @Override
    protected void createBody(Body body)
    {
        body.addElement(new H1("Error " + getStatusCode()));
        body.addElement(new Paragraph(getMessage()));
        body.addElement(new Paragraph("Path: " + getRequestPath()));
    }
}

The ErrorPage base class provides convenience getters for error details:

Method Description
getStatusCode() The HTTP status code (e.g., 404, 500)
getRequestPath() The original request path that triggered the error
getException() The exception that caused the error, or null
getTitle() Human-readable title for the status code (e.g., "Page Not Found")
getMessage() Descriptive message for the status code

To add custom styling, use inline styles on your elements. To add shared CSS rules, override createHead(Head head) and add a Style element:

Java
public class BrandedErrorPage extends ErrorPage
{
    @Override
    protected void createBody(Body body)
    {
        H1 title = new H1(getTitle());
        title.setColor("#2563eb");
        title.setTextAlign("center");
        body.addElement(title);

        body.addElement(new Paragraph(getMessage()));
    }
}
ErrorPage vs HtmlPage

Error pages extend ErrorPage, not HtmlPage. They are lightweight, standalone HTML pages that avoid the WebSocket/event overhead and render reliably even when the normal page infrastructure has failed. Build your content with the same Oorian elements (Div, H1, Paragraph, etc.) but without the event system.

Registering Error Pages

Register custom error pages for specific HTTP status codes using Application.setErrorPage(). When the framework encounters an error, it looks up the registered page for that status code and renders it instead of the container's default error page.

Java
@WebListener
public class MyApp extends Application
{
    @Override
    protected void initialize(ServletContext context)
    {
        registerPackage("com.myapp");

        // Register custom error pages for specific status codes
        setErrorPage(404, NotFoundPage.class);
        setErrorPage(403, ForbiddenPage.class);
        setErrorPage(500, ServerErrorPage.class);

        // Set a default for any status code without a specific page
        setDefaultErrorPage(GenericErrorPage.class);
    }
}

The lookup order is: status-specific page → default error page → built-in default.

Default Error Pages

Oorian includes built-in error pages that work out of the box with no configuration. These extend ErrorPage for maximum reliability — they use only inline styles and have no external dependencies.

Class Status Code Title
DefaultNotFoundPage 404 Page Not Found
DefaultForbiddenPage 403 Forbidden
DefaultServerErrorPage 500 Internal Server Error
DefaultErrorPage Any Determined by status code

If no custom error pages are registered, the built-in DefaultErrorPage automatically activates as the fallback for all status codes — including requests to non-existent URLs. All default pages render a clean, user-friendly message with the status code, description, and a "Return to Home" link. Dynamic content is HTML-escaped to prevent XSS.

Centralized Exception Handler

For application-wide exception monitoring, logging, and alerting, implement the ExceptionHandler interface and register it in your Application class:

Java
public class AppExceptionHandler implements ExceptionHandler
{
    private static final OorianLog LOG =
        OorianLog.getLogger(AppExceptionHandler.class);

    @Override
    public void handle(Exception exception, HtmlPage page)
    {
        // Log to your monitoring system
        LOG.error("Unhandled exception", exception);

        // Send an alert (e.g., email, Slack, PagerDuty)
        alertService.notify(exception);
    }
}
Java
@Override
protected void initialize(ServletContext context)
{
    registerPackage("com.myapp");

    // Register a centralized exception handler
    setExceptionHandler(new AppExceptionHandler());
}

The handler integrates at three points in the framework:

  • Page creation — exceptions during initializePage(), createHead(), or createBody()
  • WebSocket message handling — exceptions while processing client events
  • Worker threads — exceptions in OorianWorkerThread execution

The centralized handler is invoked before the page-level onException() hook, giving you a single place to add logging or alerting without modifying individual pages. Handler errors are wrapped in a try-catch to prevent cascading failures.

Development Mode

Development mode controls how much detail error pages display. In production (the default), error pages show a user-friendly message with no technical details. In development mode, they include the request path, exception class, message, and full stack trace.

Java
@Override
protected void initialize(ServletContext context)
{
    registerPackage("com.myapp");

    // Enable development mode programmatically
    setDevMode(true);
}

You can also activate development mode via a system property, which is useful for enabling it on development servers without changing code:

JVM Argument
-Doorian.mode=dev
Prefer System Properties Over Code

Using the -Doorian.mode=dev system property is the recommended approach. A setDevMode(true) call in initialize() can easily be overlooked during code review and accidentally deployed to production.

Never Enable Development Mode in Production

Development mode exposes stack traces, class names, and request paths that could help an attacker understand your application's internals. Always ensure devMode is disabled (the default) in production environments.

27. Security

Oorian includes a comprehensive security infrastructure to protect your applications. Because Oorian controls both the server-side rendering and the client-server communication, it can enforce security at the framework level. All security features are opt-in and configured in your Application.initialize() method, ensuring backward compatibility with existing applications.

Security Headers

Security headers instruct the browser to enable built-in protections against common attacks like clickjacking, MIME sniffing, and protocol downgrade. Oorian's SecurityHeaders class provides a fluent API for configuring these headers, which are then applied to every HTTP response via the framework's filter.

Java
@WebListener
public class MyApp extends Application
{
    @Override
    public void initialize(ServletContext context)
    {
        registerPackage("com.myapp");

        // Enable security headers with sensible defaults
        setSecurityHeaders(new SecurityHeaders());
    }
}

With no customization, SecurityHeaders applies these default headers to every response:

Header Default Value Purpose
Strict-Transport-Security max-age=31536000; includeSubDomains Enforce HTTPS for one year, including subdomains
X-Content-Type-Options nosniff Prevent MIME type sniffing
X-Frame-Options DENY Prevent framing (clickjacking protection)
X-XSS-Protection 1; mode=block Enable browser XSS filter
Referrer-Policy strict-origin-when-cross-origin Control referrer information sent with requests
Permissions-Policy geolocation=(), camera=(), microphone=() Restrict access to browser features

Customize individual headers as needed:

Java
SecurityHeaders headers = new SecurityHeaders()
    .setFrameOption(SecurityHeaders.FrameOption.SAMEORIGIN)
    .setReferrerPolicy(SecurityHeaders.ReferrerPolicy.NO_REFERRER)
    .setHstsMaxAge(63072000)           // 2 years
    .setHstsPreload(true)
    .setPermissionsPolicy("geolocation=(self), camera=()");

setSecurityHeaders(headers);

The FrameOption enum provides DENY (prevents all framing) and SAMEORIGIN (allows framing from the same origin). The ReferrerPolicy enum includes all standard values: NO_REFERRER, NO_REFERRER_WHEN_DOWNGRADE, ORIGIN, ORIGIN_WHEN_CROSS_ORIGIN, SAME_ORIGIN, STRICT_ORIGIN, STRICT_ORIGIN_WHEN_CROSS_ORIGIN, and UNSAFE_URL.

HTTPS Enforcement

Force all HTTP traffic to HTTPS with a single configuration call. When enabled, Oorian's filter intercepts HTTP requests and returns a 301 Moved Permanently redirect to the HTTPS equivalent URL:

Java
@Override
public void initialize(ServletContext context)
{
    registerPackage("com.myapp");
    setHttpsRequired(true);
}

HTTPS enforcement includes several practical safeguards:

  • Localhost exception — Requests to localhost and 127.0.0.1 are never redirected, so local development works without SSL certificates
  • Load balancer support — The filter checks the X-Forwarded-Proto header, so HTTPS termination at a reverse proxy or load balancer is correctly detected
  • Query string preservation — The redirect preserves the original query string

CSRF Protection

Cross-Site Request Forgery (CSRF) protection is built into Oorian. When enabled, every form submission and AJAX request automatically includes a CSRF token that the server validates. Enable it in your Application class:

Java
@WebListener
public class MyApp extends Application
{
    @Override
    public void initialize(ServletContext context)
    {
        registerPackage("com.myapp");
        setCsrfProtectionEnabled(true);
    }

    @Override
    public void destroy(ServletContext context) { }
}

You can disable CSRF protection on specific pages that serve as public API endpoints:

Java
@Page("/api/webhook")
public class PublicApiPage extends HtmlPage
{
    @Override
    protected void createBody(Body body)
    {
        setCsrfProtectionEnabled(false);
        // ... page content
    }
}

CSRF Token Management

For advanced use cases, you can access and manage CSRF tokens directly through the OorianSession:

Java
OorianSession session = OorianSession.get();

// Get the current CSRF token
String token = session.getCsrfToken();

// Validate a token received from the client
boolean valid = session.validateCsrfToken(submittedToken);

// Regenerate the token (e.g., after authentication)
String newToken = session.regenerateCsrfToken();

Authentication Framework

Oorian provides a built-in authentication framework based on two interfaces: UserPrincipal (represents an authenticated user) and OorianAuthenticator (validates credentials). Combined with the @RequiresAuth annotation, this gives you declarative access control with minimal code.

Implementing UserPrincipal

Create a class that implements UserPrincipal to represent your application's authenticated users:

Java
public class AppUser implements UserPrincipal
{
    private final String id;
    private final String name;
    private final Set<String> roles;

    public AppUser(String id, String name, Set<String> roles)
    {
        this.id = id;
        this.name = name;
        this.roles = roles;
    }

    @Override
    public String getId() { return id; }

    @Override
    public String getName() { return name; }

    @Override
    public Set<String> getRoles() { return roles; }
}

The UserPrincipal interface also provides a default hasRole(String) method that checks against the roles set.

Implementing OorianAuthenticator

Create an authenticator that validates credentials and returns a UserPrincipal on success, or null on failure:

Java
public class MyAuthenticator implements OorianAuthenticator
{
    @Override
    public UserPrincipal authenticate(String username, String password)
    {
        // Look up user and verify password
        User user = userRepository.findByUsername(username);

        if (user != null && passwordHasher.verify(password, user.getPasswordHash()))
        {
            return new AppUser(user.getId(), user.getName(), user.getRoles());
        }

        return null;
    }
}

Application Configuration

Register your authenticator and login page in Application.initialize():

Java
@Override
public void initialize(ServletContext context)
{
    registerPackage("com.myapp");
    setAuthenticator(new MyAuthenticator());
    setLoginPage("/login");
}

Protecting Pages with @RequiresAuth

Annotate any page class with @RequiresAuth to restrict access to authenticated users. The annotation is inherited, so subclasses are automatically protected:

Java
@Page("/dashboard")
@RequiresAuth
public class DashboardPage extends HtmlPage
{
    @Override
    protected void createBody(Body body)
    {
        // Only reached if user is authenticated
        UserPrincipal user = getSession().getPrincipal();
        body.addElement(new H1().setText("Welcome, " + user.getName()));
    }
}

When an unauthenticated user accesses a @RequiresAuth page:

  • If a login page is configured, the user is redirected to it with a ?redirect= parameter so they return to the original page after login
  • If no login page is configured, the server returns HTTP 401 Unauthorized

Session Login and Logout

Use the OorianSession methods to manage authentication state:

Java
// On your login page, after form submission:
OorianAuthenticator auth = Application.getAuthenticator();
UserPrincipal principal = auth.authenticate(username, password);

if (principal != null)
{
    getSession().login(principal);    // Stores principal, regenerates CSRF token
    navigateTo(redirectUrl);
}

// Logout:
getSession().logout();               // Clears principal, invalidates session
navigateTo("/login");

Password Hashing

Oorian provides the PasswordHasher interface with two production-ready implementations for securely hashing and verifying passwords. Both implementations generate cryptographically random salts automatically and use constant-time comparison to prevent timing attacks.

BcryptHasher

The recommended default. Bcrypt is memory-hard and GPU-resistant:

Java
PasswordHasher hasher = new BcryptHasher();       // Default cost: 12
PasswordHasher strong = new BcryptHasher(14);      // Higher cost for high-security apps

// Hash a password (salt is generated automatically)
String hash = hasher.hash("user-password");
// Produces: $2a$12$...

// Verify a password against a stored hash
boolean valid = hasher.verify("user-password", storedHash);

The cost parameter controls the work factor (valid range: 4–31). Higher values are more secure but slower:

Cost Approximate Time Use Case
10 ~65ms Development and testing
12 ~250ms Most applications (default)
14 ~1s High-security applications

Pbkdf2Hasher

A standards-based alternative using PBKDF2-HMAC-SHA256 with no external dependencies (pure JDK):

Java
PasswordHasher hasher = new Pbkdf2Hasher();          // Default: 210,000 iterations (OWASP 2023)
PasswordHasher custom = new Pbkdf2Hasher(600000);    // Custom iteration count (min: 10,000)

String hash = hasher.hash("user-password");
// Produces: PBKDF2:210000:BASE64_SALT:BASE64_HASH

boolean valid = hasher.verify("user-password", storedHash);
Choose Bcrypt for New Applications

Bcrypt is the recommended choice for most applications because it is memory-hard and resistant to GPU-based attacks. Use Pbkdf2Hasher when you need a NIST-approved algorithm or have compliance requirements.

Session Security

Oorian's OorianSession wraps the servlet container's session with additional security features.

Session ID Rotation

After a user authenticates, rotate the session ID to prevent session fixation attacks. The rotateSession() method generates a new session ID while preserving all session attributes, page cache, and client profile:

Java
// After verifying credentials:
OorianSession session = getSession();
session.rotateSession();          // New session ID, CSRF token regenerated
session.login(principal);          // Store the authenticated user
Automatic CSRF Regeneration

Both rotateSession() and login() automatically regenerate the CSRF token. If you call login() after rotateSession(), the token is regenerated twice, which is harmless but redundant. You can call either one — both protect against session fixation.

Best Practices

  • Set session timeouts — Configure appropriate session timeouts for your application's security requirements
  • Invalidate on logout — Always call session.logout() or session.invalidate() when a user logs out
  • Rotate on authentication — Call rotateSession() or login() after successful authentication to prevent session fixation
  • Use secure cookies — Configure cookie defaults (see next section) to enforce HttpOnly, Secure, and SameSite attributes

Secure Cookie Defaults

The CookieDefaults class lets you configure security attributes that are automatically applied to session cookies and any OorianCookie instances. This ensures consistent cookie security across your application:

Java
@Override
public void initialize(ServletContext context)
{
    registerPackage("com.myapp");

    setSecureCookieDefaults(new CookieDefaults()
        .setHttpOnly(true)                              // Prevent JavaScript access (default)
        .setSecure(true)                                // HTTPS only
        .setSameSite(CookieDefaults.SameSite.STRICT)   // Strict same-site enforcement
    );
}
Attribute Default Description
HttpOnly true Prevents JavaScript from accessing the cookie, mitigating XSS theft
Secure false Cookie only sent over HTTPS. Set to true in production
SameSite Lax Controls cross-site cookie sending

The SameSite enum provides three values:

  • STRICT — Cookie is never sent with cross-site requests (maximum protection)
  • LAX — Cookie is sent with top-level navigations but not with embedded requests (default, good balance)
  • NONE — Cookie is always sent (requires Secure=true)

Content Security Policy

A Content Security Policy (CSP) tells the browser which sources of content are allowed on your page, preventing cross-site scripting (XSS) and data injection attacks. Oorian's ContentSecurityPolicy class provides a fluent builder for constructing CSP headers.

Basic Usage

Build a policy and apply it to the page's Head element as a <meta> tag:

Java
ContentSecurityPolicy csp = new ContentSecurityPolicy()
    .addDefaultSrc(ContentSecurityPolicy.SELF)
    .addScriptSrc(ContentSecurityPolicy.SELF, "https://cdn.example.com")
    .addStyleSrc(ContentSecurityPolicy.SELF, ContentSecurityPolicy.UNSAFE_INLINE)
    .addImgSrc(ContentSecurityPolicy.SELF, "data:", "https:")
    .upgradeInsecureRequests();

// Apply as a meta tag on a specific page
head.setContentSecurityPolicy(csp);

Application-Wide CSP via HTTP Header

For a consistent policy across all pages, configure CSP at the application level. This delivers the policy as an HTTP response header rather than a meta tag:

Java
@Override
public void initialize(ServletContext context)
{
    registerPackage("com.myapp");

    ContentSecurityPolicy csp = new ContentSecurityPolicy()
        .addDefaultSrc(ContentSecurityPolicy.SELF)
        .addScriptSrc(ContentSecurityPolicy.SELF)
        .addStyleSrc(ContentSecurityPolicy.SELF, ContentSecurityPolicy.UNSAFE_INLINE)
        .addObjectSrc(ContentSecurityPolicy.NONE)
        .addFrameAncestors(ContentSecurityPolicy.NONE)
        .addBaseUri(ContentSecurityPolicy.SELF);

    // Enforcing mode — sends Content-Security-Policy header
    setContentSecurityPolicy(csp);

    // Or report-only mode — sends Content-Security-Policy-Report-Only header
    setContentSecurityPolicy(csp, true);
}

Source Constants

The ContentSecurityPolicy class provides constants for common CSP source keywords:

Constant Value Description
SELF 'self' Same origin only (same scheme, host, and port)
NONE 'none' Block all sources for this directive
UNSAFE_INLINE 'unsafe-inline' Allow inline scripts or styles (use sparingly)
UNSAFE_EVAL 'unsafe-eval' Allow eval() and similar (use sparingly)
STRICT_DYNAMIC 'strict-dynamic' Trust scripts loaded by already-trusted scripts
UNSAFE_HASHES 'unsafe-hashes' Allow specific inline event handlers matched by hash

Directives

Each add* method accepts one or more source values (strings or constants). Calling the same directive multiple times accumulates sources rather than replacing them:

Java
ContentSecurityPolicy csp = new ContentSecurityPolicy();

// Fetch directives — control where resources can be loaded from
csp.addDefaultSrc(ContentSecurityPolicy.SELF);      // Fallback for all fetch directives
csp.addScriptSrc(ContentSecurityPolicy.SELF);       // JavaScript sources
csp.addStyleSrc(ContentSecurityPolicy.SELF);        // Stylesheet sources
csp.addImgSrc(ContentSecurityPolicy.SELF);          // Image sources
csp.addFontSrc(ContentSecurityPolicy.SELF);         // Font sources
csp.addConnectSrc(ContentSecurityPolicy.SELF);      // AJAX, WebSocket, SSE sources
csp.addMediaSrc(ContentSecurityPolicy.SELF);        // Audio and video sources
csp.addObjectSrc(ContentSecurityPolicy.NONE);       // Object/embed/applet sources
csp.addFrameSrc(ContentSecurityPolicy.SELF);        // Frame and iframe sources
csp.addChildSrc(ContentSecurityPolicy.SELF);        // Nested browsing and workers
csp.addWorkerSrc(ContentSecurityPolicy.SELF);       // Worker script sources
csp.addManifestSrc(ContentSecurityPolicy.SELF);     // Application manifest sources

// Navigation and framing directives
csp.addBaseUri(ContentSecurityPolicy.SELF);          // Restrict <base> URLs
csp.addFormAction(ContentSecurityPolicy.SELF);       // Restrict form submission targets
csp.addFrameAncestors(ContentSecurityPolicy.NONE);  // Who can frame this page (clickjacking protection)

// Special directives
csp.upgradeInsecureRequests();                      // Upgrade HTTP to HTTPS
csp.setReportUri("/csp-report");                     // Violation report endpoint
csp.setReportTo("csp-group");                       // Reporting API group name
csp.addSandbox("allow-scripts", "allow-forms");   // Sandbox restrictions

Nonce and Hash Sources

Instead of allowing 'unsafe-inline', use nonces or hashes to permit specific inline scripts and styles:

Java
// Allow a specific inline script by nonce
String myNonce = "abc123";
csp.addScriptSrc(ContentSecurityPolicy.nonce(myNonce));
// Adds: 'nonce-abc123'

// Allow a specific inline script by hash
csp.addScriptSrc(ContentSecurityPolicy.sha256("base64EncodedHash..."));
// Adds: 'sha256-base64EncodedHash...'

// SHA-384 and SHA-512 are also available
csp.addStyleSrc(ContentSecurityPolicy.sha384(hash));
csp.addStyleSrc(ContentSecurityPolicy.sha512(hash));

Report-Only Mode

Test a new policy without blocking anything. Violations are reported but content is still loaded:

Java
// Per-page: report-only mode — violations logged but not enforced
head.setContentSecurityPolicyReportOnly(csp);

// Per-page: enforcement mode — violations are blocked
head.setContentSecurityPolicy(csp);
Start with Report-Only

When deploying CSP for the first time, use report-only mode with a setReportUri() endpoint. Monitor the violation reports to identify what your policy needs to allow before switching to enforcement mode.

Cache Control

The CacheControl class provides a fluent builder for constructing Cache-Control HTTP headers. Use it to control how browsers and proxies cache your responses:

Java
// Custom cache control header
CacheControl cc = new CacheControl()
    .setPublic()
    .setMaxAge(3600)
    .mustRevalidate();
// Produces: "public, must-revalidate, max-age=3600"

CacheControl provides factory methods for common caching strategies:

Java
// Static resources — cached for one year, immutable
CacheControl cc = CacheControl.staticResources();
// Produces: "public, max-age=31536000, immutable"

// Prevent all caching (sensitive data)
CacheControl cc = CacheControl.preventCaching();
// Produces: "no-store"

// Always revalidate with server
CacheControl cc = CacheControl.revalidate();
// Produces: "no-cache"

Available directives:

Method Directive Description
setPublic() public Response can be cached by any cache
setPrivate() private Response is for a single user only
noCache() no-cache Must revalidate before using cached copy
noStore() no-store Do not cache at all
noTransform() no-transform Proxies must not modify the response
mustRevalidate() must-revalidate Stale responses must not be used
immutable() immutable Response will never change (use with versioned URLs)
setMaxAge(int) max-age=N Maximum age in seconds
setSMaxAge(int) s-maxage=N Maximum age for shared caches (CDNs, proxies)

Input Handling

Oorian provides several layers of protection for user input:

  • XSS prevention — Text set via setText() is automatically escaped. Use RawHtml only when you explicitly need unescaped content
  • No direct SQL — Oorian doesn't include a database layer. Use parameterized queries with your preferred database library to prevent SQL injection
  • Validated forms — Use ValidatedForm for server-side input validation on all user-submitted data
Always Validate on the Server

Client-side validation improves user experience, but server-side validation is the security boundary. Never trust client-submitted data without server-side validation. Oorian's ValidatedForm validates on the server, ensuring malicious clients cannot bypass validation rules.

Comprehensive Example

Here is a fully-configured Application class enabling multiple security features together:

Java
@WebListener
public class SecureApp extends Application
{
    @Override
    public void initialize(ServletContext context)
    {
        registerPackage("com.myapp");

        // HTTPS enforcement
        setHttpsRequired(true);

        // Security headers
        setSecurityHeaders(new SecurityHeaders()
            .setFrameOption(SecurityHeaders.FrameOption.DENY)
            .setHstsMaxAge(63072000)
            .setHstsPreload(true)
        );

        // Secure cookie defaults
        setSecureCookieDefaults(new CookieDefaults()
            .setSecure(true)
            .setSameSite(CookieDefaults.SameSite.STRICT)
        );

        // Content Security Policy
        setContentSecurityPolicy(new ContentSecurityPolicy()
            .addDefaultSrc(ContentSecurityPolicy.SELF)
            .addScriptSrc(ContentSecurityPolicy.SELF)
            .addStyleSrc(ContentSecurityPolicy.SELF, ContentSecurityPolicy.UNSAFE_INLINE)
            .addImgSrc(ContentSecurityPolicy.SELF, "data:")
            .addObjectSrc(ContentSecurityPolicy.NONE)
            .addFrameAncestors(ContentSecurityPolicy.NONE)
            .addBaseUri(ContentSecurityPolicy.SELF)
            .upgradeInsecureRequests()
        );

        // CSRF protection
        setCsrfProtectionEnabled(true);

        // Authentication
        setAuthenticator(new MyAuthenticator());
        setLoginPage("/login");
    }

    @Override
    public void destroy(ServletContext context) { }
}

28. Accessibility

Oorian provides built-in support for building accessible web applications. ARIA attributes are available on every element, and dedicated accessibility components handle common patterns like skip links, live regions, and focus traps.

ARIA Attributes

Every Oorian element supports ARIA attributes through type-safe methods on the Element base class:

Java
Button menuButton = new Button();
menuButton.setRole("menubutton");
menuButton.setAriaLabel("Open main menu");
menuButton.setAriaExpanded(false);
menuButton.setAriaHasPopup(true);

Div dialog = new Div();
dialog.setRole(AriaRole.DIALOG);
dialog.setAriaLabelledBy("dialog-title");
dialog.setAriaDescribedBy("dialog-description");

TextInput emailInput = new TextInput();
emailInput.setAriaInvalid(true);
emailInput.setAriaDescribedBy("email-error");

Div decorative = new Div();
decorative.setAriaHidden(true);

Div tab = new Div();
tab.setAriaSelected(true);
tab.setTabIndex(0);

Skip Links

Skip links allow keyboard users to bypass navigation and jump directly to the main content. Oorian's SkipLink component handles the standard visually-hidden-until-focused pattern:

Java
@Override
protected void createBody(Body body)
{
    // Add skip link as the first element in the body
    body.addElement(new SkipLink("main-content", "Skip to main content"));

    // Navigation...
    body.addElement(createNavigation());

    // Main content with matching ID
    Div mainContent = new Div();
    mainContent.setId("main-content");
    mainContent.setTabIndex(-1);
    body.addElement(mainContent);
}

Live Regions

Live regions announce dynamic content changes to screen readers without moving focus. Use them for status messages, error notifications, and progress updates:

Java
// Create a polite live region (default) for status messages
LiveRegion statusRegion = new LiveRegion();
body.addElement(statusRegion);

// For urgent messages, use assertive mode
LiveRegion alertRegion = new LiveRegion(AriaLive.ASSERTIVE);
body.addElement(alertRegion);

// In an event handler, announce a message
statusRegion.announce("File saved successfully");

// For critical alerts
alertRegion.announce("Session will expire in 1 minute");

Focus Traps

Focus traps keep keyboard navigation within a container element, which is essential for modal dialogs. When activated, Tab and Shift+Tab cycle only through the focusable elements inside the container:

Java
// Create a modal dialog
Div modal = new Div();
modal.setRole(AriaRole.DIALOG);
modal.setId("my-modal");

// Create a focus trap for the modal
FocusTrap focusTrap = new FocusTrap(modal);

// When showing the modal, activate the trap
focusTrap.activate();

// When closing the modal, deactivate and return focus
focusTrap.deactivate(openButton);

Visually Hidden Text

The VisuallyHidden component renders text that is invisible to sighted users but readable by screen readers. Use it to add context to icon-only buttons or clarify ambiguous links:

Java
// Icon button with accessible label
Button deleteBtn = new Button();
deleteBtn.addElement(new Icon("trash"));
deleteBtn.addElement(new VisuallyHidden("Delete item"));

// Clarify a "Read more" link
A readMore = new A("/article/123");
readMore.setText("Read more");
readMore.addElement(new VisuallyHidden(" about Web Accessibility"));

29. Internationalization

Oorian includes a complete internationalization (i18n) framework that handles locale resolution, message translation, text direction, and locale-aware formatting. All i18n classes are in the com.oorian.i18n package.

Setting the Locale

Oorian stores the current user's locale in a thread-local variable via OorianLocale. The framework sets this automatically on each request using the configured locale resolver. You can also set it manually:

Java
// Set the locale for the current request thread
OorianLocale.set(Locale.GERMANY);

// Get the current locale
Locale current = OorianLocale.get();

// Clear when done (framework handles this automatically)
OorianLocale.clear();

Configure the application-wide default locale in your Application class:

Java
@Override
protected void initialize(ServletContext context)
{
    registerPackage("com.myapp");

    // Set the application's default locale
    LocaleConfiguration.setDefaultLocale(Locale.US);
}

Locale Resolution

Locale resolution determines which locale to use for each request. Oorian provides several built-in resolvers that implement the LocaleResolver interface.

Accept-Language (Default)

By default, Oorian uses the browser's Accept-Language header to determine the locale. This works without any configuration:

Java
// This is the default — no configuration needed
LocaleConfiguration.setLocaleResolver(new AcceptLanguageLocaleResolver());

For applications that only support specific locales, configure the supported locales to prevent the framework from setting a locale that has no corresponding translations:

Java
AcceptLanguageLocaleResolver resolver = new AcceptLanguageLocaleResolver();
resolver.addSupportedLocale(Locale.US);
resolver.addSupportedLocale(Locale.GERMANY);
resolver.addSupportedLocale(Locale.FRANCE);
LocaleConfiguration.setLocaleResolver(resolver);

// Browser sends: Accept-Language: fr-CA,fr;q=0.9,en;q=0.8
// Resolver matches: Locale.FRANCE (fr-CA not supported, but fr matches)

Session-Based

The SessionLocaleResolver stores the locale in the user's session, allowing per-user locale preferences that persist across requests:

Java
// Set a user's preferred locale (e.g., from a language picker)
SessionLocaleResolver.setLocale(Locale.JAPAN);

// Read the stored locale
Locale stored = SessionLocaleResolver.getLocale();

// Clear (revert to other resolution)
SessionLocaleResolver.clearLocale();

Chaining Resolvers

Use CompositeLocaleResolver to chain multiple resolvers. The first resolver that returns a non-null locale wins. This is the recommended pattern for applications that support user-selectable languages:

Java
AcceptLanguageLocaleResolver browserResolver = new AcceptLanguageLocaleResolver();
browserResolver.addSupportedLocale(Locale.US);
browserResolver.addSupportedLocale(Locale.GERMANY);
browserResolver.addSupportedLocale(Locale.FRANCE);

CompositeLocaleResolver resolver = new CompositeLocaleResolver()
    .addResolver(new SessionLocaleResolver())   // 1. User preference (if set)
    .addResolver(browserResolver);               // 2. Browser language

LocaleConfiguration.setLocaleResolver(resolver);
Resolution Order Matters

Place SessionLocaleResolver first so that a user's explicit language choice takes priority over the browser's Accept-Language header. The browser resolver then serves as the fallback for users who haven't set a preference.

Message Bundles

Oorian's Messages class provides a central API for managing localized text from standard Java ResourceBundle property files. Register named bundles at startup, then look up localized strings with simple static methods.

Setup

Java
@Override
protected void initialize(ServletContext context)
{
    registerPackage("com.myapp");

    // Register a message bundle
    Messages.registerBundle("app", "com.myapp.AppMessages");
    Messages.setDefaultBundle("app");
}

Property Files

Property files follow standard ResourceBundle conventions. Place them on the classpath in the package matching the base name:

Properties
# AppMessages.properties (default / English)
page.title=Welcome
welcome.user=Welcome, {name}!
items.count=You have {count} items

# AppMessages_de.properties (German)
page.title=Willkommen
welcome.user=Willkommen, {name}!
items.count=Sie haben {count} Artikel

# AppMessages_fr.properties (French)
page.title=Bienvenue
welcome.user=Bienvenue, {name} !
items.count=Vous avez {count} articles

Looking Up Messages

Messages are automatically resolved in the user's current locale. Parameters use {name} placeholders with alternating key-value pairs:

Java
// Simple message from the default bundle
String title = Messages.get("page.title");
// English: "Welcome"  |  German: "Willkommen"

// Parameterized message
String greeting = Messages.get("welcome.user", "name", "John");
// English: "Welcome, John!"  |  German: "Willkommen, John!"

// From a specific named bundle
String error = Messages.getFrom("errors", "not.found", "item", "User");

// Check if a key exists
if (Messages.hasKey("optional.feature"))
{
    // ...
}

Validation Messages

Oorian's validation framework uses DefaultMessages for built-in English validation messages. To translate validation messages, register a bundle with the well-known name Messages.VALIDATION_BUNDLE:

Java
// Register localized validation messages
Messages.registerBundle(Messages.VALIDATION_BUNDLE, "ValidationMessages");
Properties
# ValidationMessages.properties (English)
validation.required=This field is required
validation.email=Please enter a valid email address
validation.length.between=Must be between {min} and {max} characters

# ValidationMessages_de.properties (German)
validation.required=Dieses Feld ist erforderlich
validation.email=Bitte geben Sie eine gültige E-Mail-Adresse ein
validation.length.between=Muss zwischen {min} und {max} Zeichen lang sein

The message resolution chain works as follows:

  1. If a "validation" bundle is registered, resolve from it
  2. If a default bundle is configured, resolve from it
  3. Fall back to DefaultMessages (built-in English defaults)

This means most applications only need to register the validation bundle and the framework handles the rest. You can also replace the entire resolution chain with a custom MessageResolver:

Java
// Replace the default resolution chain entirely
MessageConfiguration.setMessageResolver(myCustomResolver);

// Restore the default chain
MessageConfiguration.setMessageResolver(null);

Localized Elements

Any HtmlElement can display localized text that automatically updates when the locale changes. Use setTextKey() instead of setText() to bind an element's text to a message key:

Java
// Bind an element's text to a message key
H1 heading = new H1();
heading.setTextKey("page.title");

// With parameters
Span welcome = new Span();
welcome.setTextKey("welcome.user", "name", userName);

// From a specific bundle
Span error = new Span();
error.setTextKeyFrom("errors", "not.found", "item", "User");

// Clear the message key binding
heading.clearTextKey();

The LocalizedText component is a convenience Span that takes a message key in its constructor:

Java
// Inline localized text
body.addElement(new LocalizedText("page.title"));

// With parameters
body.addElement(new LocalizedText("welcome.user", "name", "John"));

// From a specific bundle
body.addElement(new LocalizedText("errors", "not.found", "item", "User"));

Refreshing After Locale Change

When using SSE or WebSocket communication, you can update all localized elements on a live page after changing the locale. Call refreshLocalizedText() on the page to walk the element tree and re-resolve all message keys:

Java
// User selects a new language from a dropdown
OorianLocale.set(Locale.GERMANY);
SessionLocaleResolver.setLocale(Locale.GERMANY);

// Re-resolve all localized elements and push updates to the browser
refreshLocalizedText();

Text Direction (RTL Support)

Oorian automatically detects right-to-left languages and provides the TextDirection enum and DirectionResolver for RTL support:

Java
// Resolve direction for the current locale
TextDirection dir = DirectionResolver.resolveCurrentLocale();
// Returns TextDirection.LTR or TextDirection.RTL

// Check if a specific locale is RTL
boolean rtl = DirectionResolver.isRtl(new Locale("ar"));  // true

// Get the HTML dir attribute value
String htmlDir = DirectionResolver.getHtmlDir(OorianLocale.get());
// Returns "ltr", "rtl", or "auto"

The TextDirection enum maps directly to the HTML dir attribute:

Enum Value HTML Value Languages
TextDirection.LTR ltr English, French, German, Spanish, etc.
TextDirection.RTL rtl Arabic, Hebrew, Persian, Urdu, etc.
TextDirection.AUTO auto Browser determines from content

Locale-Aware Formatting

The Formatters class provides static methods for formatting dates, times, numbers, and currencies according to the current locale:

Dates and Times

Java
LocalDate date = LocalDate.of(2026, 3, 15);

// Format using the current locale (default MEDIUM style)
String formatted = Formatters.formatDate(date);
// US: "Mar 15, 2026"  |  Germany: "15.03.2026"  |  Japan: "2026/03/15"

// With specific format style
Formatters.formatDate(date, FormatStyle.FULL);
// US: "Sunday, March 15, 2026"  |  Germany: "Sonntag, 15. März 2026"

// Date and time
LocalDateTime now = LocalDateTime.now();
Formatters.formatDateTime(now);
Formatters.formatTime(now.toLocalTime());

Numbers and Currency

Java
// Number formatting
Formatters.formatNumber(1234567.89);
// US: "1,234,567.89"  |  Germany: "1.234.567,89"  |  France: "1 234 567,89"

// With specific decimal places
Formatters.formatNumber(1234.5, 2);
// US: "1,234.50"

// Currency
Formatters.formatCurrency(49.99, Currency.getInstance("USD"));
// US: "$49.99"  |  Germany: "49,99 $"

// Percentage
Formatters.formatPercent(0.85);
// US: "85%"  |  France: "85 %"

Parsing

The Formatters class can also parse locale-formatted strings back into values:

Java
// Parse a locale-formatted date
LocalDate date = Formatters.parseDate("15.03.2026", FormatStyle.MEDIUM);

// Parse a locale-formatted number
Number value = Formatters.parseNumber("1.234.567,89");

Complete Example

Here is a complete example showing locale configuration, message bundles, and localized page content with a language switcher:

Java — Application Setup
@WebListener
public class MyApp extends Application
{
    @Override
    protected void initialize(ServletContext context)
    {
        registerPackage("com.myapp");

        // Default locale
        LocaleConfiguration.setDefaultLocale(Locale.US);

        // Locale resolution: session first, then browser
        AcceptLanguageLocaleResolver browserResolver = new AcceptLanguageLocaleResolver();
        browserResolver.addSupportedLocale(Locale.US);
        browserResolver.addSupportedLocale(Locale.GERMANY);

        LocaleConfiguration.setLocaleResolver(
            new CompositeLocaleResolver()
                .addResolver(new SessionLocaleResolver())
                .addResolver(browserResolver)
        );

        // Message bundles
        Messages.registerBundle("app", "com.myapp.AppMessages");
        Messages.setDefaultBundle("app");
        Messages.registerBundle(Messages.VALIDATION_BUNDLE, "com.myapp.ValidationMessages");
    }

    @Override
    public void destroy(ServletContext context) { }
}
Java — Localized Page with Language Switcher
@Page("/home")
public class HomePage extends HtmlPage implements InputListener
{
    private Select languagePicker;

    @Override
    protected void createBody(Body body)
    {
        // Language picker
        languagePicker = new Select();
        languagePicker.addOption("en-US", "English");
        languagePicker.addOption("de-DE", "Deutsch");
        languagePicker.registerListener(this, InputChangeEvent.class);
        body.addElement(languagePicker);

        // Localized content
        H1 heading = new H1();
        heading.setTextKey("page.title");
        body.addElement(heading);

        body.addElement(new LocalizedText("welcome.user", "name", "John"));
    }

    @Override
    public void onEvent(InputChangeEvent event)
    {
        Locale locale = Locale.forLanguageTag(languagePicker.getSelectedValue());
        OorianLocale.set(locale);
        SessionLocaleResolver.setLocale(locale);
        refreshLocalizedText();
    }
}

API Summary

Class Purpose
OorianLocale Thread-local storage for the current request's locale
LocaleConfiguration Application-level default locale and resolver configuration
AcceptLanguageLocaleResolver Resolves locale from the browser's Accept-Language header
SessionLocaleResolver Stores and resolves locale from the user's session
CompositeLocaleResolver Chains multiple resolvers, first non-null wins
Messages Central API for registering bundles and looking up localized text
MessageConfiguration Configures the validation message resolution chain
DefaultMessages Built-in English validation messages (fallback)
TextDirection Enum for LTR, RTL, and AUTO text direction
DirectionResolver Resolves text direction from a locale
Formatters Locale-aware formatting for dates, numbers, and currency
LocalizedText Convenience Span that resolves text from a message key