Table of Contents
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
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:
@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;
}
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:
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:
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():
@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 |
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:
@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.
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.
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:
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:
- Programmatic:
setProfile("dev")ininitialize() - System property:
-Doorian.profile=dev - Environment variable:
OORIAN_PROFILE=dev - Default:
"prod"
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:
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.
db.host=localhost
db.port=5432
db.pool.size=2
oorian.mode=dev
db.host=db.mycompany.com
db.port=5432
db.pool.size=20
Complete Example
Using configuration and profiles together in your application:
@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:
// 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:
@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:
@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:
- initializePage() — Called first. Perform authentication checks, load data,
or redirect. Return
falseto stop page rendering. - createHead() — Build the
<head>section: title, meta tags, stylesheets, and scripts. - createBody() — Build the
<body>section: your page content. - 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:
@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:
@Override
public void onEvent(MouseClickedEvent event)
{
statusLabel.setText("Processing...");
progressBar.setWidth("50%");
submitBtn.setDisabled(true);
sendUpdate();
}
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:
@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:
@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 |
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:
// 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");
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:
// 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:
// 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():
@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():
@Override
protected boolean initializePage()
{
// Prevent caching of sensitive pages
getHttpResponse().setHeader("Cache-Control",
CacheControl.preventCaching().toString());
return true;
}
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(), andsetFocus(). - 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 |
@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:
@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 |
H1 – H6 |
<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:
// 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():
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 |
// 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:
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():
@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:
@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:
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);
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:
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():
// 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:
// 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:
// 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
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:
int columns = getColumnCount();
sidebar.setWidth((100 / columns) + "%");
sidebar.setMinWidth(sidebarMinPx + "px");
sidebar.setPadding(basePadding + "rem");
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:
// 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:
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
// 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
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
// 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
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
// 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:
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:
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:
// 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 |
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);
ClassRule menu = new ClassRule("menu");
ElementRule menuItem = new ElementRule("li");
menuItem.setListStyleType("none");
menuItem.setPadding(8);
menu.addChild(menuItem);
sheet.addRule(menu);
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:
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
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
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:
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:
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():
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 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);
// Scale up from top-left corner
Grow grow = new Grow();
grow.setScale(2.0f);
grow.setOrigin(Origin.LEFT, Origin.TOP);
panel.addTransition(grow);
// 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));
// 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:
// 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 |
|---|---|---|---|
| 0 | 1200ms | 6 | 200ms |
| 1 | 1000ms | 7 | 150ms |
| 2 | 800ms | 8 | 100ms |
| 3 | 500ms | 9 | 75ms |
| 4 | 400ms | 10 | 50ms |
| 5 (default) | 300ms |
Fluent Chaining
All transition configuration methods return this, enabling fluent chaining:
panel.addTransition(new Fade().setSpeed(7).setTo(0.5f))
.addTransition(new ColorShift().setTo("#e0e0e0"))
.transitionForward();
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.
// 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
// 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:
// 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:
// 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
// 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));
// 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 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():
@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 |
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(...):
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:
// 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
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
// 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:
// 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.
// 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:
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.
// 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:
// 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:
// 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.
// 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) |
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.
@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:
@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:
@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;
}
}
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.
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:
// 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:
+--------------------------------------+
| NORTH |
+--------+-----------------+-----------+
| | | |
| WEST | CENTER | EAST |
| | | |
+--------+-----------------+-----------+
| SOUTH |
+--------------------------------------+
// 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:
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:
// 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:
// 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:
// 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:
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:
// 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:
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:
// 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):
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:
// 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:
// 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:
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:
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:
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:
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:
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. |
// 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:
// 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:
// 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
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.
@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.
@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.
@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:
// 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:
@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);
}
}
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:
@Override
public void onEvent(MouseClickedEvent event)
{
statusLabel.setText("Processing...");
progressBar.setWidth("50%");
submitBtn.setDisabled(true);
sendUpdate(); // Push all three changes in one batch
}
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:
- Implement the listener interface for the events you want to handle
- Register your listener with
registerListener() - 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:
@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();
}
}
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:
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
}
// 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:
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:
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);
}
// 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());
}
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.
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):
// Dispatch a page event from any element
dispatchEvent(new CategoryChangedEvent("electronics"));
// 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:
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
}
// 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.
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
}
// 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 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:
- An event is dispatched to your listener
- Your handler modifies element properties (text, visibility, styles, etc.)
- 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.
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:
// 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");
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:
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 |
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:
// 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);
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:
@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 |
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:
// 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:
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:
@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:
// 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. ThrowsConversionExceptionon 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:
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:
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:
// 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:
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:
- Required check — if
asRequired()was called and the field is empty, validation stops with the required message - Type conversion — the converter's
convertToModel()is called; aConversionExceptionstops validation - 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
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:
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:
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:
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:
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():
// 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:
// 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:
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
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:
// 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:
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:
@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:
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()));
}
}
}
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().
@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
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:
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:
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:
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:
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.
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:
// 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:
// 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:
// 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):
// 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:
// 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 |
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:
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:
- Your page implements a listener interface (e.g.,
GeolocationListener) - You register the listener on an element:
element.registerListener(this, EventClass.class) - You call a static API method:
GeolocationApi.getCurrentPosition(element) - Oorian sends a JavaScript command to the browser
- The browser executes the API call and sends the result back
- 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.
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:
// 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.
// Copy text to clipboard — convenience method
ClipboardApi.writeText("Text to copy");
// Copy from a specific page context
ClipboardApi.writeText(page, "Text to copy");
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.
// 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();
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.
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:
// 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);
// 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");
// 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);
// 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:
// 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:
// 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:
// 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]");
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.
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 asfinal) - toJsonValue() — serializes fields to a
JsonObject
Once a class implements Jsonable, it integrates seamlessly with
JsonObject and JsonArray:
// 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();
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:
@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):
<!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:
- Loads the HTML file from the servlet context
- Parses it with Oorian's
Html5Parserinto a full element tree - Copies attributes from the template's
<html>element (e.g.,lang,dir) to the page - Extracts the
<head>and<body>and sets them as the page's head and body - Calls
createHead()andcreateBody()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:
@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:
@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:
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:
Html5Parser parser = new Html5Parser();
Element content = parser.parse(
"<div class='card'>" +
" <h2>Welcome</h2>" +
" <p>Hello, world!</p>" +
"</div>"
);
body.addElement(content);
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:
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
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:
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. |
@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:
@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:
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:
// 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"
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:
@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
thisfor 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():
@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:
// 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
thisfor method chaining where appropriate
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():
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 |
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:
// 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:
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).
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:
# 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:
-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.
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:
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()));
}
}
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.
@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:
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);
}
}
@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(), orcreateBody() - WebSocket message handling — exceptions while processing client events
- Worker threads — exceptions in
OorianWorkerThreadexecution
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.
@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:
-Doorian.mode=dev
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.
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.
@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:
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:
@Override
public void initialize(ServletContext context)
{
registerPackage("com.myapp");
setHttpsRequired(true);
}
HTTPS enforcement includes several practical safeguards:
- Localhost exception — Requests to
localhostand127.0.0.1are never redirected, so local development works without SSL certificates - Load balancer support — The filter checks the
X-Forwarded-Protoheader, 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:
@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:
@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:
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:
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:
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():
@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:
@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:
// 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:
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):
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);
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:
// After verifying credentials:
OorianSession session = getSession();
session.rotateSession(); // New session ID, CSRF token regenerated
session.login(principal); // Store the authenticated user
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()orsession.invalidate()when a user logs out - Rotate on authentication — Call
rotateSession()orlogin()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:
@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 (requiresSecure=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:
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:
@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:
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:
// 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:
// Per-page: report-only mode — violations logged but not enforced
head.setContentSecurityPolicyReportOnly(csp);
// Per-page: enforcement mode — violations are blocked
head.setContentSecurityPolicy(csp);
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:
// 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:
// 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. UseRawHtmlonly 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
ValidatedFormfor server-side input validation on all user-submitted data
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:
@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:
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:
@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:
// 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:
// 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:
// 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:
// 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:
@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:
// 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:
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:
// 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:
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);
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
@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:
# 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:
// 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:
// Register localized validation messages
Messages.registerBundle(Messages.VALIDATION_BUNDLE, "ValidationMessages");
# 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:
- If a
"validation"bundle is registered, resolve from it - If a default bundle is configured, resolve from it
- 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:
// 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:
// 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:
// 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:
// 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:
// 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
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
// 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:
// 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:
@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) { }
}
@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 |