7 Commits

Author SHA1 Message Date
Amer Agovic
e644a1aa13 Sync generated POM with current Jetty versions 2026-05-01 13:48:53 -05:00
Amer Agovic
bfb6ff82bf Upgrade Jetty and harden WebSocket upgrade lifecycle 2026-05-01 13:47:23 -05:00
Amer Agovic
c86fc03c2e Ignore local bstore junction 2026-05-01 13:39:15 -05:00
Amer Agovic
847c69f112 Refocus Jabba on web runtime and improve file serving 2026-05-01 13:38:37 -05:00
Amer Agovic
6efc097544 fix: Resolve javadoc errors for Maven publish
- Fix heading hierarchy in CallSession (h3 -> h2)
- Escape HTML characters in ResponseState comment
- Correct @param tags in ResponseEncoder.writeError()

These fixes allow successful javadoc generation and Maven artifact publishing.
2026-01-07 09:12:36 -06:00
Amer Agovic
38603744e2 fix: Update .settings/pom.xml dependencies to resolve security vulnerabilities
- Update PostgreSQL driver from 42.5.0 to 42.7.4 (fixes SQL injection CVE)
- Update H2 database from 2.1.214 to 2.3.232 (fixes password exposure CVE)
- Update all Jetty dependencies to 12.0.15 stable release
- Update other dependencies to match build.gradle versions
- Sync pom.xml with current project state for VS Code IntelliSense
2026-01-07 09:06:43 -06:00
Amer Agovic
5f36b3d3e2 Add WebSocket support with Jakarta WebSocket integration
- Implemented WebSocketSession abstract class with callback-based API
- Added ServletWebSocketSession with full Jakarta WebSocket bridging
- Created @WebSocket annotation for declarative endpoint marking
- Updated JettyApp to initialize Jakarta WebSocket container
- Split Request/Response into abstract base and servlet implementations
- Moved JettyApp to jabba.servlet package
- Moved annotations to jabba.decor package
- Added comprehensive WebSocket test suite (5 tests, all passing)
- Updated README.md with WebSocket documentation and examples
- All 31 tests passing (async, sync, security, websocket, database)
- Fixed spelling errors in README.md
2026-01-07 08:57:12 -06:00
80 changed files with 4911 additions and 6577 deletions
Vendored
+4 -1
View File
@@ -38,4 +38,7 @@ gradle-app.setting
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
.classpath
# do not consider bstore as part - will be its own repo
bstore/
+7 -7
View File
@@ -1,13 +1,13 @@
arguments=
auto.sync=false
arguments=--init-script /home/amer/.config/Code/User/globalStorage/redhat.java/1.50.0/config_linux/org.eclipse.osgi/58/0/.cp/gradle/init/init.gradle --init-script /home/amer/.config/Code/User/globalStorage/redhat.java/1.50.0/config_linux/org.eclipse.osgi/58/0/.cp/gradle/protobuf/init.gradle
auto.sync=true
build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(8.9))
connection.project.dir=
eclipse.preferences.version=1
gradle.user.home=
java.home=
java.home=/home/amer/.vscode/extensions/redhat.java-1.50.0-linux-x64/jre/21.0.9-linux-x86_64
jvm.arguments=
offline.mode=false
override.workspace.settings=false
show.console.view=false
show.executions.view=false
override.workspace.settings=true
show.console.view=true
show.executions.view=true
+3 -3
View File
@@ -42,9 +42,9 @@ org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.lambda.genericSignature=do not generate
org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
org.eclipse.jdt.core.compiler.codegen.shareCommonFinallyBlocks=disabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.compliance=11
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
@@ -169,7 +169,7 @@ org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
org.eclipse.jdt.core.compiler.processAnnotations=disabled
org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.source=1.8
org.eclipse.jdt.core.compiler.source=11
org.eclipse.jdt.core.compiler.storeAnnotations=disabled
org.eclipse.jdt.core.compiler.taskCaseSensitive=enabled
org.eclipse.jdt.core.compiler.taskPriorities=NORMAL,HIGH,NORMAL
+41 -23
View File
@@ -9,7 +9,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.reliancy</groupId>
<artifactId>jabba</artifactId>
<version>0.1</version>
<version>3.0.0-SNAPSHOT</version>
<licenses>
<license>
<name>The Apache License, Version 2.0</name>
@@ -17,41 +17,59 @@
</license>
</licenses>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>12.0.32</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>jetty-http2-server</artifactId>
<version>12.0.32</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee10</groupId>
<artifactId>jetty-ee10-servlet</artifactId>
<version>12.0.32</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee10.websocket</groupId>
<artifactId>jetty-ee10-websocket-jakarta-server</artifactId>
<version>12.0.32</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>12.0.0.alpha1</version>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.0-alpha0</version>
<artifactId>slf4j-jdk14</artifactId>
<version>2.0.16</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.github.jknack</groupId>
<artifactId>handlebars</artifactId>
<version>4.3.0</version>
<version>4.4.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.5.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.0</version>
<scope>runtime</scope>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>jetty-websocket-jetty-client</artifactId>
<version>12.0.32</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
+250 -68
View File
@@ -1,20 +1,18 @@
# Jabba the easy going java web app plumber
Jabba is a java library that gets its inspiration from Python Flask. It will expose all the elementary features needed for deveopment of web apps and microservices.
Jabba is a java library that gets its inspiration from Python Flask. It will expose all the elementary features needed for development of web apps and microservices.
# How to Build Things
* running a build via: gradle jar
* running a test via: gradle test
* running a continouse server via: gradle --watch-fs -t runServer, then work on code (every save will rebuild and restart so you just refresh browser)
* running a continuous server via: gradle --watch-fs -t runServer, then work on code (every save will rebuild and restart so you just refresh browser)
Jabba depends on following libraries:
* org.eclipse.jetty:jetty-server - http servlet container
* org.slf4j:slf4j-simple - logging facility
* com.github.jknack:handlebars - handlebars templating
* com.h2database:h2 - java native comprehensive sql database (so comprehensive it can emulate postgres)
* org.postgresql:postgresql - proven and logical sql server implementation (no hacks like the identity column)
* com.zaxxer:HikariCP - database connection pooling (trust me you need it)
Jabba depends on the following libraries:
* org.eclipse.jetty:jetty-server - HTTP servlet container
* org.slf4j:slf4j-jdk14 - logging facility
* com.github.jknack:handlebars - handlebars templating
* com.reliancy:bstore-j - storage, record, and utility foundation used by Jabba
Normally maven or gradle will auto-resolve and download these dependencies.
If you build the libraries yourself there is "fat jabba" task that with combine all dependencies in one jar/zip file.
@@ -40,63 +38,251 @@ This is a sonatype nexus repository manager we host here at reliancy.
* ~~Error page~~
* ~~Menu handling~~
* ~~Database layer or serial/deserial system like SQL Alchemy~~
* ~~Asynchronous request processing with CompletableFuture support~~
* ~~WebSocket support for real-time bidirectional communication~~
With above things complete we ~~will~~ have a library that can be used for new webapps.
Don't have a profile page yet (which goes into app templates) and the dbo layer is basic not like sql alchemy but mostly things are in place.
With above things complete we ~~will~~ have a library that can be used for new webapps.
Don't have a profile page yet (which goes into app templates) but mostly things are in place.
At this point we could use jabba to spawn new apps.
# Security Best Practices
When deploying Jabba applications in production, follow these security guidelines:
## Secret Key Management
**Critical:** Always set a strong, unique secret key for encryption. Never use the default or commit secrets to version control.
* Set `SECRET_KEY` in your configuration file, or
* Set `JABBA_SECRET_KEY` environment variable, or
* Set `jabba.secret.key` system property
The secret key should be:
- At least 32 characters long
- Randomly generated (use a secure random generator)
- Unique per application instance
- Stored securely (use a secrets manager in production)
Example:
```bash
export JABBA_SECRET_KEY=$(openssl rand -base64 32)
```
## Cookie Security
Session cookies are automatically set with `HttpOnly` flag to prevent XSS attacks. The `Secure` flag is automatically set when requests are made over HTTPS.
## Input Validation
Jabba includes basic input validation to prevent DoS attacks:
- String parameters are limited to 100,000 characters
- Array parameters are limited to 1,000 elements
For additional validation, implement custom validation in your endpoint methods.
## Authentication
* Use strong password policies
* Never log passwords or sensitive credentials
* Use HTTPS in production to protect credentials in transit
* Implement proper session timeout and expiration
## Configuration
* Validate all configuration values on startup
* Use environment variables for sensitive configuration
* Never commit `.env` files or configuration with secrets to version control
* Use `.gitignore` to exclude sensitive files
## Database Security
* Use parameterized queries (Jabba uses PreparedStatement by default)
* Never construct SQL queries by concatenating user input
* Use connection pooling with appropriate limits
* Restrict database user permissions to minimum required
## Error Handling
* In production, sanitize error messages to avoid information disclosure
* Log detailed errors server-side, but return generic messages to clients
* Use proper HTTP status codes
## Server Configuration
* Configure request size limits to prevent DoS attacks
* Set appropriate timeouts
* Use reverse proxy (nginx, Apache) in front of Jabba for additional security layers
* Keep dependencies up to date
# Code Structure
There are 4 major modules all located under com.reliancy.
Jabba now focuses on web-application concerns and depends on `bstore-j` for the shared
`com.reliancy.util`, `com.reliancy.rec`, and `com.reliancy.dbo` foundations.
Within this repository the main areas are:
* `com.reliancy.jabba` - request routing, middleware, sessions, security, and app lifecycle
* `com.reliancy.jabba.servlet` - Jetty/Jakarta servlet integration
* `com.reliancy.jabba.ui` - server-side rendering helpers and UI support classes
* `com.reliancy.io` - Jabba-local protocol encoding and decoding helpers
If you need the data/storage layer itself, use `bstore-j` directly. Jabba intentionally does
not carry its own SQL implementation anymore.
## Static Files And SPA Hosting
Jabba's `FileServer` can now serve both classic static assets and single-page applications.
It supports:
* root document serving such as `/ -> index.html`
* SPA fallback routing for browser navigation
* `ETag`, `Last-Modified`, and `Content-Length`
* `HEAD` support
* cache-control tuning for assets, index, and fallback responses
* in-memory caching for hot small assets
Example:
```java
new FileServer("/", "", work_dir + "/public")
.setIndexFile("index.html")
.setFallbackFile("index.html")
.setAssetCacheControl("public, max-age=3600")
.setIndexCacheControl("no-cache")
.publish(app);
```
## jabba
Finally the center of the library is the module that implements an HTTP servlet (jetty handler actually). Entire machinery is added to perform marshalling and unmarshalling of HTTP requests into and out of java methods. Along the way we also deal with sessions and security and errors and also server side templating. Ideally your app will be a set of REST endpoints that are used by ReactJS or similar front end GUIs but in case you like server-side templating it is available.
They are:
You application can be any POJO, the entry point is JettyApp from there a request processor is installed which parses your class and discovers all the endpoints. We chain request processors landing on a MethodEndPoint. Middleware is injected, you guessed it, as a request processor in the middle of the chain. Examples include session management, security policy.
* rec - slot based object and array definition, akin to json, base of dbo and any data access object (DAO)
* dbo - database access layer on top of rec
* jabba - web application layer
* util - ulity methods maximally independent
### Asynchronous Support
## util
This module is a treasure trove of useful classes and methods. Most standalone methods are implemented in Handy class. One very useful class is the Tokenizer which starts out as a static method and is then wrapped by an iterator.
Jabba provides first-class support for asynchronous request processing, allowing your application to handle long-running operations efficiently without blocking server threads. Async support is automatically detected and requires no special configuration.
You can then do something like:
``` java
for(String token:new Tokenizer(bodyOftext)){
System.out.println("Word:"+token);
**Automatic Detection:**
- Methods returning `CompletableFuture<T>` are automatically handled asynchronously
- Methods annotated with `@Async` are executed in a thread pool
- Regular synchronous methods continue to work as expected
**Benefits:**
- Improved throughput for I/O-bound operations
- Efficient handling of concurrent requests
- Non-blocking execution for database queries, external API calls, and file operations
- Seamless integration with Java's `CompletableFuture` API
**Example:**
```java
// Async endpoint - automatically detected by return type
@Routed(path="/users/{id}")
public CompletableFuture<User> getUser(int id) {
return CompletableFuture.supplyAsync(() -> {
// Long-running database query
return database.findUserById(id);
});
}
// Async endpoint using @Async annotation
@Routed(path="/report")
@Async
public Report generateReport(String month, int year) {
// Heavy computation executed in thread pool
return reportService.generate(month, year);
}
// Regular synchronous endpoint - no changes needed
@Routed(path="/ping")
public String ping() {
return "pong";
}
```
## rec
The async implementation handles proper resource cleanup, session management, and error propagation automatically. Both synchronous and asynchronous endpoints can coexist in the same application without any special routing or configuration.
The core of rec module is the interface Rec/Arr and plain implementation of it called Obj which can be an array or a key-value object. The values are kept in Obj while a parent object of type Hdr describes structure (the header). JSON encoder and decoder are provided and others could be implemented.
### WebSocket Support
At the field level the class Slot describes a field or property. It is usually defined statically at class level. Accessing a value can be done in two ways:
``` java
rec.get(Slot s); // record centric
Product.first_name.get(rec); // field centric
```
The slot mechanism does not just describe fields it also allows us to generate conditions over slots which is useful in query construction.
Jabba provides built-in support for WebSocket connections, enabling real-time bidirectional communication between clients and servers. WebSocket endpoints are treated as first-class citizens alongside HTTP endpoints, with automatic lifecycle management and session integration.
One example would be notation like:
``` java
db.query(Product.class).where(Product.first_name.equals("Bla"))
**Key Features:**
- Declarative WebSocket endpoints using `@WebSocket` annotation
- Automatic protocol upgrade from HTTP to WebSocket
- Session management and authentication integration
- Callback-based message handling (text and binary)
- Built-in support for broadcasting to multiple clients
- Seamless integration with application security policies
**Architecture:**
- WebSocket endpoints work in conjunction with `@Routed` for path mapping
- Full access to `AppSession` context for authenticated connections
- Automatic session tracking with built-in registry for broadcasting
- Clean separation between Jabba abstractions and underlying Jakarta WebSocket implementation
**Example:**
```java
// Simple echo endpoint
@Routed(path="/ws/echo")
@WebSocket
public void echoEndpoint(WebSocketSession session) {
session.onText(message -> {
try {
session.sendText("Echo: " + message);
} catch (IOException e) {
log().error("Failed to send message", e);
}
});
}
// Chat room with broadcasting
@Routed(path="/ws/chat")
@WebSocket
public void chatEndpoint(WebSocketSession session) {
String route = session.getRoute();
session.onText(message -> {
// Broadcast to all clients on this route
WebSocketSession.broadcast(route, "User says: " + message);
});
session.onClose((code, reason) -> {
WebSocketSession.broadcast(route, "User disconnected");
});
// Welcome message
try {
session.sendText("Welcome to the chat room!");
} catch (IOException e) {
log().error("Failed to send welcome message", e);
}
}
// Authenticated WebSocket with AppSession access
@Routed(path="/ws/notifications")
@WebSocket
@Secured
public void notificationEndpoint(WebSocketSession session) {
Session appSession = session.getAppSession();
String userId = appSession != null ? appSession.getId() : "anonymous";
session.onText(message -> {
log().info("Received from user {}: {}", userId, message);
});
}
```
Use slot based value access instead of by string names. The reason is that change happens and then you potentially have multiple places to change if using string. When using slots you can rely on refactoring help. Slots are smart they know type and format and position of the field.
WebSocket connections can be tested using standard WebSocket clients. In JavaScript:
## dbo
Here we define a very generic DAO interface. At the center is a Terminal which represents a data store and allows us to perform CRUD with some extra facility for complex queries. For database purposes we implement DBO or database object as a special instance of Rec interface. Finally to make this a useful module we provide SQLTerminal and helper classes to deal with Read, Create/Update, Delete actions using SQL language. Of course read action deals with querying.
```javascript
const ws = new WebSocket('ws://localhost:8090/ws/echo');
ws.onopen = () => ws.send('Hello Server');
ws.onmessage = (event) => console.log('Received:', event.data);
```
Plese note one thing about SQL in particular. SQL and related RDBMs are nothing ground breaking or throughput busting. They are an old and messy protocol to access data. The most important point is do not treat SQL connections as an open file handle. Instead you connect, you CRUD, you disconnect (and if that sounds inefficient it is). If you forget this, as I did, and you build your entire app on the premise that you can reuse a connection by multiplexing commands down the pipe you will be in a world of hurt. The hurt does not manifest during development but once tens or tousands of sessions start working the same few connections.
In any case this module tries to be SQL agnostic while sharing nomenclature. If you stick with the interface your app will be too. In my decades old experience there is no way to allow just a little SQL in your code. So treat your database layer as if it was not a SQL database and maybe you will be able to later switch to something else. Otherwise it will SQL(squeel) till the judgement day.
## jabba
Finally the center of the library is the module that implements an HTTP servlet (jetty handler actually). Entire machinery is added to perform marshalling and unmarshaling of HTTP requests into and out of java methods. Along the way we also deal with sessions and security and errors and also server side templating. Ideally your app will be a set of REST endpoints that are used by ReactJS or similar front end GUIs but in case you like server-side templating it is available.
You application can be any POJO, the entry point is JettyApp from there a request processor is installed which parses your class and discovers all the endpoints. We chain request processors landing on a MethodEndPoint. Middleware is injected, you guessed it, as a request processor in the middle of the chain. Examples include session management, security policy.
The WebSocket implementation ensures proper cleanup of resources, handles reconnection scenarios gracefully, and maintains compatibility with standard Jakarta WebSocket clients while providing a cleaner, callback-based API.
### Where to Start
@@ -114,23 +300,22 @@ public class App extends JettyApp{
work_dir="../var";
}
Template.search_path(work_dir,App.class);
JettyApp app=new JettyApp();
app.addAppSession();
SecurityPolicy secpol=new SecurityPolicy().setStore(new PlainSecurityStore());
app.setSecurityPolicy(secpol);
// this is where method parsing happens and app could be any POJO
RoutedEndPoint rep=new RoutedEndPoint().importMethods(app);
app.setRouter(rep);
// it helps to support static file serving too
FileServer fs=new FileServer("/static",work_dir+"/public");
fs.exportRoutes(app.getRouter());
// setup menu if you gonna use templates
Menu top_menu=Menu.request(Menu.TOP);
top_menu.add(new MenuItem("home")).addSpacer().add(new MenuItem("login"));
top_menu.setTitle("Jabba");
app.run(new FileConfig());
//System.out.println("Goodbye World!");
}
App app=new App();
app.addAppSession();
SecurityPolicy secpol=new SecurityPolicy().setStore(new PlainSecurityStore());
app.setSecurityPolicy(secpol);
Router router=app.getRouter();
router.importMethods(app);
// it helps to support static file serving too
new FileServer("/static","",work_dir+"/public").publish(app);
// setup menu if you are going to use templates
Menu top_menu=Menu.request(Menu.TOP);
top_menu.add(new MenuItem("home")).addSpacer().add(new MenuItem("login"));
top_menu.setTitle("Jabba");
router.compile();
app.begin(new FileConfig());
//System.out.println("Goodbye World!");
}
// case 1: simplest endpoint (path from method name)
@Routed()
public String hello(){
@@ -196,12 +381,9 @@ public class App extends JettyApp{
if(req.getVerb().equals("POST")){
// here we need to process login and redirect
try{
System.out.println("Post login");
String userid=(String)req.getParam("userid",null);
String pwd=(String)req.getParam("password",null);
AppSession ass=AppSession.getInstance();
System.out.println("SS:"+ass);
System.out.println("P:"+userid+"/"+pwd);
SecurityPolicy secpol=ass.getApp().getSecurityPolicy();
SecurityActor user=secpol.authenticate(userid, pwd);
if(user==null) throw new NotAuthentic("invalid credentials");
@@ -235,4 +417,4 @@ public class App extends JettyApp{
}
}
```
```
+35 -17
View File
@@ -12,30 +12,37 @@ apply from: 'extra.gradle'
project.buildDir = 'target'
group='com.reliancy'
version = '0.3-SNAPSHOT'
version = '3.0.0-SNAPSHOT'
application{
mainClass=(group+'.'+name+'.JettyApp')
}
java{
// make our library a bit more compatible (jetty forced 11 else it would have been 1.8)
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
// Updated to Java 21 for modern features and better performance
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
dependencies {
def jettyVersion="11.0.18"
implementation "org.eclipse.jetty:jetty-server:${jettyVersion}"
implementation "org.eclipse.jetty.http2:http2-server:${jettyVersion}"
implementation "org.slf4j:slf4j-jdk14:2.0.10"
//implementation "org.slf4j:slf4j-simple:2.0.10"
//implementation 'com.hubspot.jinjava:jinjava:2.5.10'
implementation 'com.github.jknack:handlebars:4.3.0'
implementation 'com.h2database:h2:2.1.214'
implementation 'org.postgresql:postgresql:42.5.0'
implementation 'com.zaxxer:HikariCP:5.0.0'
testImplementation "junit:junit:4.12"
tasks.withType(JavaCompile) {
options.compilerArgs << '-parameters'
}
dependencies {
def jettyVersion="12.0.32"
def bstoreVersion="1.0.0-SNAPSHOT"
implementation "org.eclipse.jetty:jetty-server:${jettyVersion}"
implementation "org.eclipse.jetty.http2:jetty-http2-server:${jettyVersion}"
implementation "org.eclipse.jetty.ee10:jetty-ee10-servlet:${jettyVersion}"
implementation "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server:${jettyVersion}"
implementation "jakarta.servlet:jakarta.servlet-api:6.0.0"
implementation "org.slf4j:slf4j-jdk14:2.0.16"
//implementation "org.slf4j:slf4j-simple:2.0.16"
//implementation 'com.hubspot.jinjava:jinjava:2.5.10'
implementation 'com.github.jknack:handlebars:4.4.0'
implementation "com.reliancy:bstore-j:${bstoreVersion}"
testImplementation "junit:junit:4.13.2"
testImplementation "org.eclipse.jetty.websocket:jetty-websocket-jetty-client:${jettyVersion}"
}
sourceSets {
main {
resources {
@@ -59,7 +66,7 @@ processResources {
}
repositories {
//mavenLocal()
//mavenCentral()
mavenCentral()
maven{
url "https://repo.reliancy.com/repository/maven-hub"
}
@@ -153,3 +160,14 @@ eclipse{
}
}
}
// Task to run DemoApp demonstration application
task runDemo(type: JavaExec, dependsOn: testClasses) {
group = 'application'
description = 'Run the DemoApp demonstration application'
classpath = sourceSets.test.runtimeClasspath
mainClass = 'com.reliancy.jabba.DemoApp'
args = project.hasProperty('appArgs') ? project.appArgs.split('\\s+') : []
workingDir = projectDir
standardInput = System.in
}
+18 -3
View File
@@ -92,16 +92,31 @@ class Server implements Runnable{
info("stopping server");
if(driver!=null){
driver.interrupt();
//driver.join();
try{
driver.join(5000); // Wait up to 5 seconds for graceful shutdown
}catch(InterruptedException e){
info("interrupted while waiting for driver to stop");
}
}
// Clean up stale threads using proper interruption
for(Thread th:Thread.getAllStackTraces().keySet()){
if(th.getName().equalsIgnoreCase("executor")){
info("cleaning up stale driver:"+th.toString())
th.stop();
th.interrupt();
try{
th.join(2000); // Wait up to 2 seconds
}catch(InterruptedException e){
// Ignore
}
}
if(th.getName().equalsIgnoreCase("server.driver")){
info("cleaning up stale driver:"+th.toString())
th.stop();
th.interrupt();
try{
th.join(2000); // Wait up to 2 seconds
}catch(InterruptedException e){
// Ignore
}
}
}
return this;
-230
View File
@@ -1,230 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
/** Description of a terminal operation with a slice of dbo objects as input or output.
* This object is not just for reading but also bulk updating.
* It will be used to describe a multi DBO read or write and to then also track results.
* At its core are action traits which are classes that define either loading,saving or deleting.
* The items field is a consumable object when consumed the action is done.
* So for loading we iterate once done it cannot be done again. Also when items are provided for saving
* once iterated over and saved we are done.
*/
public class Action implements Iterable<DBO>,SiphonIterator<DBO>{
public static class Trait{
public String toString(){return getClass().getSimpleName();}
}
public static class Load extends Trait{
int limit,offset;
Check filter;
}
public static class Save extends Trait{
}
public static class Delete extends Trait{
Check filter;
}
Terminal terminal;
Trait trait;
Entity entity;
Object[] params;
SiphonIterator<DBO> items;
public Action(){
trait=null;
}
public Action(Trait t){
trait=t;
}
public Action(Terminal t){
terminal=t;
trait=null;
}
public Action execute() throws IOException{
return terminal.execute(this);
}
public Terminal getTerminal() {
return terminal;
}
public Action setTerminal(Terminal terminal) {
this.terminal = terminal;
return this;
}
public Trait getTrait() {
return trait;
}
public Action setTrait(Trait t) {
this.trait = t;
return this;
}
public Entity getEntity() {
return entity;
}
public Action setEntity(Entity entity) {
this.entity = entity;
return this;
}
public void clear(){
terminal=null;
trait=null;
entity=null;
setItems((DBO)null);
}
public Action load(Entity ent){
trait=new Load();
entity=ent;
return this;
}
public Action load(Class<? extends DBO> cls){
trait=new Load();
entity=Entity.recall(cls);
return this;
}
public Action save(Entity ent){
trait=new Save();
entity=ent;
return this;
}
public Action delete(Entity ent){
trait=new Delete();
entity=ent;
return this;
}
public Action params(Object...p){
params=p;
return this;
}
public Action setItems(final DBO ...itms){
SiphonIterator<DBO> it=null;
if(itms!=null){
it=new SiphonIterator<DBO>() {
private int index = 0;
@Override
public boolean hasNext() {
return itms.length > index;
}
@Override
public DBO next() {
return itms[index++];
}
@Override
public void close() throws IOException {
}
};
}
return setItems(it);
}
public Action setItems(final Collection<DBO> itms){
SiphonIterator<DBO> it=null;
if(itms!=null){
it=new SiphonIterator<DBO>() {
private final Iterator<DBO> str = itms.iterator();
@Override
public boolean hasNext() {
return str.hasNext();
}
@Override
public DBO next() {
return str.next();
}
@Override
public void close() throws IOException {
}
};
}
return setItems(it);
}
public Action setItems(SiphonIterator<DBO> itms){
if(items==itms) return this;
if(items!=null){
try {
items.close();
} catch (Exception e) {
}
}
items=itms;
return this;
}
protected SiphonIterator<DBO> getItems(){
return items;
}
@Override
public Iterator<DBO> iterator() {
return this;
}
@Override
public boolean hasNext() {
return items!=null?items.hasNext():false;
}
@Override
public DBO next() {
return items.next();
}
@Override
public void close() throws IOException {
if(items!=null){
items.close();
items=null;
if(terminal!=null) terminal.end(this);
}
}
public Action limit(int max) {
((Load)trait).limit=max;
return this;
}
public Action filterBy(Check... c){
Check filter=null;
if(c!=null){
if(c.length>1) filter=Check.and(c);
else filter=c[0];
}
if(trait instanceof Load){
((Load)trait).filter=filter;
}else
if(trait instanceof Delete){
((Delete)trait).filter=filter;
}else{
throw new IllegalStateException("filtering not supported by trait:"+trait);
}
return this;
}
public Check getFilter(){
if(trait instanceof Load){
return ((Load)trait).filter;
}else
if(trait instanceof Delete){
return ((Delete)trait).filter;
}else{
throw new IllegalStateException("filtering not supported by trait:"+trait);
}
}
public Action if_pk(Object... id) {
Field pk=entity.getPk();
return filterBy(pk.eq(id));
}
public DBO first() {
try{
if(this.hasNext()){
return this.next();
}else{
return null;
}
}finally{
clear();
}
}
public boolean isDone(){
return items==null || items.hasNext()==false;
}
}
-168
View File
@@ -1,168 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Observable;
/** A more or less virtual collection of items.
* this object is a suitable holder of resultsets. it will be overridable so
* we can create specialized virtual holders that use backends. by itself it will
* implement in memory list.
* also this class is an observable and we can monitor update to it.
*/
public class Bag<E> extends Observable implements Collection<E>{
/** event to send to observers. */
public static final class BagChanged<E>{
public static final int ADD=0;
public static final int REMOVE=1;
public static final int ACCESS=2;
public static final int POST_LOAD=3;
public static final int PRE_SAVE=4;
final Bag<E> bag;
final int operation;
final Object[] arguments;
public BagChanged(Bag<E> p,int op,Object ... args){
bag=p;
operation=op;
arguments=args;
}
public Bag<E> getBag() {
return bag;
}
public int getOperation() {
return operation;
}
public Object[] getArguments() {
return arguments;
}
}
final ArrayList<E> items=new ArrayList<>();
public Bag(){
}
public Bag(Iterable<E> o){
this(o.iterator());
}
public Bag(Iterator<E> o){
while(o.hasNext()) add(o.next());
}
@Override
public int size() {
return items.size();
}
@Override
public boolean isEmpty() {
return size()==0;
}
@Override
public boolean contains(Object o) {
final Iterator<E> it=iterator();
while(it.hasNext()){
final E e=it.next();
if(e!=null && o!=null && e.equals(o)) return true;
else if(e==o) return true;
}
return false;
}
@Override
public boolean containsAll(Collection<?> c) {
for (Object e : c) if (!contains(e)) return false;
return true;
}
public ListIterator<E> listIterator(){
return listIterator(0);
}
public ListIterator<E> listIterator(int offset){
return items.listIterator(offset);
}
@Override
public Iterator<E> iterator() {
return items.iterator();
}
@Override
public Object[] toArray() {
return toArray(new Object[size()]);
}
@Override
public <T> T[] toArray(T[] a) {
return items.toArray(a);
}
@Override
public boolean add(E e) {
if(items.contains(e)) return true;
if(countObservers()>0){
BagChanged<E> evt=new Bag.BagChanged<>(this,BagChanged.ADD,e);
setChanged();
notifyObservers(evt);
}
return items.add(e);
}
public Bag<E> append(E e){
add(e);
return this;
}
@Override
public boolean remove(Object o) {
if(!contains(o)) return false;
if(countObservers()>0){
BagChanged<E> evt=new Bag.BagChanged<>(this,BagChanged.REMOVE,o);
setChanged();
notifyObservers(evt);
}
return items.remove(o);
}
@Override
public boolean addAll(Collection<? extends E> c) {
if(countObservers()>0){
BagChanged<E> evt=new Bag.BagChanged<>(this,BagChanged.ADD,c.toArray());
setChanged();
notifyObservers(evt);
}
if(c==null || c.size()==0) return false;
c.forEach(e->{this.append(e);});
return true;
}
@Override
public boolean removeAll(Collection<?> c) {
if(countObservers()>0){
BagChanged<E> evt=new Bag.BagChanged<>(this,BagChanged.REMOVE,c!=null?c.toArray():null);
setChanged();
notifyObservers(evt);
}
if(c!=null){
return items.removeAll(c);
}else{
items.clear();
return true;
}
}
@Override
public boolean retainAll(Collection<?> c) {
return items.retainAll(c);
}
@Override
public void clear() {
removeAll(null);
}
}
-239
View File
@@ -1,239 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.util.Iterator;
/** constraint on a field.
* conditions can be leafs or groups such as and,or,not
*/
public class Check implements Iterable<Check> {
public static abstract class Op{
public abstract boolean met(Check c,Object val);
}
/** logical AND operation. */
public static Op AND=new Op(){
public String toString(){return "AND";}
public boolean met(Check c,Object val){
return true;
}
};
/** logical OR operation. */
public static Op OR=new Op(){
public String toString(){return "OR";}
public boolean met(Check c,Object val){
return true;
}
};
/** logical NOT operation. */
public static Op NOT=new Op(){
public String toString(){return "NOT";}
public boolean met(Check c,Object val){
return true;
}
};
/** arithmetic equal test. */
public static Op EQ=new Op(){
public String toString(){return "=";}
public boolean met(Check c,Object val){
return true;
}
};
/** arithmetic negated equal test. */
public static Op NEQ=new Op(){
public String toString(){return "<>";}
public boolean met(Check c,Object val){
return true;
}
};
/** greater than check. */
public static Op GT=new Op(){
public String toString(){return ">";}
public boolean met(Check c,Object val){
return true;
}
};
/** greater than or equal check. */
public static Op GTE=new Op(){
public String toString(){return ">=";}
public boolean met(Check c,Object val){
return true;
}
};
/** less than check. */
public static Op LT=new Op(){
public String toString(){return "<";}
public boolean met(Check c,Object val){
return true;
}
};
/** less than or equal check. */
public static Op LTE=new Op(){
public String toString(){return "<=";}
public boolean met(Check c,Object val){
return true;
}
};
/** like check case insensitive. */
public static Op LIKE=new Op(){
public String toString(){return "LIKE";}
public boolean met(Check c,Object val){
return true;
}
};
/** set membership check. */
public static Op IN=new Op(){
public String toString(){return "IN";}
public boolean met(Check c,Object val){
return true;
}
};
/** negated set membership check. */
public static Op NOT_IN=new Op(){
public String toString(){return "NOT IN";}
public boolean met(Check c,Object val){
return true;
}
};
/** iterator over checks.
*
*/
public static class CheckIterator implements Iterator<Check>{
final Check root;
Check cur;
int index;
public CheckIterator(Check ch){
root=ch;
cur=root;
index=0;
}
@Override
public boolean hasNext() {
return cur.isLeaf()==false && index<cur.args.length;
}
@Override
public Check next() {
return (Check)cur.args[index++];
}
}
public static Check and(Check... c) {
return new Check(AND,c);
}
public static Check all(Check... c) {
return new Check(AND,c);
}
public static Check or(Check... c) {
return new Check(OR,c);
}
public static Check any(Check... c) {
return new Check(OR,c);
}
public static Check not(Check... c) {
return new Check(NOT,c);
}
public static Check none(Check... c) {
return new Check(NOT,c);
}
public static Check eq(Field pk, Object... args) {
Object id=args;
if(id!=null && args.length==1) id=args[0];
return new Check(EQ,pk,id);
}
public static Check neq(Field pk, Object... args) {
Object id=args;
if(id!=null && args.length==1) id=args[0];
return new Check(NEQ,pk,id);
}
public static Check gt(Field pk, Object... args) {
Object id=args;
if(id!=null && args.length==1) id=args[0];
return new Check(GT,pk,id);
}
public static Check gte(Field pk, Object... args) {
Object id=args;
if(id!=null && args.length==1) id=args[0];
return new Check(GTE,pk,id);
}
public static Check lt(Field pk, Object... args) {
Object id=args;
if(id!=null && args.length==1) id=args[0];
return new Check(LT,pk,id);
}
public static Check lte(Field pk, Object... args) {
Object id=args;
if(id!=null && args.length==1) id=args[0];
return new Check(LTE,pk,id);
}
public static Check like(Field pk, Object... args) {
Object id=args;
if(id!=null && args.length==1) id=args[0];
return new Check(LIKE,pk,id);
}
public static Check in(Field pk, Object... id) {
return new Check(IN,pk,id);
}
public static Check not_in(Field pk, Object... id) {
return new Check(NOT_IN,pk,id);
}
Op code;
boolean leaf;
Object[] args;
boolean locked;
public Check(Op code,Field f,Object val){
this.code=code;
leaf=true;
args=new Object[]{f,val};
}
public Check(Op code,Check ... sub){
this.code=code;
leaf=false;
args=sub;
}
public Check setLocked(boolean f){
locked=f;
return this;
}
public boolean isLocked(){
return locked;
}
public Op getCode(){
return code;
}
public boolean isLeaf(){
return leaf;
}
public boolean met(Object val){
return code.met(this,val);
}
@Override
public Iterator<Check> iterator() {
return new CheckIterator(this);
}
public int getChildCount(){
return leaf?0:args.length;
}
public Check getChild(int index){
return leaf?null:(Check)args[index];
}
public Field getField(){
return (Field)args[0];
}
public Object getValue(){
return (Object)args[1];
}
public Check setValue(Object val){
if(locked) throw new IllegalStateException("check value is locked");
if(!leaf) throw new IllegalStateException("check is not a leaf");
args[1]=val;
return this;
}
}
-126
View File
@@ -1,126 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.io.IOException;
import com.reliancy.rec.Hdr;
import com.reliancy.rec.JSON;
import com.reliancy.rec.Rec;
import com.reliancy.rec.Slot;
/** Instance of an entity, usually a row in a table.
*
*/
public class DBO implements Rec{
public static enum Status{
NEW,USED,DELETED,COMPUTED
}
Terminal terminal;
Entity type;
Status status;
Object[] values;
public DBO() {
Class<? extends DBO> cls=this.getClass();
if(cls!=DBO.class){
Entity ent=Entity.recall(cls);
setType(ent);
}
status=Status.NEW;
}
@Override
public String toString(){
try {
StringBuffer ret=new StringBuffer();
JSON.writes(this,ret);
return ret.toString();
} catch (IOException e) {
return e.toString();
}
}
public Terminal getTerminal() {
return terminal;
}
public DBO setTerminal(Terminal terminal) {
this.terminal = terminal;
return this;
}
public Status getStatus(){
return status;
}
public DBO setStatus(Status s) {
this.status = s;
return this;
}
public final Entity getType() {
return type;
}
public final DBO setType(Entity type) {
this.type = type;
if(type==null){
values=null;
}else{
values=new Object[type.count()];
}
return this;
}
@Override
public Hdr meta() {
return type;
}
@Override
public int count() {
return values!=null?values.length:0;
}
@Override
public Rec set(int pos, Object val) {
if(pos<0) pos=count()+pos;
values[pos]=val;
return this;
}
@Override
public Object get(int pos) {
if(pos<0) pos=count()+pos;
return values[pos];
}
@Override
public Rec add(Object val) {
throw new UnsupportedOperationException("dbo is not array");
}
@Override
public Rec remove(int s) {
throw new UnsupportedOperationException("dbo is not array");
}
@Override
public Rec set(Slot s, Object val) {
if(s==null) throw new IllegalArgumentException("invalid key provided");
int index=s.getPosition(); // try slot position
//if(index<0) index=type.findSlot(s.getName());// fall back to search if slot not set
if(index<0){
throw new IllegalArgumentException("invalid key provided:"+s.getName());
}else{
values[index]=val;
}
return this;
}
@Override
public Object get(Slot s, Object def) {
if(s==null) throw new IllegalArgumentException("invalid key provided");
int index=s.getPosition(); // try slot position
//if(index<0) index=type.findSlot(s.getName());// fall back to search if slot not set
if(index<0) throw new IllegalArgumentException("invalid key provided:"+s.getName());
Object ret=values[index];
return ret==null?def:ret;
}
@Override
public Rec remove(Slot s) {
throw new UnsupportedOperationException("dbo is not resizable");
}
}
-185
View File
@@ -1,185 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.util.HashMap;
import java.util.Iterator;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collection;
import com.reliancy.rec.Hdr;
import com.reliancy.rec.Slot;
/** Describes an object structure, usually a table.
*
*/
public class Entity extends Hdr{
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public static @interface Info {
String name();
}
static final HashMap<String,Entity> registry=new HashMap<>();
public static final void publish(Entity ent){
registry.put(ent.getName(),ent);
registry.put(ent.getId(),ent);
}
public static final void retract(Entity ent){
if(ent==null) return;
Collection<Entity> vals=registry.values();
while(vals.remove(ent)){}
}
public static final void retract(Class<? extends DBO> cls){
Entity ent=recall(cls.getSimpleName());
if(ent!=null){
retract(ent);
}
}
public static final Entity recall(String name){
return registry.get(name);
}
public static final Entity recall(Class<? extends DBO> cls){
Entity ent=recall(cls.getSimpleName());
if(ent==null){
ent=publish(cls);
}
return ent;
}
/**
* this method will analyze a DBO class and forumate an Entity object out of it.
* @param cls
* @return
*/
@SuppressWarnings("unchecked")
public static final Entity publish(Class<? extends DBO> cls){
Entity ret=registry.get(cls.getSimpleName());
if(ret!=null) return ret;
//System.out.println("Analyzing:"+cls);
Class<?> base=cls.getSuperclass();
Entity base_ent=null;
int position0=0;
if(base!=null && base!=DBO.class){
base_ent=publish((Class<? extends DBO>)base);
position0=base_ent.count();
}
java.lang.reflect.Field[] declaredFields = cls.getDeclaredFields();
ArrayList<Field> slots=new ArrayList<>();
for (java.lang.reflect.Field field : declaredFields) {
if (!java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
continue;
}
try {
String sf_name=field.getName();
Field slot=(Field) field.get(cls);
slot.setId(sf_name);
slot.setPosition(position0+slots.size());
slots.add(slot);
//System.out.println(sf_name+":"+slot+" atpos:"+slot.getPosition());
} catch (Exception e) {
}
}
Info info=cls.getAnnotation(Info.class);
ret=new Entity(info!=null?info.name():cls.getSimpleName()).setId(cls.getSimpleName());
ret.setBase(base_ent);
ret.setType(cls);
ret.getOwnSlots().addAll(slots);
publish(ret);
return ret;
}
Entity base;
String id;
Field pk;
public Entity(String name) {
super(name);
}
@Override
public Slot makeSlot(String name){
return new Field(name);
}
@Override
public Iterator<Slot> iterator(int offset){
if(offset>0) throw new IllegalArgumentException("Offset not supported");
final Entity ent=this;
return new Iterator<Slot>(){
final FieldSlice slice=new FieldSlice(ent).including(Field.FLAG_STORABLE);
@Override
public boolean hasNext() {
return slice.hasNext();
}
@Override
public Slot next() {
return slice.next();
}
};
}
@Override
public int count(){
return super.count()+(base!=null?base.count():0);
}
/**
* gets a slot which could be here or in base.
*/
@Override
public Slot getSlot(int pos){
if(base!=null){ // we got base
int ofs=base.count();
if(pos<ofs) return base.getSlot(pos);
else return super.getSlot(pos-ofs);
}else{ // regular no base
return super.getSlot(pos);
}
}
public Field getField(int index){
return (Field)getSlot(index);
}
public int getDepth(){
return base!=null?1+base.getDepth():0;
}
public Entity getBase() {
return base;
}
public Entity setBase(Entity base) {
this.base = base;
return this;
}
public String getId() {
return id;
}
public Entity setId(String id) {
this.id = id;
return this;
}
public Entity setPk(Field pk) {
this.pk = pk;
return this;
}
public Field getPk(){
if(pk!=null) return pk;
// try to locate the pk - this now gos over base as well
for(int i=0;i<count() && pk==null;i++){
Field pp=(Field) getSlot(i);
if(pp.isPk()) pk=pp;
}
return pk;
}
public DBO newInstance() throws InstantiationException, IllegalAccessException{
return newInstance(null).setStatus(DBO.Status.NEW);
}
public DBO newInstance(Terminal t) throws InstantiationException, IllegalAccessException{
Class<?> cls=getType();
DBO ret=(DBO) cls.newInstance();
ret.setType(this).setTerminal(t).setStatus(DBO.Status.NEW);
return ret;
}
}
-108
View File
@@ -1,108 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.math.BigDecimal;
import java.sql.Date;
import java.sql.Timestamp;
import com.reliancy.rec.Slot;
/**
* Description of a column or property.
*/
public class Field extends Slot {
public static Field Int(String name){
return new Field(name,Integer.class);
}
public static Field Str(String name){
return new Field(name,String.class);
}
public static Field Bool(String name){
return new Field(name,Boolean.class);
}
public static Field Float(String name){
return new Field(name,Float.class);
}
public static Field Num(String name){
return new Field(name,BigDecimal.class);
}
public static Field Date(String name){
return new Field(name,Date.class);
}
public static Field DateTime(String name){
return new Field(name,Timestamp.class);
}
public static final int FLAG_PK =0x0100;
public static final int FLAG_AUTOINC =0x0200;
String id;
String typeParams;
public Field(String name) {
super(name);
this.raiseFlags(Field.FLAG_STORABLE);
}
public Field(String name,Class<?> typ) {
super(name,typ);
this.raiseFlags(Field.FLAG_STORABLE);
}
@Override
public boolean equals(String str){
return super.equals(str) || (id!=null && id.equalsIgnoreCase(str));
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public boolean isPk() {
return checkFlags(FLAG_PK);
}
public Field setPk(boolean pk) {
if(pk) raiseFlags(FLAG_PK); else clearFlags(FLAG_PK);
return this;
}
public boolean isAutoIncrement() {
return checkFlags(FLAG_AUTOINC);
}
public Field setAutoIncrement(boolean pk) {
if(pk) raiseFlags(FLAG_AUTOINC); else clearFlags(FLAG_AUTOINC);
return this;
}
public String getTypeParams() {
return typeParams;
}
public Field setTypeParams(String p) {
typeParams=p;
return this;
}
public Check eq(Object... val) {
return Check.eq(this,val);
}
public Check neq(Object... val) {
return Check.neq(this,val);
}
public Check gt(Object... val) {
return Check.gt(this,val);
}
public Check gte(Object... val) {
return Check.gte(this,val);
}
public Check lt(Object... val) {
return Check.lt(this,val);
}
public Check lte(Object... val) {
return Check.lte(this,val);
}
public Check like(Object... val) {
return Check.like(this,val);
}
public Check in(Object... val) {
return Check.in(this,val);
}
}
@@ -1,109 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.util.Iterator;
import java.util.List;
/** Field iterator matching flags over entity hierarchy.
*
*/public class FieldSlice implements Iterator<Field>,Iterable<Field>{
protected final Entity entity;
protected FieldSlice sup;
protected int includeMask;
protected int excludeMask;
protected int raw_index;
protected Field next_field;
protected int next_index;
protected Entity next_entity;
public FieldSlice(Entity ent){
entity=ent;
if(entity.getBase()!=null){
sup=new FieldSlice(ent.getBase());
}else{
sup=null;
}
raw_index=-1;
next_index=-1;
}
public FieldSlice including(int mask){
includeMask=mask;
if(sup!=null) sup.including(mask);
return this;
}
public FieldSlice excluding(int mask){
excludeMask=mask;
if(sup!=null) sup.excluding(mask);
return this;
}
/** we add rewind capability to allow reuse of same fieldslice.
* i.e we use it to generate recipe, then later to properly enumerate values.
*/
public FieldSlice rewind(){
raw_index=-1;
next_index=-1;
next_field=null;
next_entity=null;
if(sup!=null) sup.rewind();
return this;
}
// search for next valid field
public final Field findNext(){
List<?> local=entity.getOwnSlots();
if(raw_index>=local.size()) return null; // we have exhausted local supply
next_field=null; // clear prev result
// search at base
if(sup!=null && sup.hasNext()){
next_field=sup.next();
next_index++;
next_entity=sup.nextEntity();
return next_field;
}
next_entity=entity;
// now search locally
for(raw_index=raw_index+1;raw_index<local.size();raw_index++){
Field f=(Field) local.get(raw_index);
int attr=f.getFlags();
if((attr & excludeMask)!=0) continue; // skip if in exluding set
if((attr & includeMask)==0) continue; // skip if not in including set
next_field=f;
next_index+=1;
break;
}
return next_field;
}
@Override
public Iterator<Field> iterator() {
return this;
}
@Override
public boolean hasNext() {
Field next=findNext();
return next!=null;
}
@Override
public Field next() {
return next_field;
}
public int nextIndex(){
return next_field!=null?next_index:-1;
}
public Entity nextEntity(){
return next_entity;
}
public DBO makeRecord() throws InstantiationException, IllegalAccessException{
return entity.newInstance();
}
public void writeRecord(DBO rec,Object val){
rec.set(next_field, val);
}
public Object readRecord(DBO rec,Object def){
return rec.get(next_field, def);
}
}
-270
View File
@@ -1,270 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.reliancy.util.Handy;
public final class SQL implements Appendable{
public final static Object NULL=new Object();
public final static String WS=" ";
public final static String SELECT="SELECT";
public final static String INSERT="INSERT INTO";
public final static String UPDATE="UPDATE";
public final static String DELETE="DELETE";
public final static String FROM="FROM";
public final static String INNER_JOIN="INNER JOIN";
public final static String ON="ON";
public final static String SET="SET";
public final static String WHERE="WHERE";
final StringBuffer buffer=new StringBuffer();
final SQLTerminal terminal;
final String ql,qr;
final HashMap<Entity,String> entAlias=new HashMap<>();
public SQL(SQLTerminal terminal){
this.terminal=terminal;
ql=terminal!=null?terminal.getQuoteLeft():"\"";
qr=terminal!=null?terminal.getQuoteRight():"\"";
}
@Override
public String toString(){
return buffer.toString();
}
@Override
public final SQL append(CharSequence csq){
buffer.append(csq);
return this;
}
@Override
public final SQL append(CharSequence csq, int start, int end){
buffer.append(csq,start,end);
return this;
}
@Override
public final SQL append(char c){
buffer.append(c);
return this;
}
public final SQL select(){
append(SELECT);
return this;
}
public final SQL insert(){
append(INSERT);
return this;
}
public final SQL update(){
append(UPDATE);
return this;
}
public final SQL delete(){
append(DELETE);
return this;
}
public final SQL from(){
append(WS).append(FROM).append(WS);
return this;
}
public final SQL inner_join(){
append(WS).append(INNER_JOIN).append(WS);
return this;
}
public final SQL on(){
append(WS).append(ON).append(WS);
return this;
}
public final String wrap(String id){
if(id.startsWith(ql) && id.endsWith(qr)){
return id;
}else{
return ql+id.replace(".",qr+"."+ql)+qr;
}
}
public final SQL id(String id){
if(id.startsWith(ql) && id.endsWith(qr)){
append(id);
}else{
append(ql).append(id.replace(".",qr+"."+ql)).append(qr);
}
return this;
}
public final String getAlias(Entity e){
String eAlias=entAlias.get(e);
if(eAlias!=null) return eAlias;
eAlias="e"+entAlias.size();
entAlias.put(e,eAlias);
return eAlias;
}
public final SQL select(Entity ent,FieldSlice fit){
entAlias.clear();;
select();
while(fit.hasNext()){
int index=fit.nextIndex();
Field f=fit.next();
Entity e=fit.nextEntity();
String alias=getAlias(e);
//System.out.println("It:"+index+":/"+f+"/"+e+"/"+alias);
append(index==0?" ":",");
append(alias).append(".").id(f.getName());
}
from();
String eAlias=getAlias(ent);
id(ent.getName()).append(" ").append(eAlias);
for(Entity b=ent.getBase();b!=null;b=b.getBase()){
String bAlias=getAlias(b);
inner_join();
id(b.getName()).append(" ").append(bAlias);
on();
Field bPk=b.getPk();
Field ePk=ent.getPk();
append(eAlias).append(".").id(ePk.getName());
append("=");
append(bAlias).append(".").id(bPk.getName());
}
return this;
}
public final SQL where(){
append(WS).append(WHERE).append(WS);
return this;
}
public final SQL where(Check filter) {
append(WS).append(WHERE).append(WS).check(filter);
return this;
}
/// using entalias locate field entity and its prefix
public final String getFieldPrefix(Field f){
for(Map.Entry<Entity,String> e:entAlias.entrySet()){
Entity ent=e.getKey();
String prefix=e.getValue();
if(ent.isOwned(f)) return prefix+".";
}
return "";
}
public final SQL check(Check filter) {
if(filter.isLeaf()){
Check.Op op=filter.getCode();
Field field=filter.getField();
String fname=wrap(filter.getField().getName());
String opname=op.toString();
String arg="?";
Object val=filter.getValue();
if(op==Check.LIKE && terminal!=null && terminal.getProtocol().contains(":postgre")){
opname="ILIKE";
}
if(op==Check.IN){
arg="("+Handy.toString(val)+")";
}
if(Handy.isEmpty(val)){
// if not value then shortcuircuid condition
fname="1";
opname="=";
arg="1";
}
if(val==NULL){
arg="NULL";
if(op==Check.EQ) opname="IS";
if(op==Check.NEQ) opname="IS NOT";
}
append("(");
String fprefix=getFieldPrefix(field);
append(fprefix).append(fname).append(WS).append(opname).append(WS).append(arg);
append(")");
}else{
Check.Op op=filter.getCode();
String delim=op.toString();
if(op==Check.NOT){
append(delim).append("(").check(filter.getChild(0)).append(")");
}else{
append("(");
for(int i=0;i<filter.getChildCount();i++){
if(i>0) append(WS).append(delim).append(WS);
check(filter.getChild(i));
}
append(")");
}
}
return this;
}
/** fills params list with non-trivial parameters.
* we place this method here to be as close as possible to the one which generates the sql code.
* check and check_export must be in synch.
* @param filter check operation to perform over conditions.
* @param params extracted params.
*/
public final void check_export(Check filter,List<Object> params) {
if(filter.isLeaf()){
Check.Op op=filter.getCode();
Object val=filter.getValue();
if(Handy.isEmpty(val) || val==NULL || op==Check.IN) return; // skip over empty or NULL values
params.add(val);
}else{
for(Check ch:filter) check_export(ch,params);
}
}
/** fills check values from dbo record where equal and not-equal are used.
* we place this method here to be as close as possible to the one which generates the sql code.
* check and check_import must be in synch.
* @param filter set of checks
* @param rec record to check
*/
public final void check_import(Check filter,DBO rec) {
if(filter.isLeaf()){
if(filter.isLocked()) return; // no import on locked condition
Check.Op op=filter.getCode();
if(op!=Check.EQ && op!=Check.NEQ) return; // skip over all conditions except = and <>
Field f=filter.getField();
Object val=f.get(rec,null);
filter.setValue(val);
}else{
for(Check ch:filter) check_import(ch,rec);
}
}
public final SQL insert(Entity entity,List<Field> supplied){
insert();
append(SQL.WS).id(entity.getName()).append(" (");
StringBuffer ext=new StringBuffer();
String delim="";
Field pk=entity.getPk();
if(!entity.isOwned(pk)){
append(delim).id(pk.getName());
ext.append(delim).append("?");
delim=",";
}
for(int index=0;index<supplied.size();index++){
Field f=supplied.get(index);
if(index>0) delim=",";
append(delim).id(f.getName());
ext.append(delim).append("?");
}
append(") VALUES (").append(ext).append(")");
return this;
}
public final SQL update(Entity entity,List<Field> supplied){
update();
append(SQL.WS).id(entity.getName()).append(" SET ");
for(int index=0;index<supplied.size();index++){
Field f=supplied.get(index);
String delim=index==0?"":",";
append(delim);
id(f.getName()).append("=?");
}
where();
Field pk=entity.getPk();
id(pk.getName()).append("=?");
return this;
}
public final SQL delete(Entity entity){
delete().from().id(entity.getName());
return this;
}
}
@@ -1,152 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.io.Closeable;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
/** Helper object which impleents DBO deleting.
* It manages the recipe and the prepared statmenet.
* The cleaner works in two ways.
* If you supply items iterator it will delete by pk id those items.
* If you supply a Check filter and no items then it will delete based on a where statement.
*/
public class SQLCleaner implements Closeable{
protected final Entity entity;
protected final SQLTerminal terminal;
protected final SQLCleaner base; /// used for nesting
protected final SQL sql;
protected final ArrayList<Object> params;
protected Check filter;
protected Connection external;
protected PreparedStatement deleteStmt;
protected int itemsDeleted;
protected Exception error;
public SQLCleaner(Entity ent,SQLTerminal t) {
entity=ent;
terminal=t;
base=(entity.getBase()!=null)?new SQLCleaner(entity.getBase(),t):null;
sql=new SQL(terminal);
params=new ArrayList<>();
}
public SQL compileRecipe(){
if(filter==null){
// no filter we go with PK
Field pk=entity.getPk();
filter=pk.eq("?");
}
sql.delete(entity).where(filter);
return sql;
}
public boolean isLinkExternal(){
return external!=null;
}
public SQLCleaner setExternalLink(Connection link){
external=link;
return this;
}
protected Connection getExternalLink(){
return external;
}
protected Connection getInternalLink(){
try{
if(deleteStmt!=null) return deleteStmt.getConnection();
}catch(SQLException ex){
}
return null;
}
public SQLCleaner open() throws SQLException{
return open(null);
}
public SQLCleaner open(Check where) throws SQLException{
this.filter=where;
Connection link=isLinkExternal()?getExternalLink():terminal.getConnection();
if(base!=null) base.setExternalLink(link).open(filter); // definitely external link for base
SQL delSQL=compileRecipe();
//System.out.println("DEL:"+delSQL+"/"+filter);
deleteStmt=link.prepareStatement(delSQL.toString());
return this;
}
@Override
public void close() throws IOException{
if(base!=null) base.close(); // since link is external it will not close link just the rest
Connection link=getInternalLink();
if(deleteStmt!=null){
try{
deleteStmt.close();
}catch(SQLException ex){
if(error==null) error=ex;
}
}
try{
if(link!=null && !isLinkExternal()) link.close();
external=null;
}catch(SQLException ex){
if(error==null) error=ex;
}
if(error!=null){
if(error instanceof IOException) throw (IOException)error;
else throw new IOException(error);
}
}
public void flush(Iterator<DBO> items) throws SQLException {
Connection link=getInternalLink();
boolean autocommited=link.getAutoCommit();
try{
link.setAutoCommit(false);
if(items==null){ // deleting by filter
throw new SQLException("delete by filter not implemented yet");
// we would need to leave the primary filter
// we would use filter in an ID in (SUBQUERY)
// this is because filter could reference all entities and we have inheritance so multiple
// we would generate a select statement with filter and selecting only ID
}else{ // deleting by incoming records
while(items.hasNext()){
DBO rec=items.next();
deleteRecord(rec);
}
}
if(!link.getAutoCommit()){
link.commit();
}
}catch(SQLException ex){
if(!link.getAutoCommit()){
link.rollback();
}
throw ex;
}finally{
link.setAutoCommit(autocommited);
}
}
/**
* This calls one delete. It can and is called from outside in case of nesting when link is external.
* @param rec database object to delete
* @throws SQLException sql related error
*/
public boolean deleteRecord(DBO rec) throws SQLException{
if(rec==null) return false;
if(base!=null) base.deleteRecord(rec); // save the superclass first
sql.check_import(filter,rec); // get values from dbo
params.clear();
sql.check_export(filter,params); // move them into params
for(int pindex=0;pindex<params.size();pindex++){
Object val=params.get(pindex);
deleteStmt.setObject(pindex+1,val);
}
int dcode=deleteStmt.executeUpdate();
itemsDeleted+=dcode;
return dcode>0;
}
}
@@ -1,134 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import com.reliancy.dbo.Action.Load;
/** SQLIterator will delay closing a connection and will iterate over result set.
* TODO: no support for orderby yet
*/
public class SQLReader implements SiphonIterator<DBO>{
protected final Entity entity;
protected final SQLTerminal terminal;
protected final SQL sql;
protected FieldSlice slice;
protected ResultSet result;
protected Exception error;
public SQLReader(Entity ent,SQLTerminal t) {
this.entity=ent;
terminal=t;
// slice controls sql fields but also lets us correctly import values later
slice=new FieldSlice(entity).including(Field.FLAG_STORABLE);
sql=new SQL(terminal);
}
public SQLReader open() throws SQLException{
return open(null);
}
public SQLReader open(Action action) throws SQLException{
error=null;
if(action==null){
sql.select(entity,slice); // simple case
}else{
compileRecipe(action); // complete case
}
//System.out.println("SQL:"+sql);
Connection link=terminal.getConnection();
PreparedStatement prep=link.prepareStatement(sql.toString());
if(action!=null){
Load tr=(Load) action.getTrait();
if(tr.filter!=null){
ArrayList<Object> params=new ArrayList<>();
sql.check_export(tr.filter, params);
for(int pindex=0;pindex<params.size();pindex++){
Object val=params.get(pindex);
prep.setObject(pindex+1,val);
}
}
}
result=prep.executeQuery();
if(link.getAutoCommit()==false) link.commit();
//action.setItems(this); -- maybe we want multiple readers on same actions - leave this to terminal
return this;
}
public SQL compileRecipe(Action action){
sql.select(action.getEntity(),slice);
Load tr=(Load) action.getTrait();
if(tr.filter!=null){
sql.where(tr.filter);
}
return sql;
}
@Override
public boolean hasNext() {
try {
return error==null?result.next():false;
} catch (SQLException e) {
error=e;
return false;
}
}
@Override
public DBO next() {
try {
DBO ret=(DBO) slice.makeRecord();
FieldSlice fit=slice.rewind();
while(fit.hasNext()){
int findex=fit.nextIndex();
//Field field=fit.next();
Object val=result.getObject(findex+1);
fit.writeRecord(ret, val);
}
ret.setStatus(DBO.Status.USED);
return ret;
} catch (Exception e) {
error=e;
return null;
}
}
@Override
public void close() throws IOException {
if(result!=null){
Statement stmt=null;
Connection link=null;
try{
stmt=result.getStatement();
link=stmt!=null?stmt.getConnection():null;
if(!result.isClosed()) result.close();
}catch(SQLException ex){
if(error==null) error=ex;
}
try{
if(stmt!=null) stmt.close();
}catch(SQLException ex){
if(error==null) error=ex;
}
try{
if(link!=null) link.close();
}catch(SQLException ex){
if(error==null) error=ex;
}
}
if(error!=null){
if(error instanceof IOException) throw (IOException)error;
else throw new IOException(error);
}
}
}
@@ -1,231 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.io.IOException;
import java.lang.reflect.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.JDBCType;
import java.sql.SQLException;
import java.sql.Types;
import java.util.HashMap;
import java.util.Map;
import com.reliancy.util.Path;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
/** SQL particular implementation of a terminal.
* It will use a connection pool under it to take care of connection re-use.
*
*/
public class SQLTerminal implements Terminal{
HikariConfig config = new HikariConfig();
HikariDataSource ds;
Path url;
String quoteLeft="\""; // quotes could be subject to sql flavour
String quoteRight="\"";
public SQLTerminal(String url){
this.url=new Path(url);
String proto=this.url.getProtocol();
if(!proto.startsWith("jdbc:")) proto="jdbc:"+proto;
String u=proto+"://"+this.url.getHost()+":"+this.url.getPort()+"/"+this.url.getDatabase();
config.setJdbcUrl(u);
config.setUsername(this.url.getUserid());
config.setPassword(this.url.getPassword());
//config.setAutoCommit(false); -- do this in batch cases only
config.addDataSourceProperty( "cachePrepStmts" , "true" );
config.addDataSourceProperty( "prepStmtCacheSize" , "250" );
config.addDataSourceProperty( "prepStmtCacheSqlLimit" , "2048" );
ds = new HikariDataSource( config );
}
public Connection getConnection() throws SQLException{
return ds.getConnection();
}
@Override
public Action execute(Action q) throws IOException{
// System.out.println("Executing..."+q.getTrait());
Action.Trait tr=q.getTrait();
if(tr instanceof Action.Load){
Entity ent=q.getEntity();
SQLReader reader=new SQLReader(ent,this);
try {
reader.open(q);
q.setItems(reader);
} catch (SQLException e) {
reader.close();
throw new IOException(e);
}
//System.out.println("Executing...Done");
return q;
}else if(tr instanceof Action.Save){
Entity ent=q.getEntity();
try(SQLWriter writer=new SQLWriter(ent,this)) {
writer.open();
writer.flush(q.getItems());
//System.out.println("Executing...Done");
return q;
}catch(SQLException e){
throw new IOException(e);
}
}else if(tr instanceof Action.Delete){
Entity ent=q.getEntity();
try(SQLCleaner cleaner=new SQLCleaner(ent,this)) {
cleaner.open();
cleaner.flush(q.getItems());
//System.out.println("Executing...Done");
return q;
}catch(SQLException e){
throw new IOException(e);
}
}else{
throw new UnsupportedOperationException("Trait not supported:"+tr);
}
}
public String getProtocol() {
return url.getProtocol();
}
public String getQuoteLeft(){
return this.quoteLeft;
}
public String getQuoteRight(){
return this.quoteRight;
}
final HashMap<Integer,Class<?>> sql2java=new HashMap<>();
final HashMap<Class<?>,Integer> java2sql=new HashMap<>();
public Map<Class<?>,Integer> getJava2SQL(){
if(!java2sql.isEmpty()) return java2sql;
String protocol=url.getProtocol();
java2sql.put(java.math.BigDecimal.class,Types.DECIMAL);
java2sql.put(java.math.BigInteger.class,Types.DECIMAL);
java2sql.put(Boolean.class,protocol.contains(":oracle")?Types.INTEGER:Types.BOOLEAN);
java2sql.put(Byte.class,Types.TINYINT);
java2sql.put(Short.class,Types.SMALLINT);
java2sql.put(Integer.class,Types.INTEGER);
java2sql.put(Long.class,Types.BIGINT);
java2sql.put(Float.class,Types.FLOAT);
java2sql.put(Double.class,Types.DOUBLE);
java2sql.put(byte[].class,Types.VARBINARY);
java2sql.put(Blob.class,Types.BLOB);
java2sql.put(char[].class,Types.VARCHAR);
java2sql.put(String.class,Types.VARCHAR);
java2sql.put(StringBuffer.class,Types.VARCHAR);
java2sql.put(Clob.class,Types.CLOB);
java2sql.put(java.sql.Date.class,Types.DATE);
java2sql.put(java.sql.Time.class,Types.TIME);
java2sql.put(java.sql.Timestamp.class,Types.TIMESTAMP);
java2sql.put(Array.class,Types.ARRAY);
return java2sql;
}
public Map<Integer,Class<?>> getSQL2Java(){
if(!sql2java.isEmpty()) return sql2java;
//String protocol=url.getProtocol();
sql2java.put(Types.NUMERIC,java.math.BigDecimal.class);
sql2java.put(Types.DECIMAL,java.math.BigDecimal.class);
sql2java.put(Types.BIT,Boolean.class);
sql2java.put(Types.BOOLEAN,Boolean.class);
sql2java.put(Types.TINYINT,Byte.class);
sql2java.put(Types.SMALLINT,Short.class);
sql2java.put(Types.INTEGER,Integer.class);
sql2java.put(Types.BIGINT,Long.class);
sql2java.put(Types.REAL,Float.class);
sql2java.put(Types.FLOAT,Float.class);
sql2java.put(Types.DOUBLE,Double.class);
sql2java.put(Types.BINARY,byte[].class);
sql2java.put(Types.VARBINARY,byte[].class);
sql2java.put(Types.LONGVARBINARY,byte[].class);
sql2java.put(Types.CHAR,String.class);
sql2java.put(Types.NCHAR,String.class);
sql2java.put(Types.VARCHAR,String.class);
sql2java.put(Types.NVARCHAR,String.class);
sql2java.put(Types.LONGVARCHAR,String.class);
sql2java.put(Types.LONGNVARCHAR,String.class);
sql2java.put(Types.DATE,java.sql.Date.class);
sql2java.put(Types.TIME,java.sql.Time.class);
sql2java.put(Types.TIMESTAMP,java.sql.Timestamp.class);
sql2java.put(Types.BLOB,byte[].class);
sql2java.put(Types.CLOB,char[].class);
sql2java.put(Types.ARRAY,java.sql.Array.class);
sql2java.put(Types.JAVA_OBJECT,Object.class);
return sql2java;
}
/**
* Returns back java class for given id and or name.
* The name is not used in default implementation.
* @param typeid sql type to map
* @return Class matching sql typeid.
*/
public Class<?> getJavaType(int typeid) {
Class<?> ret=getSQL2Java().get(typeid);
return ret;
}
/**
* This method will correct cases when sqltype is varchar (12) but type name is date or similar.
* @param sqltype
* @param type_name
* @return tries to promote sqltype given type name to something more specific.
*/
public int getTypeId(int sqltype,String type_name){
if(type_name==null) return sqltype;
type_name=type_name.toLowerCase();
if(sqltype==Types.VARCHAR || sqltype==Types.CHAR){
if(type_name.equals("date")) sqltype=Types.DATE;
if(type_name.equals("time")) sqltype=Types.TIME;
if(type_name.equals("datetime")) sqltype=Types.TIMESTAMP;
}
return sqltype;
}
/**
* @param cls
* @param createParams
* @return SQL type given java class and create params
*/
public int getTypeId(Class<?> cls,String createParams){
int ret=getJava2SQL().get(cls);
return ret;
}
public String getTypeName(Class<?> cls,String createParams){
int id=this.getTypeId(cls, createParams);
String ret = JDBCType.valueOf(id).getName();
if(ret==null) return null;
String protocol=url.getProtocol();
if(protocol.contains(":sqlserver")){
if("boolean".equalsIgnoreCase(ret)) ret="BIT";
if("timestamp".equalsIgnoreCase(ret)) ret="DATETIME";
if("double".equalsIgnoreCase(ret)) ret="float";
if("float".equalsIgnoreCase(ret)) ret="real";
}
if(protocol.contains(":postgre")){
if("varbinary".equalsIgnoreCase(ret)) ret="bytea";
if("double".equalsIgnoreCase(ret)) ret="double precision";
}
if("varchar".equalsIgnoreCase(ret) && (createParams!=null && !createParams.isEmpty())){
long size=Long.parseLong(createParams);
if(protocol.contains(":sqlserver")) ret=size>8000?ret.concat("(").concat("MAX").concat(")"):ret.concat("(").concat(String.valueOf(size)).concat(")");
else if(protocol.contains(":oracle")) ret=size>2000?"CLOB":ret.concat("(").concat(String.valueOf(size)).concat(")");
else if(protocol.contains(":mysql")) ret=size>Character.MAX_VALUE?"TEXT":ret.concat("(").concat(String.valueOf(size)).concat(")");
else if(protocol.contains(":h2")) ret=size>Integer.MAX_VALUE?"CLOB":ret.concat("(").concat(String.valueOf(size)).concat(")");
else if(protocol.contains(":postgre")) ret=size>Character.MAX_VALUE?"TEXT":ret.concat("(").concat(String.valueOf(size)).concat(")");
else ret=(size>Character.MAX_VALUE)?"CLOB":ret.concat("(").concat(String.valueOf(size)).concat(")");
}
String args=null;
if(ret.indexOf('(')==-1 && createParams!=null && !createParams.isEmpty()){
if("decimal".equalsIgnoreCase(ret)) args=createParams;
if("numeric".equalsIgnoreCase(ret)) args=createParams;
}
if(args!=null){
ret=ret.concat("(").concat(args).concat(")");
}
return ret;
}
}
@@ -1,222 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.io.Closeable;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import com.reliancy.util.Handy;
/** Helper object which impleents DBO saving. It manages the recipe and the prepared statmenet.
* Also keeps track of which fields are sent down to DB.
* The writer does the work in flush method during which it exhausts the items. While in flush it
* will disable autocommit and enable it at the end. We could call flush multiple times to send multiple
* batches down to DB.
* Initially we would ctor with Action object but actually we need to generate writes for different entities
* especially in inheritance cases.
*/
public class SQLWriter implements Closeable{
protected final Entity entity;
protected final SQLTerminal terminal;
protected final SQLWriter base; /// used for nesting
protected final ArrayList<Field> supplied=new ArrayList<Field>();
protected final ArrayList<Field> generated=new ArrayList<Field>();
protected String insertSQL;
protected String updateSQL;
protected Connection external;
protected PreparedStatement insertStmt;
protected PreparedStatement updateStmt;
protected int itemsInserted;
protected int itemsUpdated;
protected Exception error;
public SQLWriter(Entity ent,SQLTerminal t) {
entity=ent;
terminal=t;
base=(entity.getBase()!=null)?new SQLWriter(entity.getBase(),t):null;
// we select proper fields for this entity
FieldSlice slice=new FieldSlice(entity).including(Field.FLAG_STORABLE); // includes all even autoincrement
while(slice.hasNext()){
Field f=slice.next();
Entity e=slice.nextEntity();
if(e!=entity) continue; // skip if not part of this entity
if(f.isAutoIncrement()){
generated.add(f);
}else{
supplied.add(f);
}
}
}
public String compileInsertRecipe(){
if(insertSQL!=null) return insertSQL;
SQL buf=new SQL(terminal);
buf.insert(entity,supplied);
insertSQL=buf.toString();
return insertSQL;
}
public String compileUpdateRecipe(){
if(updateSQL!=null) return updateSQL;
SQL buf=new SQL(terminal);
buf.update(entity,supplied);
updateSQL=buf.toString();
return updateSQL;
}
public boolean isLinkExternal(){
return external!=null;
}
public SQLWriter setExternalLink(Connection link){
external=link;
return this;
}
protected Connection getExternalLink(){
return external;
}
protected Connection getInternalLink(){
try{
if(insertStmt!=null) return insertStmt.getConnection();
if(updateStmt!=null) return updateStmt.getConnection();
}catch(SQLException ex){
}
return null;
}
public SQLWriter open() throws SQLException{
Connection link=isLinkExternal()?getExternalLink():terminal.getConnection();
if(base!=null) base.setExternalLink(link).open(); // definitely external link for base
String inSql=compileInsertRecipe();
String upSql=compileUpdateRecipe();
//System.out.println("INS:"+inSql);
//System.out.println("UPD:"+upSql);
String[] genkeys=new String[generated.size()];
for(int i=0;i<generated.size();i++){
Field f=generated.get(i);
genkeys[i]=f.getName();
}
insertStmt=link.prepareStatement(inSql,genkeys);
updateStmt=link.prepareStatement(upSql);
//result=prep.executeQuery();
//if(link.getAutoCommit()==false) link.commit();
return this;
}
@Override
public void close() throws IOException{
if(base!=null) base.close(); // since link is external it will not close link just the rest
Connection link=getInternalLink();
if(insertStmt!=null){
try{
insertStmt.close();
}catch(SQLException ex){
if(error==null) error=ex;
}
}
if(updateStmt!=null){
try{
updateStmt.close();
}catch(SQLException ex){
if(error==null) error=ex;
}
}
try{
if(link!=null && external!=link) link.close();
external=null;
}catch(SQLException ex){
if(error==null) error=ex;
}
if(error!=null){
if(error instanceof IOException) throw (IOException)error;
else throw new IOException(error);
}
}
public void flush(Iterator<DBO> items) throws SQLException {
Connection link=isLinkExternal()?getExternalLink():getInternalLink();
boolean autocommited=link.getAutoCommit();
try{
link.setAutoCommit(false);
while(items.hasNext()){
DBO rec=items.next();
writeRecord(rec);
}
if(!link.getAutoCommit()){
link.commit();
}
}catch(SQLException ex){
if(!link.getAutoCommit()){
link.rollback();
}
throw ex;
}finally{
link.setAutoCommit(autocommited);
}
}
/**
* This calls one update/insert. It can and is called from outside in case of nesting when link is external.
* @param rec
* @throws SQLException
*/
public boolean writeRecord(DBO rec) throws SQLException{
if(base!=null) base.writeRecord(rec); // save the superclass first
// select mode
int pindex=0;
Field pk=entity.getPk();
boolean pk_owned=entity.isOwned(pk);
PreparedStatement stmt=null;
if(rec.getStatus()==DBO.Status.NEW){
stmt=insertStmt;
// need to inject pk here is not owned
if(!pk_owned) stmt.setObject(++pindex,pk.get(rec,null),terminal.getTypeId(pk.getType(),pk.getTypeParams()));
}
if(rec.getStatus()==DBO.Status.USED){
stmt=updateStmt;
// update has a pk condition for sure
Object pkval=pk.get(rec,null);
if(Handy.isEmpty(pkval)) throw new SQLException("Used object with empty PK");
//System.out.println("UPDT:"+stmt+"/"+pkval);
stmt.setObject(
supplied.size()+1,
pkval,
terminal.getTypeId(pk.getType(),pk.getTypeParams())
);
}
if(stmt==null) return false;
// copy values
for(int index=0;index<supplied.size();index++){
Field f=supplied.get(index);
pindex+=1;
int tid=terminal.getTypeId(f.getType(),f.getTypeParams());
Object val=f.get(rec,null);
//System.out.println("Param:"+pindex+":"+f.getName()+":"+val);
stmt.setObject(pindex,val,tid);
}
int ucode=stmt.executeUpdate();
//System.out.println("UCode:"+ucode);
if(rec.getStatus()==DBO.Status.NEW){
this.itemsInserted+=ucode;
if(ucode>0 && !generated.isEmpty()){
try (ResultSet keys = stmt.getGeneratedKeys()) {
if(keys.next()){
for(int i=0;i<generated.size();i++){
Field f=generated.get(i);
Object autoval=keys.getObject(i+1);
f.set(rec,autoval);
}
}
}
}
}else
if(rec.getStatus()==DBO.Status.USED){
this.itemsUpdated+=ucode;
}
return ucode>0;
}
}
@@ -1,16 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.io.Closeable;
import java.util.Iterator;
/** Iterator interface suitable for dbo resultsets.
*
*/
public interface SiphonIterator<T> extends Iterator<T>, Closeable {
}
@@ -1,63 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.io.IOException;
/** Endpoint for dbo objects.
* this interface will implemet CRUD plus control over a databse or folder.
* control will be implemented via meta terminal which will return specialized terminals for each entity and running actions on it
* will modify the entity structure.
*
* the core of the temrminal will be the Action object. The others will just be wrappers for item actions.
* the action will be a read or write query with session management.
*/
public interface Terminal {
public Action execute(Action q) throws IOException;
public default Action begin(){
return begin(null);
}
public default Action begin(String sig){
return new Action(this);
}
public default void end(Action act){
act.clear();
}
public default Terminal meta(Entity ent){
return null;
}
public default <T extends DBO> T load(Class<T> cls,Object...id) throws IOException {
Entity ent=Entity.recall(cls);
String sig="/"+ent.getName()+"/load";
try(Action act=begin(sig).load(ent).limit(1).if_pk(id).execute()){
return cls.cast(act.first());
}
}
public default DBO load(Entity ent,Object...id) throws IOException {
String sig="/"+ent.getName()+"/load";
try(Action act=begin(sig).load(ent).limit(1).if_pk(id).execute()){
return act.first();
}
}
public default boolean save(DBO rec) throws IOException{
Entity ent=rec.getType();
String sig="/"+ent.getName()+"/save";
try(Action act=begin(sig).save(ent).setItems(rec).execute()){
return act.isDone();
}
}
public default boolean delete(DBO rec) throws IOException {
if(rec==null) return false;
Entity ent=rec.getType();
String sig="/"+ent.getName()+"/delete";
try(Action act=begin(sig).delete(ent).setItems(rec).execute()){
return act.isDone();
}
}
}
+57
View File
@@ -0,0 +1,57 @@
package com.reliancy.io;
/**
* Small protocol-oriented helpers used by Jabba I/O codecs.
*
* This intentionally avoids depending on the broader bstore/jabba utility stack.
*/
public final class Handy {
private Handy() {
}
public static boolean isEmpty(CharSequence seq) {
if (seq == null) {
return true;
}
for (int i = 0; i < seq.length(); i++) {
if (!Character.isWhitespace(seq.charAt(i))) {
return false;
}
}
return true;
}
public static boolean isNumeric(String value) {
if (value == null || value.isEmpty()) {
return false;
}
int start = value.charAt(0) == '-' || value.charAt(0) == '+' ? 1 : 0;
if (start >= value.length()) {
return false;
}
boolean sawDigit = false;
boolean sawDot = false;
boolean sawExponent = false;
for (int i = start; i < value.length(); i++) {
char ch = value.charAt(i);
if (Character.isDigit(ch)) {
sawDigit = true;
continue;
}
if (ch == '.' && !sawDot && !sawExponent) {
sawDot = true;
continue;
}
if ((ch == 'e' || ch == 'E') && !sawExponent && sawDigit && i + 1 < value.length()) {
sawExponent = true;
sawDigit = false;
if (value.charAt(i + 1) == '+' || value.charAt(i + 1) == '-') {
i++;
}
continue;
}
return false;
}
return sawDigit;
}
}
@@ -0,0 +1,266 @@
package com.reliancy.io;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Lightweight JSON decoder that produces common Java object trees.
*/
public final class JsonDecoder {
private final CharSequence input;
private int index;
private JsonDecoder(CharSequence input) {
this.input = input == null ? "" : input;
}
public static Object decode(CharSequence input) {
JsonDecoder parser = new JsonDecoder(input);
Object value = parser.readValue();
parser.skipWhitespace();
if (parser.hasMore()) {
throw parser.error("Unexpected trailing content");
}
return value;
}
public static Map<String, Object> decodeObject(CharSequence input) {
Object value = decode(input);
if (value instanceof Map<?, ?> map) {
@SuppressWarnings("unchecked")
Map<String, Object> typed = (Map<String, Object>) map;
return typed;
}
throw new IllegalArgumentException("JSON payload is not an object");
}
public static List<Object> decodeArray(CharSequence input) {
Object value = decode(input);
if (value instanceof List<?> list) {
@SuppressWarnings("unchecked")
List<Object> typed = (List<Object>) list;
return typed;
}
throw new IllegalArgumentException("JSON payload is not an array");
}
private Object readValue() {
skipWhitespace();
if (!hasMore()) {
throw error("Unexpected end of input");
}
char ch = current();
switch (ch) {
case '{':
return readObject();
case '[':
return readArray();
case '"':
return readString();
case 't':
expectLiteral("true");
return Boolean.TRUE;
case 'f':
expectLiteral("false");
return Boolean.FALSE;
case 'n':
expectLiteral("null");
return null;
default:
if (ch == '-' || Character.isDigit(ch)) {
return readNumber();
}
throw error("Unexpected token");
}
}
private Map<String, Object> readObject() {
expect('{');
LinkedHashMap<String, Object> result = new LinkedHashMap<>();
skipWhitespace();
if (peek('}')) {
index++;
return result;
}
while (true) {
skipWhitespace();
String key = readString();
skipWhitespace();
expect(':');
Object value = readValue();
result.put(key, value);
skipWhitespace();
if (peek('}')) {
index++;
return result;
}
expect(',');
}
}
private List<Object> readArray() {
expect('[');
ArrayList<Object> result = new ArrayList<>();
skipWhitespace();
if (peek(']')) {
index++;
return result;
}
while (true) {
result.add(readValue());
skipWhitespace();
if (peek(']')) {
index++;
return result;
}
expect(',');
}
}
private String readString() {
expect('"');
StringBuilder buf = new StringBuilder();
while (hasMore()) {
char ch = input.charAt(index++);
if (ch == '"') {
return buf.toString();
}
if (ch != '\\') {
buf.append(ch);
continue;
}
if (!hasMore()) {
throw error("Unterminated escape sequence");
}
char esc = input.charAt(index++);
switch (esc) {
case '"':
case '\\':
case '/':
buf.append(esc);
break;
case 'b':
buf.append('\b');
break;
case 'f':
buf.append('\f');
break;
case 'n':
buf.append('\n');
break;
case 'r':
buf.append('\r');
break;
case 't':
buf.append('\t');
break;
case 'u':
buf.append(readUnicodeEscape());
break;
default:
throw error("Unsupported escape sequence");
}
}
throw error("Unterminated string");
}
private Number readNumber() {
int start = index;
if (peek('-')) {
index++;
}
readDigits();
boolean fractional = false;
if (peek('.')) {
fractional = true;
index++;
readDigits();
}
if (peek('e') || peek('E')) {
fractional = true;
index++;
if (peek('+') || peek('-')) {
index++;
}
readDigits();
}
String token = input.subSequence(start, index).toString();
if (!Handy.isNumeric(token)) {
throw error("Invalid number");
}
if (fractional) {
return Double.valueOf(token);
}
try {
return Integer.valueOf(token);
} catch (NumberFormatException ignore) {
return Long.valueOf(token);
}
}
private char readUnicodeEscape() {
if (index + 4 > input.length()) {
throw error("Invalid unicode escape");
}
int value = 0;
for (int i = 0; i < 4; i++) {
char ch = input.charAt(index++);
int digit = Character.digit(ch, 16);
if (digit < 0) {
throw error("Invalid unicode escape");
}
value = (value << 4) + digit;
}
return (char) value;
}
private void readDigits() {
int start = index;
while (hasMore() && Character.isDigit(current())) {
index++;
}
if (start == index) {
throw error("Expected digit");
}
}
private void expectLiteral(String literal) {
for (int i = 0; i < literal.length(); i++) {
if (!hasMore() || input.charAt(index++) != literal.charAt(i)) {
throw error("Expected literal " + literal);
}
}
}
private void expect(char expected) {
skipWhitespace();
if (!peek(expected)) {
throw error("Expected '" + expected + "'");
}
index++;
}
private void skipWhitespace() {
while (hasMore() && Character.isWhitespace(current())) {
index++;
}
}
private boolean hasMore() {
return index < input.length();
}
private boolean peek(char ch) {
return hasMore() && current() == ch;
}
private char current() {
return input.charAt(index);
}
private IllegalArgumentException error(String message) {
return new IllegalArgumentException(message + " at offset " + index);
}
}
@@ -0,0 +1,180 @@
package com.reliancy.io;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.Iterator;
import java.util.Map;
/**
* Lightweight JSON encoder for common Java object trees used in web payloads.
*/
public final class JsonEncoder {
private JsonEncoder() {
}
public static int encode(Object value, Appendable out) throws IOException {
if (value == null) {
if (out != null) {
out.append("null");
}
return 4;
}
if (value instanceof String || value instanceof CharSequence || value instanceof Character) {
return writeQuoted(String.valueOf(value), out);
}
if (value instanceof Number || value instanceof Boolean) {
String text = String.valueOf(value);
if (out != null) {
out.append(text);
}
return text.length();
}
if (value instanceof Map<?, ?> map) {
return encodeMap(map, out);
}
if (value instanceof Iterable<?> iterable) {
return encodeIterable(iterable.iterator(), out);
}
if (value instanceof Iterator<?> iterator) {
return encodeIterable(iterator, out);
}
Class<?> type = value.getClass();
if (type.isArray()) {
return encodeArray(value, out);
}
return writeQuoted(String.valueOf(value), out);
}
public static int encodeMap(Map<?, ?> map, Appendable out) throws IOException {
int len = 2;
if (out != null) {
out.append('{');
}
int index = 0;
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (index++ > 0) {
len += 1;
if (out != null) {
out.append(',');
}
}
len += writeQuoted(String.valueOf(entry.getKey()), out);
len += 1;
if (out != null) {
out.append(':');
}
len += encode(entry.getValue(), out);
}
if (out != null) {
out.append('}');
}
return len;
}
public static int encodeArray(Object array, Appendable out) throws IOException {
int len = 2;
if (out != null) {
out.append('[');
}
int size = Array.getLength(array);
for (int i = 0; i < size; i++) {
if (i > 0) {
len += 1;
if (out != null) {
out.append(',');
}
}
len += encode(Array.get(array, i), out);
}
if (out != null) {
out.append(']');
}
return len;
}
public static int encodeIterable(Iterator<?> iterator, Appendable out) throws IOException {
int len = 2;
if (out != null) {
out.append('[');
}
int index = 0;
while (iterator.hasNext()) {
if (index++ > 0) {
len += 1;
if (out != null) {
out.append(',');
}
}
len += encode(iterator.next(), out);
}
if (out != null) {
out.append(']');
}
return len;
}
public static boolean needsEscaping(CharSequence text) {
if (text == null) {
return false;
}
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
if (ch == '"' || ch == '\\' || ch == '/' || ch < 0x20) {
return true;
}
}
return false;
}
public static CharSequence escape(CharSequence text) {
if (text == null || text.length() == 0 || !needsEscaping(text)) {
return text == null ? "" : text;
}
StringBuilder buf = new StringBuilder(text.length() + 8);
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
switch (ch) {
case '"':
buf.append("\\\"");
break;
case '\\':
buf.append("\\\\");
break;
case '/':
buf.append("\\/");
break;
case '\b':
buf.append("\\b");
break;
case '\f':
buf.append("\\f");
break;
case '\n':
buf.append("\\n");
break;
case '\r':
buf.append("\\r");
break;
case '\t':
buf.append("\\t");
break;
default:
if (ch < 0x20) {
buf.append(String.format("\\u%04x", (int) ch));
} else {
buf.append(ch);
}
break;
}
}
return buf;
}
private static int writeQuoted(String text, Appendable out) throws IOException {
CharSequence escaped = escape(text);
if (out != null) {
out.append('"').append(escaped).append('"');
}
return escaped.length() + 2;
}
}
+9 -77
View File
@@ -11,11 +11,10 @@ package com.reliancy.jabba;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import com.reliancy.dbo.Terminal;
import com.reliancy.jabba.sec.SecurityPolicy;
import com.reliancy.jabba.ui.Rendering;
import com.reliancy.jabba.ui.Template;
import com.reliancy.util.CodeException;
import com.reliancy.util.ResultCode;
@@ -50,12 +49,6 @@ public abstract class App extends Processor{
public App(String id) {
super(id);
}
/** does nothing. */
public void before(Request request,Response response) throws IOException{
}
/** does nothing. */
public void after(Request request,Response response) throws IOException{
}
/** app serves by processing first-last chain then router.
* always conditional on status being null otherwise it skips.
*/
@@ -63,70 +56,6 @@ public abstract class App extends Processor{
if(first!=null && resp.getStatus()==null) first.process(req, resp);
if(router!=null && resp.getStatus()==null) router.process(req,resp);
}
/** When an error occurs we need properly render exception.
* if html is accepted we try to render a valid response with n error within a template so it fits with the app.
* for all others we set error status code.
* for json,xml and plain we render into a message template for the rest we do nothing.
* this method returns true if a response was generated. in overloaded methods
* if false is returned we can generate response the status is set to 500 already.
* @param req incoming request
* @param ex exception state
* @param resp response to generate
* @return true if handled else it signifies we should do somthing in overloads.
* @throws IOException
*/
public boolean processError(com.reliancy.jabba.Request req,Throwable ex,com.reliancy.jabba.Response resp) throws IOException{
log().error("error:",ex);
String accepted_format=req.getHeader("Accept");
boolean present=accepted_format!=null;
if(present && (
accepted_format.contains("/html")
|| accepted_format.contains("/xhtml")
)){
// we have html request
resp.setContentType(HTTP.MIME_HTML);
Template t=Template.find("/templates/error.hbs");
if(t==null){ // no template found
resp.setStatus(Response.HTTP_INTERNAL_ERROR);
if(ex instanceof IOException) throw ((IOException)ex);
else throw new RuntimeException(ex);
}
Rendering.begin(t).with(ex).end(resp);
return true;
}else{
// for all other cases we first flag it as error
resp.setStatus(Response.HTTP_INTERNAL_ERROR);
}
// next we format a few common and supported messages
if(present && accepted_format.contains("/json")){
ResponseEncoder enc=resp.getEncoder();
if(enc.getErrorFormat()==null){
String template="'{'\n\t\"status\":\"error\",\n\t\"title\":\"{0}\",\n\t\"message\":\"{1}\"\n'}'\n";
enc.setErrorFormat(template);
}
enc.writeError(ex);
return true;
}
if(present && accepted_format.contains("/xml")){
ResponseEncoder enc=resp.getEncoder();
if(enc.getErrorFormat()==null){
String template="<response>\n\t<status>error</status>\n\t<title>{0}</title>\n\t<message>{1}</message>\n</response>\n";
enc.setErrorFormat(template);
}
enc.writeError(ex);
return true;
}
if(present && accepted_format.contains("text/plain")){
ResponseEncoder enc=resp.getEncoder();
if(enc.getErrorFormat()==null){
String template="status=error\n\ntitle={0}\n\nmessage={1}\n\n";
enc.setErrorFormat(template);
}
enc.writeError(ex);
return true;
}
return false;
}
/** add one or a chain of processors. */
public <T extends Processor> T addMiddleWare(T m){
@@ -218,12 +147,15 @@ public abstract class App extends Processor{
for(Processor p=first;p!=null;p=p.getNext()){
p.end();
}
log().info("stopped:"+getId());
super.end(); // detaches from config
}finally{
// we notify all of end (especially cleaner thread)
synchronized(this){
this.notifyAll();
try{
// detaches from config
super.end();
}finally{
// we notify all of end (especially cleaner thread)
synchronized(this){
this.notifyAll();
}
}
}
}
@@ -9,6 +9,7 @@ package com.reliancy.jabba;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/** AppSession middleware will inject an appsession object into callsession.
* During each request,response we will if not alrady present extract a cookie or param
@@ -29,7 +30,7 @@ public class AppSessionFilter extends Processor{
factory=f;
}
@Override
public void before(Request request, Response response) throws IOException {
public void beforeServe(Request request, Response response) throws IOException {
String ssid=(String)request.getParam(KEY_NAME,null);
if(ssid==null){
UUID uuid = UUID.randomUUID();
@@ -54,13 +55,16 @@ public class AppSessionFilter extends Processor{
css.setAppSession(ss);
}
@Override
public void after(Request request, Response response) throws IOException {
CallSession css=CallSession.getInstance();
AppSession ss=(AppSession) css.getAppSession();
response.setCookie(KEY_NAME,ss.id,15*60,false);
}
@Override
public void serve(Request request, Response response) throws IOException{
public void afterServe(Request request, Response response) throws IOException {
CallSession css=CallSession.getInstance();
AppSession ss=(AppSession) css.getAppSession();
if(ss==null){
return;
}
// Determine if request is HTTPS
boolean isSecure="https".equalsIgnoreCase(request.getProtocol()) ||
"https".equalsIgnoreCase(request.getScheme());
// Set secure=true for HTTPS, HttpOnly=true always for security
response.setCookie(KEY_NAME,ss.id,15*60,isSecure,true);
}
}
@@ -130,9 +130,10 @@ public class ArgsConfig extends Config.Base{
APP_SETTINGS.set(this, cwd);
}
// also logging level and format
// System.out.println("LogLog:"+LOG_LEVEL.get(this));
// System.out.println("ENV:"+System.getenv("LOG_LEVEL"));
// LOG_LEVEL.set(this,"INFO");
// Set default log level to INFO if not specified
//if(LOG_LEVEL.get(this) == null) {
// LOG_LEVEL.set(this,"INFO");
//}
Logger root=Log.setup();
Log.setLevel(root,LOG_LEVEL.get(this));
return this;
@@ -8,31 +8,118 @@ You may not use this file except in compliance with the License.
package com.reliancy.jabba;
import java.util.ArrayList;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.Executor;
/**
* Thread local object that lets us access some variables in specialized handler methods.
* For example request and response objects are accessible.
* The session is updated at process phase of each processor.
* The session is updated at process phase of each processor.
*
* <h2>Instance Counting and Multi-Threading</h2>
* CallSession tracks how many threads are currently using it via an atomic counter.
* This enables safe async processing where a single session is shared across multiple threads:
*
* <ul>
* <li><b>beginFresh()</b> - Initialize session at the top of request processing (main thread)</li>
* <li><b>beginAgain()</b> - Reattach session when switching threads (async workers)</li>
* <li><b>end()</b> - Detach from current thread, decrement counter. Only cleans up when count reaches zero.</li>
* </ul>
*
* <p><b>Async Flow Example:</b></p>
* <pre>
* // Main thread:
* session.beginFresh(appSession, request, response); // count = 1
* // ... processing ...
*
* // Fork to async thread:
* CompletableFuture.supplyAsync(() -> {
* session.beginAgain(); // count = 2
* // ... async work ...
* session.end(); // count = 1, session still alive
* });
*
* // Main thread completes:
* session.end(); // count = 0, session cleanup happens
* </pre>
*
* <p>This ensures the session and its resources remain valid until ALL threads complete.</p>
*/
public class CallSession implements Session{
ArrayList<Processor> callers=new ArrayList<>();
Session appSession;
Request request;
Response response;
Executor executor;
/** Atomic counter tracking how many threads are currently using this session */
transient AtomicInteger instanceCount=new AtomicInteger(0);
public CallSession(){
}
protected void end(){
appSession=null;
request=null;
response=null;
callers.clear();
/** End the current session.
* If the session is not the current one, do nothing.
* If the session is the current one, remove it from the thread local.
* If the session is the current one and there are no more instances, clear the session.
* If the session is the current one and there are more instances, decrement the instance count.
*/
public synchronized boolean end(){
CallSession current=instance.get();
if(current!=this) return false; // not the current session
instance.remove(); // remove from this thread
int count=instanceCount.updateAndGet(i -> i>0 ? i-1 : 0);
if(count==0){
// if no more instances, clear the session
try{
while(callers.size()>0){
Processor last=callers.remove(callers.size()-1);
if(last!=null && last.isActive()){
try{
last.afterServe(request, response); // call after to ensure proper cleanup
}catch(Exception e){
// Log but don't throw - we're in cleanup
last.log().error("Error calling after() on processor " + last.getId() + ": " + e.getMessage());
}
}
};
}finally{
appSession=null;
request=null;
response=null;
executor=null;
callers.clear();
}
return true;
}
return false;
}
protected void begin(Session ss,Request req,Response resp){
/** Begins session at the top of the call stack.
* If the session is already in use, throw an exception.
* If the session is not in use, set the session to the new one.
* @param ss
* @param req
* @param resp
* @return true if the session was successfully begun, false otherwise
*/
public synchronized boolean beginFresh(Session ss,Request req,Response resp){
appSession=ss;
request=req;
response=resp;
executor=null;
callers.clear();
return beginAgain();
}
/** Begins session again in a different thread..
* @return true if the session was successfully begun, false otherwise
*/
public synchronized boolean beginAgain(){
CallSession current=instance.get();
if(this==current) return true;
if(current!=null) current.end(); // end previous one if any
instance.set(this); // add to this thread
instanceCount.incrementAndGet(); // increment count
return true;
}
protected void enter(Processor c){callers.add(c);}
protected void leave(Processor c){
@@ -45,6 +132,14 @@ public class CallSession implements Session{
// bad last is not same c, some processors have not left properly
do{
last=callers.remove(callers.size()-1);
if(last!=null && last.isActive()){
try{
last.afterServe(request, response); // call after to ensure proper cleanup
}catch(Exception e){
// Log but don't throw - we're in cleanup
last.log().error("Error calling after() on processor " + last.getId() + ": " + e.getMessage());
}
}
}while(last!=c);
}
}
@@ -74,6 +169,12 @@ public class CallSession implements Session{
public void setResponse(Response response) {
this.response = response;
}
public Executor getExecutor() {
return executor;
}
public void setExecutor(Executor executor) {
this.executor = executor;
}
public Processor getCaller() {
int len=callers.size();
return len>0?callers.get(len-1):null;
@@ -88,4 +189,18 @@ public class CallSession implements Session{
if(ret==null) instance.set(ret=new CallSession());
return ret;
}
/** Set the current call session.
* If the session is the same as the current one, do nothing.
* If the session is null, end the current one if any.
* If the session is new, end the current one if any and set the new one.
*/
// public static void setInstance(CallSession ss){
// CallSession current=instance.get();
// if(ss==current) return;
// if(current!=null) current.end(); // end previous one if any
// if(ss!=null){
// instance.set(ss); // add to this thread
// ss.instanceCount.incrementAndGet(); // increment count
// }
// }
}
@@ -150,6 +150,8 @@ public interface Config extends Iterable<Config.Property<?>>{
public static final Property<String> APP_SETTINGS=new Property<>("APP_SETTINGS",String.class);
public static final Property<String> APP_CLASS=new Property<>("APP_CLASS",String.class);
public static final Property<List> APP_ARGS=new Property<>("APP_ARGS",List.class);
public static final Property<String> SECRET_KEY=new Property<>("SECRET_KEY",String.class);
public static final Property<Integer> SERVER_PORT=new Property<>("SERVER_PORT",Integer.class).setInitial(8090);
public default Config getParent(){return null;};
public Config clear();
@@ -8,6 +8,7 @@ You may not use this file except in compliance with the License.
package com.reliancy.jabba;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
/** EndPoint is a special processor usually the last in chain.
*
@@ -17,12 +18,5 @@ public abstract class EndPoint extends Processor{
public EndPoint(String id) {
super(id);
}
@Override
public void before(Request request, Response response) throws IOException {
}
@Override
public void after(Request request, Response response) throws IOException {
}
public abstract void serve(Request request, Response response) throws IOException;
public abstract void serve(Request request, Response response) throws IOException;
}
@@ -90,8 +90,45 @@ public class FileConfig extends Config.Base{
if(changing) p.setString(this,sval);
}
}while(changing && iterations<7);
// Validate configuration
validate();
return this;
}
/**
* Validates configuration values after loading.
* @throws IllegalArgumentException if validation fails
*/
protected void validate() throws IllegalArgumentException{
// Validate SERVER_PORT if present
if(hasProperty(Config.SERVER_PORT)){
Integer port=getProperty(Config.SERVER_PORT,null);
if(port!=null && (port<1 || port>65535)){
throw new IllegalArgumentException("SERVER_PORT must be between 1 and 65535, got: "+port);
}
}
// Validate LOG_LEVEL if present
if(hasProperty(Config.LOG_LEVEL)){
String level=getProperty(Config.LOG_LEVEL,"");
if(!level.isEmpty() && !isValidLogLevel(level)){
throw new IllegalArgumentException("Invalid LOG_LEVEL: "+level+". Must be one of: TRACE, DEBUG, INFO, WARN, ERROR");
}
}
// Validate required properties from schema
for(Property<?> p:getSchema()){
if(p.isRequired() && !hasProperty(p)){
throw new IllegalArgumentException("Required property '"+p.getName()+"' is missing");
}
}
}
/**
* Checks if log level is valid.
*/
protected boolean isValidLogLevel(String level){
if(level==null) return false;
String upper=level.toUpperCase();
return "TRACE".equals(upper) || "DEBUG".equals(upper) ||
"INFO".equals(upper) || "WARN".equals(upper) || "ERROR".equals(upper);
}
@Override
public Config save() throws IOException{
return this;
+303 -121
View File
@@ -5,19 +5,20 @@ Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.jabba;
import com.reliancy.util.Handy;
import com.reliancy.util.LRUCache;
import com.reliancy.util.Resources;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Iterator;
import org.slf4j.Logger;
package com.reliancy.jabba;
import com.reliancy.util.Handy;
import com.reliancy.util.LRUCache;
import com.reliancy.util.Resources;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Iterator;
import org.slf4j.Logger;
/** FileServer is an module and endpoint that exposes multiple URLs thru which files are served.
* First it will be just get(tting), later
@@ -28,41 +29,73 @@ import org.slf4j.Logger;
* Please note Router is for routing.
* Bucket is there to process input/output given verbs over resources under it.
*/
public class FileServer extends EndPoint implements AppModule,Resources.PathRewrite{
/** Bucket interface to abstract i/o and provide easier extensibility.
* asContainer matches path and then returns local-to-packet path.
* signature returns a hash over lastModified or content that reflects modification.
*/
public static interface Bucket{
String getPrefix();
String asContained(String path);
boolean equals(String pref);
InputStream openSource(String local_path,FileServer user) throws IOException;
OutputStream openSink(String local_path,FileServer user) throws IOException;
String signature(String local_path);
}
public static class FileBucket implements Bucket{
final String prefix;
String[] extAllowed;
Object[] domain;
LRUCache<String,Long> hit_history=new LRUCache<>(2*Runtime.getRuntime().availableProcessors());
public FileBucket(String prefix){
this.prefix=prefix;
extAllowed=new String[]{};
domain=new Object[]{};
public class FileServer extends EndPoint implements AppModule,Resources.PathRewrite{
public static class CachedAsset{
final URL source;
final long lastModified;
final long contentLength;
final byte[] content;
final String signature;
public CachedAsset(URL source,long lastModified,long contentLength,byte[] content){
this.source=source;
this.lastModified=lastModified;
this.contentLength=contentLength;
this.content=content;
if(lastModified>0 || contentLength>=0){
signature=Handy.hashMD5(lastModified+":"+contentLength);
}else if(content!=null){
signature=Handy.hashMD5(new String(content,java.nio.charset.StandardCharsets.ISO_8859_1));
}else{
signature=null;
}
}
public InputStream openSource() throws IOException{
if(content!=null) return new ByteArrayInputStream(content);
if(source==null) return null;
return source.openStream();
}
public boolean hasContent(){
return content!=null;
}
}
/** Bucket interface to abstract i/o and provide easier extensibility.
* asContainer matches path and then returns local-to-packet path.
* signature returns a hash over lastModified or content that reflects modification.
*/
public static interface Bucket{
String getPrefix();
String asContained(String path);
boolean equals(String pref);
InputStream openSource(String local_path,FileServer user) throws IOException;
OutputStream openSink(String local_path,FileServer user) throws IOException;
String signature(String local_path);
Long lastModified(String local_path);
Long contentLength(String local_path);
CachedAsset getAsset(String local_path,FileServer user) throws IOException;
}
public static class FileBucket implements Bucket{
final String prefix;
String[] extAllowed;
Object[] domain;
LRUCache<String,CachedAsset> hit_history=new LRUCache<>(64);
public FileBucket(String prefix){
this.prefix=prefix;
extAllowed=new String[]{};
domain=new Object[]{};
}
@Override
public final String getPrefix(){return prefix;}
@Override
public String asContained(String path) {
if(!path.startsWith(prefix)) return null; // not contained
String local_path=path.replace(prefix,"");
if(extAllowed.length==0) return local_path;
for(String ext:extAllowed){
if(path.endsWith(ext)){
return local_path;
}
@Override
public String asContained(String path) {
if(!path.startsWith(prefix)) return null; // not contained
String local_path=path.substring(prefix.length());
if(extAllowed.length==0) return local_path;
for(String ext:extAllowed){
if(path.endsWith(ext)){
return local_path;
}
}
return null;
}
@@ -78,35 +111,71 @@ public class FileServer extends EndPoint implements AppModule,Resources.PathRewr
domain=sp;
return this;
}
public Object[] getDomain(){
return (domain!=null && domain.length>0)?domain:Resources.search_path;
}
public InputStream openSource(String local_path,FileServer user) throws IOException{
Object[] sp=getDomain();
URL f=Resources.findFirst(user,local_path, sp);
if(f==null) return null; // skip if rpath not located
URLConnection conn=f.openConnection();
hit_history.put(local_path,conn.getLastModified()); // pull last modified for signature
return conn.getInputStream();
}
public OutputStream openSink(String local_path,FileServer user) throws IOException{
return null;
}
public String signature(String local_path){
Long last_modified=hit_history.get(local_path);
if(last_modified==null) return null;
String sig=String.valueOf(last_modified);
return Handy.hashMD5(sig);
}
}
final ArrayList<Bucket> buckets=new ArrayList<>();
String diskPrefix; // will be prefixed to source if file
String classPrefix; // will be prefixed to source if class
String urlPrefix; // will be prefixed to source if URL
public FileServer(String url_path,String offset,Object ... source){
super(null);
diskPrefix=classPrefix=offset;
addBucket(new FileBucket(url_path).setDomain(source));
public Object[] getDomain(){
return (domain!=null && domain.length>0)?domain:Resources.search_path;
}
@Override
public CachedAsset getAsset(String local_path,FileServer user) throws IOException{
Object[] sp=getDomain();
URL f=Resources.findFirst(user,local_path, sp);
if(f==null) return null;
URLConnection conn=f.openConnection();
long lastModified=conn.getLastModified();
long contentLength=conn.getContentLengthLong();
CachedAsset cached=hit_history.get(local_path);
if(cached!=null
&& cached.lastModified==lastModified
&& cached.contentLength==contentLength){
return cached;
}
byte[] content=null;
if(user.shouldCacheContent(local_path,contentLength)){
try(InputStream is=conn.getInputStream()){
content=is.readAllBytes();
contentLength=content.length;
}
}
CachedAsset asset=new CachedAsset(f,lastModified,contentLength,content);
hit_history.remove(local_path);
hit_history.put(local_path,asset);
return asset;
}
public InputStream openSource(String local_path,FileServer user) throws IOException{
CachedAsset asset=getAsset(local_path,user);
return asset!=null?asset.openSource():null;
}
public OutputStream openSink(String local_path,FileServer user) throws IOException{
return null;
}
public String signature(String local_path){
CachedAsset asset=hit_history.get(local_path);
return asset!=null?asset.signature:null;
}
@Override
public Long lastModified(String local_path){
CachedAsset asset=hit_history.get(local_path);
return asset!=null && asset.lastModified>0?asset.lastModified:null;
}
@Override
public Long contentLength(String local_path){
CachedAsset asset=hit_history.get(local_path);
return asset!=null && asset.contentLength>=0?asset.contentLength:null;
}
}
final ArrayList<Bucket> buckets=new ArrayList<>();
String diskPrefix; // will be prefixed to source if file
String classPrefix; // will be prefixed to source if class
String urlPrefix; // will be prefixed to source if URL
String indexFile;
String fallbackFile;
String assetCacheControl="public, max-age=3600";
String indexCacheControl="no-cache";
String fallbackCacheControl="no-cache";
long memoryCacheContentLimit=256*1024;
public FileServer(String url_path,String offset,Object ... source){
super(null);
diskPrefix=classPrefix=offset;
addBucket(new FileBucket(url_path).setDomain(source));
}
public FileServer(){
super(null);
@@ -119,10 +188,34 @@ public class FileServer extends EndPoint implements AppModule,Resources.PathRewr
classPrefix=prefix;
return this;
}
public FileServer setURLOffset(String offset){
urlPrefix=offset;
return this;
}
public FileServer setURLOffset(String offset){
urlPrefix=offset;
return this;
}
public FileServer setIndexFile(String path){
indexFile=path;
return this;
}
public FileServer setFallbackFile(String path){
fallbackFile=path;
return this;
}
public FileServer setAssetCacheControl(String value){
assetCacheControl=value;
return this;
}
public FileServer setIndexCacheControl(String value){
indexCacheControl=value;
return this;
}
public FileServer setFallbackCacheControl(String value){
fallbackCacheControl=value;
return this;
}
public FileServer setMemoryCacheContentLimit(long value){
memoryCacheContentLimit=value;
return this;
}
/**
* we prefix our path for disk and class contexts.
*/
@@ -135,41 +228,35 @@ public class FileServer extends EndPoint implements AppModule,Resources.PathRewr
return path;
}
@Override
public void serve(Request request, Response response) throws IOException {
String verb=request.getVerb();
String path=request.getPath();
Logger logger=log();
boolean atDebug=logger.isDebugEnabled();
if(atDebug) logger.debug("{}:{}",verb,path);
if(HTTP.VERB_GET.equals(verb)){
for(Bucket bucket:buckets){
String local_path=bucket.asContained(path);
if(local_path==null) continue; // this bucket is not accepting
try(InputStream ins=bucket.openSource(local_path,this)){
if(ins==null) continue; // url did not take
String etag=bucket.signature(local_path);
if(etag!=null){
response.setHeader("Cache-Control","max-age=0, must-revalidate");
response.setHeader("ETag",etag);
String etag_old=request.getHeader("If-None-Match");
if(etag.equals(etag_old)){
// we got same etag no change
response.setStatus(Response.HTTP_NOT_MODIFIED);
return;
}
}
if(atDebug) logger.debug("\tfound:"+local_path);
String ctype=HTTP.ext2mime(local_path);
response.setStatus(Response.HTTP_OK);
response.setContentType(ctype);
ResponseEncoder enc=response.getEncoder();
enc.writeStream(ins);
return; // we got something
}
}
}else{
// these verbs are not supported
}
public void serve(Request request, Response response) throws IOException {
String verb=request.getVerb();
String path=request.getPath();
Logger logger=log();
boolean atDebug=logger.isDebugEnabled();
if(atDebug) logger.debug("{}:{}",verb,path);
if(HTTP.VERB_GET.equals(verb) || HTTP.VERB_HEAD.equals(verb)){
for(Bucket bucket:buckets){
String local_path=bucket.asContained(path);
if(local_path==null) continue; // this bucket is not accepting
if(isUnsafePath(local_path)){
response.setStatus(Response.HTTP_BAD_REQUEST);
response.getEncoder().writeln("invalid file path:"+path);
logger.warn("rejected unsafe path:{}",path);
return;
}
String resolved_path=resolveLocalPath(local_path);
if(tryServe(bucket,resolved_path,request,response,atDebug,false)){
return;
}
if(shouldServeFallback(local_path,request) && fallbackFile!=null){
if(tryServe(bucket,fallbackFile,request,response,atDebug,true)){
return;
}
}
}
}else{
// these verbs are not supported
}
response.setStatus(Response.HTTP_NOT_FOUND);
response.getEncoder().writeln("missing file:"+path);
logger.error("not found:{}",path);
@@ -213,11 +300,106 @@ public class FileServer extends EndPoint implements AppModule,Resources.PathRewr
for(Bucket b:buckets) if(b.equals(url_path)) return b;
return null;
}
public Iterator<Bucket> enumBuckets(){
return buckets.iterator();
}
public void publish(App app) {
Router rep=app.getRouter();
for(Bucket b:buckets) rep.addRoute("GET",b.getPrefix()+".*",this);
}
}
public Iterator<Bucket> enumBuckets(){
return buckets.iterator();
}
public void publish(App app) {
Router rep=app.getRouter();
for(Bucket b:buckets){
rep.addRoute("GET",b.getPrefix()+".*",this);
rep.addRoute("HEAD",b.getPrefix()+".*",this);
}
}
protected String resolveLocalPath(String local_path){
String normalized=normalizeLocalPath(local_path);
if((normalized==null || normalized.isEmpty()) && indexFile!=null){
return indexFile;
}
return normalized;
}
protected String normalizeLocalPath(String local_path){
if(local_path==null) return null;
local_path=local_path.replace('\\','/');
while(local_path.startsWith("/")) local_path=local_path.substring(1);
if(local_path.equals(".")) return "";
return local_path;
}
protected boolean shouldServeFallback(String local_path,Request request){
String normalized=normalizeLocalPath(local_path);
if(normalized==null) return false;
if(!acceptsHtml(request)) return false;
if(normalized.isEmpty()) return fallbackFile!=null;
int lastSlash=normalized.lastIndexOf('/');
int lastDot=normalized.lastIndexOf('.');
return lastDot < lastSlash + 1;
}
protected boolean tryServe(Bucket bucket,String local_path,Request request,Response response,boolean atDebug,boolean isFallback) throws IOException{
if(local_path==null) return false;
CachedAsset asset=bucket.getAsset(local_path,this);
if(asset==null) return false;
try(InputStream ins=asset.openSource()){
if(ins==null) return false;
response.setHeader("X-Content-Type-Options","nosniff");
String etag=asset.signature;
if(etag!=null){
response.setHeader("Cache-Control",cacheControlFor(local_path,isFallback));
response.setHeader("ETag",etag);
String etag_old=request.getHeader("If-None-Match");
if(etag.equals(etag_old)){
response.setStatus(Response.HTTP_NOT_MODIFIED);
return true;
}
}else{
response.setHeader("Cache-Control",cacheControlFor(local_path,isFallback));
}
Long lastModified=asset.lastModified>0?asset.lastModified:null;
if(lastModified!=null && lastModified>0){
response.setHeader("Last-Modified",java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME.format(
java.time.Instant.ofEpochMilli(lastModified).atZone(java.time.ZoneOffset.UTC)
));
}
Long contentLength=asset.contentLength>=0?asset.contentLength:null;
if(contentLength!=null && contentLength>=0){
response.setHeader("Content-Length",String.valueOf(contentLength));
}
if(atDebug) log().debug("\tfound:{}",local_path);
String ctype=HTTP.ext2mime(local_path);
response.setStatus(Response.HTTP_OK);
response.setContentType(ctype!=null?ctype:HTTP.MIME_BYTES);
if(!HTTP.VERB_HEAD.equals(request.getVerb())){
if(asset.hasContent()){
response.getEncoder().writeBytes(asset.content,0,asset.content.length);
}else{
response.getEncoder().writeStream(ins);
}
}
return true;
}
}
protected boolean shouldCacheContent(String local_path,long contentLength){
if(contentLength<0) return false;
return contentLength<=memoryCacheContentLimit;
}
protected boolean acceptsHtml(Request request){
if(request==null) return true;
String accept=request.getHeader("Accept");
if(accept==null || accept.trim().isEmpty()) return true;
accept=accept.toLowerCase();
return accept.contains("text/html") || accept.contains("application/xhtml+xml");
}
protected boolean isUnsafePath(String local_path){
String normalized=normalizeLocalPath(local_path);
if(normalized==null) return false;
return normalized.equals("..") || normalized.startsWith("../") || normalized.contains("/../");
}
protected String cacheControlFor(String local_path,boolean isFallback){
String normalized=normalizeLocalPath(local_path);
if(normalized==null || normalized.isEmpty() || normalized.equals(indexFile)){
return indexCacheControl;
}
if(isFallback){
return fallbackCacheControl;
}
return assetCacheControl;
}
}
+6 -1
View File
@@ -25,6 +25,7 @@ public final class HTTP {
public static String MIME_JSON="application/json";
public static String MIME_BYTES="application/octet-stream";
public static String MIME_HTML="text/html";
public static String MIME_XML="application/xml";
public static HashMap<String,String> MIME_MAP=new HashMap<>();
public static class Header{
@@ -39,8 +40,12 @@ public final class HTTP {
public String value;
public int maxAge;
public boolean secure;
public boolean httpOnly;
public Cookie(String k,String v, int maxAge, boolean sec, boolean httpOnly){
key=k;value=v;this.maxAge=maxAge;secure=sec;this.httpOnly=httpOnly;
}
public Cookie(String k,String v, int maxAge, boolean sec){
key=k;value=v;this.maxAge=maxAge;secure=sec;
this(k,v,maxAge,sec,true);
}
}
/** maps extension to mime type.
@@ -11,24 +11,93 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.concurrent.CompletableFuture;
import com.reliancy.jabba.decor.Async;
import com.reliancy.jabba.decor.Routed;
import com.reliancy.jabba.decor.WebSocket;
import com.reliancy.util.Handy;
public class MethodEndPoint extends EndPoint{
enum InvokeProfile{
PLAIN, // no return, request, response as argument
NOARG, // no arguments, possible return
FULL, // one or more arguments need to do casting
// Inner Servant classes for each invoke type
private final Servant INVOKE_PLAIN = new Servant() {
@Override
public void serve(Request request, Response response) throws IOException {
try {
method.invoke(target, request, response);
} catch (Exception ex) {
if(ex instanceof IOException) throw ((IOException)ex);
else throw new IOException(ex);
}
}
};
private final Servant INVOKE_NOARG = new Servant() {
@Override
public void serve(Request request, Response response) throws IOException {
try {
Object ret = method.invoke(target);
encodeResponse(ret, response);
} catch (Exception ex) {
if(ex instanceof IOException) throw ((IOException)ex);
else throw new IOException(ex);
}
}
};
private final Servant INVOKE_FULL = new Servant() {
@Override
public void serve(Request request, Response response) throws IOException {
try {
Object[] argVals = decodeRequest(request);
Object ret = method.invoke(target, argVals);
encodeResponse(ret, response);
} catch (Exception ex) {
if(ex instanceof IOException) throw ((IOException)ex);
else throw new IOException(ex);
}
}
};
private final Servant INVOKE_WEBSOCKET = new Servant() {
@Override
public void serve(Request request, Response response) throws IOException {
try {
// 1. Get AppSession from CallSession (set by middleware during upgrade request)
CallSession cs = CallSession.getInstance();
Session appSession = cs != null ? cs.getAppSession() : null;
// 2. Get route path for this WebSocket endpoint
String routePath = route != null ? route.path() : request.getPath();
// 3. Upgrade HTTP response to WebSocket
// TODO: ServletResponse.upgradeToWebSocket() needs implementation
WebSocketSession wsSession = response.upgradeToWebSocket(routePath, appSession);
// 4. Invoke user method to setup callbacks
// User method signature: void methodName(WebSocketSession session)
method.invoke(target, wsSession);
// 5. Don't complete response - WebSocket connection stays open
// CallSession.end() will happen in finally block but WebSocketSession lives on
// TODO: Verify response handling - should we mark as async or handled differently?
} catch (Exception ex) {
if(ex instanceof IOException) throw ((IOException)ex);
else throw new IOException(ex);
}
}
};
}
Routed route;
Object target;
Method method;
Parameter[] params;
Class<?> retType;
InvokeProfile invokeType;
Servant invokeType;
ArrayList<MethodDecorator> decorators=new ArrayList<>();
public MethodEndPoint(Object target,Method m) {
super(target.getClass().getSimpleName()+"."+m.getName());
this.route=m.getAnnotation(Routed.class);
@@ -36,13 +105,28 @@ public class MethodEndPoint extends EndPoint{
this.method=m;
this.params=m.getParameters();
this.retType=m.getReturnType();
this.invokeType=InvokeProfile.FULL;
this.invokeType=INVOKE_FULL;
if(params.length==2 && params[0].getType()==Request.class && params[1].getType()==Response.class){
invokeType=InvokeProfile.PLAIN;
invokeType=INVOKE_PLAIN;
}
if(params.length==0){
invokeType=InvokeProfile.NOARG;
invokeType=INVOKE_NOARG;
}
// Check for WebSocket endpoint
if(m.isAnnotationPresent(WebSocket.class)) {
// WebSocket methods must have exactly one parameter of type WebSocketSession
if(params.length != 1 || params[0].getType() != WebSocketSession.class) {
throw new RuntimeException(
"@WebSocket method must have exactly one WebSocketSession parameter: " +
m.getName()
);
}
invokeType = INVOKE_WEBSOCKET;
// TODO: WebSocket endpoints should probably always be async?
// For now, let user control with @Async if needed
}
// Auto-detect async from @Async annotation OR CompletableFuture return type
setAsync(m.getAnnotation(Async.class) != null || CompletableFuture.class.isAssignableFrom(retType));
bindDecorators();
}
public String getVerb(){
@@ -64,30 +148,29 @@ public class MethodEndPoint extends EndPoint{
if(d!=null) decorators.add(d);
}
}
/** serves the request by invoking invokeType.serve(request, response).
* this method will lift execution of invoketype into async task if desired and possible.
* Methods returning CompletableFuture are handled by encodeResponse() to avoid double wrapping.
*/
@Override
public void serve(Request request, Response response) throws IOException{
log().debug("Serving method....{}",invokeType);
try{
Object ret=null;
switch(invokeType){
case PLAIN:{ // plain profile just passes req,resp
method.invoke(target,request,response);
break;
// Only lift to async if @Async annotation but NOT returning CompletableFuture
// (CompletableFuture returns are handled by encodeResponse to avoid double wrapping)
boolean needsAsyncWrapper = isAsync() && !CompletableFuture.class.isAssignableFrom(retType);
if(needsAsyncWrapper && request.goAsync()) {
// Start async promise chain for @Async annotated methods
response.promiseFirst(v -> {
try {
invokeType.serve(request, response);
} catch (IOException e) {
throw new RuntimeException(e);
}
case NOARG:{ // no args will not pass any arguments, will deal with return (marshalling)
ret=method.invoke(target);
encodeResponse(ret,response);
break;
}
default:{ // here we do full unmarshalling, marshalling
Object[] argVals=decodeRequest(request);
ret=method.invoke(target,argVals);
encodeResponse(ret,response);
}
}
}catch(Exception ex2){
if(ex2 instanceof IOException) throw ((IOException)ex2);
else throw new IOException(ex2);
return null;
});
}else{
// Sync execution or CompletableFuture return (async handled in encodeResponse)
invokeType.serve(request, response);
}
}
protected Object[] decodeRequest(Request request){
@@ -98,11 +181,92 @@ public class MethodEndPoint extends EndPoint{
String byName=p.getName();
String byPos="_arg"+i;
Object val=request.getParam(byName,request.getParam(byPos,null)); // get by name or pos
// Validate input before normalization
val=validateInput(val,cls,byName);
argVals[i]=Handy.normalize(cls,val);
}
return argVals;
}
/**
* Validates input before processing to prevent injection attacks and malformed data.
* @param val raw input value
* @param expectedType expected type
* @param paramName parameter name for error messages
* @return validated value (may be modified or rejected)
* @throws IllegalArgumentException if validation fails
*/
protected Object validateInput(Object val, Class<?> expectedType, String paramName){
if(val==null) return null;
// String validation
if(val instanceof String){
String str=(String)val;
// Limit string length to prevent DoS
if(str.length()>100000){
log().warn("Input parameter '{}' exceeds maximum length, truncated",paramName);
str=str.substring(0,100000);
}
// For string types, return as-is (normalization will handle conversion)
if(expectedType==String.class || expectedType==CharSequence.class){
return str;
}
}
// Array validation
if(val instanceof String[]){
String[] arr=(String[])val;
if(arr.length>1000){
log().warn("Input parameter '{}' array exceeds maximum size",paramName);
throw new IllegalArgumentException("Array parameter '"+paramName+"' exceeds maximum size");
}
for(String s:arr){
if(s!=null && s.length()>100000){
log().warn("Input parameter '{}' array element exceeds maximum length",paramName);
throw new IllegalArgumentException("Array element in '"+paramName+"' exceeds maximum length");
}
}
}
// Type validation - ensure value can be converted to expected type
if(expectedType.isPrimitive() || Number.class.isAssignableFrom(expectedType) ||
Boolean.class.isAssignableFrom(expectedType) || expectedType==Boolean.class){
// These will be validated during normalization
return val;
}
return val;
}
/** Encodes the response to the response encoder.
* We handle here future value as well by chaining.
*/
protected void encodeResponse(Object ret, Response response) throws IOException{
final Request request=response.getRequest();
if(ret instanceof CompletableFuture){
// Method returns a future - we turn async
@SuppressWarnings("unchecked")
CompletableFuture<Object> future = (CompletableFuture<Object>)ret;
// Check if we can go async - we are not using isAsync here
if(request.goAsync()) {
// we can go async
// Chain the future directly - NO BLOCKING!
response.promiseFirst(v -> future) // Returns the future, Response will flatten it
.promiseNext(result -> {
// Encode the result (recursive call, but result is not a future)
try {
encodeResponse(result, response);
} catch (IOException e) {
throw new RuntimeException(e);
}
return null;
});
} else {
// Blocking fallback - wait for future synchronously
Object result = future.join();
encodeResponse(result, response);
}
return; // Important: exit after setting up async chain
}
if(ret instanceof Response){
// we have a response return - take its status and content type
Response resp=(Response)ret;
@@ -116,6 +280,10 @@ public class MethodEndPoint extends EndPoint{
String ctype=route.return_mime();
if(Handy.isBlank(ctype)) ctype=HTTP.guess_mime(ret);
response.setContentType(ctype);
// Set status to OK if not already set
if(response.getStatus()==null){
response.setStatus(Response.HTTP_OK);
}
if(ret!=null){
response.getEncoder().writeObject(ret);
}
+74 -25
View File
@@ -7,6 +7,7 @@ You may not use this file except in compliance with the License.
*/
package com.reliancy.jabba;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -14,18 +15,20 @@ import org.slf4j.LoggerFactory;
* App is a processor and under it a router and a chain of filters are also processors.
* Also endpoints are processors too.
*/
public abstract class Processor {
public abstract class Processor implements Servant {
protected Processor parent;
protected Processor next;
protected String id;
protected boolean active;
protected transient Config config;
protected Logger logger;
protected boolean isAsync;
public Processor(String id){
next=null;
this.id=id!=null?id:this.getClass().getSimpleName();
active=true;
isAsync=false;
}
public String getId(){
return id;
@@ -37,6 +40,20 @@ public abstract class Processor {
public void setNext(Processor next) {
this.next = next;
}
/**
* Find a processor of the given class type in the parent chain.
* @param cls the class type to search for
* @return the processor if found, null otherwise
*/
@SuppressWarnings("unchecked")
public <T extends Processor> T getParent(Class<T> cls) {
Processor p=parent;
while(p!=null){
if(cls.isAssignableFrom(p.getClass())) return (T) p;
p=p.getParent();
}
return null;
}
public Processor getParent() {
return parent;
}
@@ -57,33 +74,44 @@ public abstract class Processor {
if(parent!=null) return parent.getConfig();
return null;
}
// using config as a marker of a run so set during begin
// public void setConfig(Config config) {
// this.config = config;
// }
/**
* Main event processing chain.
* Will go down the chain until result code is set.
/** Internal processing method that can handle async and non-async use cases.
* Process the request and response, handling async if needed.
* @param request
* @param response
* @param isAsync
* @throws IOException
*/
public void process(Request request,Response response) throws IOException {
CallSession ss=CallSession.getInstance();
try{
ss.enter(this);
if(!active){
if(next!=null) next.process(request, response);
}else{
before(request, response);
if(response.getStatus()==null) serve(request, response);
if(next!=null && response.getStatus()==null) next.process(request, response);
after(request, response);
protected void process(Request request,Response response) throws IOException {
final CallSession ss=CallSession.getInstance();
// now we must account for async downstream
final Processor thisProcessor=this;
ss.enter(thisProcessor);
if(!active){
if(next!=null){
next.process(request, response);
return;
}
}else{
beforeServe(request, response);
serve(request, response);
if(response.isPromised()==false){
afterServe(request, response);
ss.leave(thisProcessor);
}else{
response.promiseNext((value) -> {
try {
afterServe(request, response);
return value;
} catch (IOException e) {
throw new RuntimeException(e);
}finally{
ss.leave(thisProcessor);
}
});
}
}finally{
ss.leave(this);
}
}
/** Place to prepare for a run. */
public void begin(Config conf) throws Exception{
this.config=conf;
@@ -109,10 +137,31 @@ public abstract class Processor {
if(ret==null) ret=logger=LoggerFactory.getLogger(this.getId());
return ret;
}
/**
* Check if this endpoint handles async requests.
* @return true if method returns CompletableFuture
*/
public boolean isAsync() {
return isAsync;
}
public void setAsync(boolean isAsync) {
this.isAsync = isAsync;
}
/** called before serve. */
public abstract void before(Request request,Response response) throws IOException;
public void beforeServe(Request request,Response response) throws IOException{
}
/** called after serve. */
public abstract void after(Request request,Response response) throws IOException;
/** main processing and subprocessing happens here. */
public abstract void serve(Request request,Response response) throws IOException;
public void afterServe(Request request,Response response) throws IOException{
}
/** default implementation of work.
* if next processor is not null and response status is null, it will process the next processor.
* otherwise it will return null if sync, or a completed future if async.
*/
public void serve(Request request,Response response) throws IOException{
if(next==null || response.getStatus()!=null) return;
next.process(request, response);
}
}
+54 -100
View File
@@ -7,116 +7,70 @@ You may not use this file except in compliance with the License.
*/
package com.reliancy.jabba;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import com.reliancy.util.Handy;
/**
* Abstract representation of an HTTP request.
* Provides container-agnostic access to request properties.
*/
public abstract class Request {
protected final HashMap<String,String> pathParams=new HashMap<>();
protected String pathOverride;
protected Runnable finisher;
protected CallSession session;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
public class Request {
final HttpServletRequest http_request;
final HashMap<String,String> pathParams=new HashMap<>();
String pathOverride;
public Request(HttpServletRequest http_request) {
this.http_request = http_request;
public Request() {
finisher = () -> {};
}
public CallSession getSession() {
return session;
}
public void setSession(CallSession session) {
this.session = session;
}
public void setFinisher(Runnable finisher) {
this.finisher = finisher;
}
public boolean isFinished() {
return finisher == null;
}
public abstract void finish();
public abstract boolean isAsync();
public abstract boolean goAsync();
public Map<String,String> getPathParams(){
return pathParams;
}
public String getPath() {
if(pathOverride!=null){
return pathOverride;
}else{
return http_request.getPathInfo();
}
}
public Request setPath(String path){
pathOverride=path;
return this;
}
public String getVerb() {
return http_request.getMethod();
}
/**
* Look for this parameter in pathParam, queryParams and forms.
* @param pname
* @return
*/
public Object getParam(String pname,Object def){
if(pathParams.containsKey(pname)) return pathParams.get(pname);
String[] vals=http_request.getParameterValues(pname);
if(vals!=null) return vals.length==1?vals[0]:vals;
String hdr=getHeader(pname);
if(hdr!=null) return hdr;
String cook=getCookie(pname,null);
if(cook!=null) return cook;
return def;
}
public Request setParam(String pname,Object val){
if(pathParams.containsKey(pname)){
pathParams.put(pname,String.valueOf(Handy.nz(val,"")));
}else{
throw new IllegalArgumentException("invalid param name:"+pname);
}
return this;
}
public String getHeader(String key){
return http_request.getHeader(key);
}
public String getCookie(String name,String def){
Cookie[] all=http_request.getCookies();
if(all!=null) for(Cookie c:all){
if(name.equalsIgnoreCase(c.getName())) return c.getValue();
}
return def;
}
private static final String[] HEADERS4IP = {
"X-Forwarded-For",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_X_FORWARDED_FOR",
"HTTP_X_FORWARDED",
"HTTP_X_CLUSTER_CLIENT_IP",
"HTTP_CLIENT_IP",
"HTTP_FORWARDED_FOR",
"HTTP_FORWARDED",
"HTTP_VIA",
"REMOTE_ADDR" };
/**
* This method will consult several headers to obain ip address.
* @return best guess for remote address.
*/
public String getRemoteAddress() {
for (String header : HEADERS4IP) {
String ip = getHeader(header);
if(ip==null || ip.length()==0 || "unknown".equalsIgnoreCase(ip)) continue;
return ip.contains(",")?ip.split(",",2)[0]:ip;
}
return http_request.getRemoteAddr();
}
/**
* will return shema://host:port/context
* @return everything preceeding the path.
*/
public String getMount(){
String scheme = http_request.getScheme();
String host = http_request.getHeader("Host"); // includes server name and server port
if(host==null || host.trim().isEmpty()){
// try differenty for host
String serverName = http_request.getServerName();
int serverPort = http_request.getServerPort();
host=serverName+":"+serverPort;
}
String resultPath = scheme + "://" + host;
String contextPath = http_request.getContextPath(); // includes leading forward slash
if(contextPath!=null){
resultPath+= contextPath;
}
return resultPath;
}
public String getProtocol(){
return http_request.getProtocol();
}
public abstract String getPath();
public abstract String getVerb();
public abstract Object getParam(String pname, Object def);
public abstract Request setParam(String pname, Object val);
public abstract String getHeader(String key);
public abstract String getCookie(String name, String def);
public abstract String getRemoteAddress();
public abstract String getMount();
public abstract String getProtocol();
public abstract String getScheme();
}
+205 -91
View File
@@ -14,62 +14,80 @@ import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.BiConsumer;
import java.util.function.Function;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
/**
* Our representation of the response.
* We usually wrap servlet response with this object and use in write mode.
* But we can also create it with no servletresponse then it represents delayed response to be
* read out later and written somewhere.
* Abstract representation of an HTTP response.
* Provides container-agnostic response handling with async support.
*/
public class Response {
// status codes
public static final int HTTP_OK=HttpServletResponse.SC_OK;
public static final int HTTP_BAD_REQUEST=HttpServletResponse.SC_BAD_REQUEST;
public static final int HTTP_NOT_FOUND=HttpServletResponse.SC_NOT_FOUND;
public static final int HTTP_UNAUTHORIZED=HttpServletResponse.SC_UNAUTHORIZED;
public static final int HTTP_FORBIDDEN=HttpServletResponse.SC_FORBIDDEN;
public static final int HTTP_TEMPORARY_REDIRECT=HttpServletResponse.SC_TEMPORARY_REDIRECT;
public static final int HTTP_FOUND_REDIRECT=HttpServletResponse.SC_FOUND;
public static final int HTTP_NOT_MODIFIED=HttpServletResponse.SC_NOT_MODIFIED;
public static final int HTTP_INTERNAL_ERROR=HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
public abstract class Response {
// HTTP status codes
public static final int HTTP_OK=200;
public static final int HTTP_BAD_REQUEST=400;
public static final int HTTP_NOT_FOUND=404;
public static final int HTTP_UNAUTHORIZED=401;
public static final int HTTP_FORBIDDEN=403;
public static final int HTTP_TEMPORARY_REDIRECT=307;
public static final int HTTP_FOUND_REDIRECT=302;
public static final int HTTP_NOT_MODIFIED=304;
public static final int HTTP_INTERNAL_ERROR=500;
final protected HttpServletResponse http_response;
final protected Writer char_response;
final protected OutputStream byte_response;
protected final Request request;
protected final Writer char_response;
protected final OutputStream byte_response;
protected ResponseEncoder encoder;
protected String content_type;
protected Integer status;
protected final ArrayList<HTTP.Header> headers=new ArrayList<>();
protected final ArrayList<HTTP.Cookie> cookies=new ArrayList<>();
protected ResponseState state = ResponseState.CREATED;
protected final ArrayList<HTTP.Header> headers=new ArrayList<>();
protected final ArrayList<HTTP.Cookie> cookies=new ArrayList<>();
protected CompletableFuture<Object> promise;
protected boolean upgradedToWebSocket;
public Response(HttpServletResponse http_response) {
this.http_response = http_response;
this.char_response=null;
this.byte_response=null;
protected Response(Request request) {
this.request = request;
this.char_response = null;
this.byte_response = null;
}
public Response(Writer w) {
this.http_response = null;
protected Response(Writer w) {
this.request = null;
this.char_response=w;
this.byte_response=null;
}
public Response(OutputStream w) {
this.http_response = null;
protected Response(OutputStream w) {
this.request = null;
this.char_response=null;
this.byte_response=w;
}
public Response() {
this.http_response = null;
protected Response() {
this.request = null;
this.char_response=new StringWriter();
this.byte_response=null;
}
public ResponseState getState() {
return state;
}
public void transitionTo(ResponseState newState) {
this.state = this.state.transitionTo(newState);
}
public Request getRequest() {
return request;
}
public ResponseEncoder getEncoder(){
if(encoder==null) encoder=new ResponseEncoder(this);
return encoder;
}
/**returns accumulated string body content if in stringwriter mode or possibly bytearray*/
public Object getContent(){
if(char_response instanceof StringWriter){
return ((StringWriter)char_response).toString();
@@ -77,9 +95,7 @@ public class Response {
return ((ByteArrayOutputStream)byte_response).toByteArray();
}else return null;
}
/** similar to get content only sends own content to external encoder.
* @throws IOException
**/
public void exportContent(ResponseEncoder ext) throws IOException {
if(char_response instanceof StringWriter){
ext.writeString(((StringWriter)char_response).toString());
@@ -88,76 +104,174 @@ public class Response {
ext.writeBytes(buf,0,buf.length);
}
}
public void setContentType(String ctype) {
content_type=ctype;
if(http_response!=null) http_response.setContentType(ctype);
public OutputStream getOutputStream() throws IOException{
return byte_response;
}
public Writer getWriter() throws IOException{
return char_response;
}
public abstract void setContentType(String ctype);
public String getContentType(){
return content_type;
}
public void setStatus(int status) {
this.status=status;
if(http_response!=null) http_response.setStatus(status);
}
public abstract void setStatus(int status);
public Integer getStatus(){
return status;
}
public String getHeader(String key){
for(HTTP.Header hdr:headers){
if(key.equalsIgnoreCase(key)) return hdr.value;
}
if(http_response!=null){
return http_response.getHeader(key);
}else{
return null;
}
}
public Response setHeader(String key,String val){
HTTP.Header sel=null;
for(HTTP.Header hdr:headers){
if(key.equalsIgnoreCase(key)){
sel=hdr;
break;
}
}
if(sel!=null) sel.value=val; else headers.add(new HTTP.Header(key,val));
if(http_response!=null) http_response.setHeader(key,val);
return this;
}
public abstract String getHeader(String key);
public abstract Response setHeader(String key, String val);
public List<HTTP.Header> getHeaders(){
return headers;
}
public String getCookie(String key){
for(HTTP.Cookie c:cookies){
if(key.equalsIgnoreCase(key)) return c.value;
if(key.equalsIgnoreCase(c.key)) return c.value;
}
return null;
}
public Response setCookie(String key,String val,int maxAge,boolean secure){
HTTP.Cookie sel=null;
for(HTTP.Cookie hdr:cookies){
if(key.equalsIgnoreCase(key)){
sel=hdr;
break;
}
}
if(sel!=null){
sel.value=val;
sel.maxAge=maxAge;
sel.secure=secure;
} else{
cookies.add(new HTTP.Cookie(key,val,maxAge,secure));
}
if(http_response!=null){
Cookie c=new Cookie(key,val);
c.setMaxAge(maxAge);
c.setSecure(secure);
http_response.addCookie(c);
}
return this;
public abstract Response setCookie(String key, String val, int maxAge, boolean secure);
public Response setCookie(String key, String val, int maxAge, boolean secure, boolean httpOnly){
return setCookie(key, val, maxAge, secure);
}
public List<HTTP.Cookie> getCookies(){
return cookies;
}
}
public abstract boolean isCommitted();
public abstract void commit();
public abstract boolean isCompleted();
public abstract void complete();
public boolean isPromised() {
return promise!=null;
}
public boolean isUpgradedToWebSocket() {
return upgradedToWebSocket;
}
/**
* Initiate an async promise chain using supplyAsync.
* Gets executor from request's CallSession.
* Automatically attaches/detaches CallSession for the executing thread.
* Can be called multiple times - will chain after existing promise.
* Automatically flattens if supplier returns a CompletableFuture.
* @param supplier lambda that accepts one value and returns one value (or CompletableFuture)
* @return this Response for chaining
*/
public Response promiseFirst(Function<Object, Object> supplier) {
if(request == null) {
throw new IllegalStateException("Cannot create promise without request");
}
final CallSession session = request.getSession();
Executor executorTemp = session != null ? session.getExecutor() : null;
if(executorTemp == null) {
executorTemp = java.util.concurrent.ForkJoinPool.commonPool();
}
final Executor executor = executorTemp;
Function<Object, CompletableFuture<Object>> newTask = (prevValue) -> {
CompletableFuture<Object> innerFuture = CompletableFuture.supplyAsync(() -> {
session.beginAgain();
try {
return supplier.apply(prevValue);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
session.end();
}
}, executor);
return innerFuture.thenCompose(result -> {
if(result instanceof CompletableFuture) {
@SuppressWarnings("unchecked")
CompletableFuture<Object> futureResult = (CompletableFuture<Object>)result;
return futureResult;
} else {
return CompletableFuture.completedFuture(result);
}
});
};
if(promise != null) {
promise = promise.thenCompose(newTask);
} else {
promise = newTask.apply(null);
}
return this;
}
/**
* Add a step to the promise chain.
* Automatically attaches/detaches CallSession for the executing thread.
* @param step lambda that accepts the value from previous step and returns a value
* @return this Response for chaining
*/
public Response promiseNext(Function<Object, Object> step) {
if(promise == null) {
throw new IllegalStateException("Promise chain not initiated. Call promiseFirst() first.");
}
final CallSession session = request.getSession();
promise = promise.thenApply(value -> {
session.beginAgain();
try {
return step.apply(value);
} catch (Exception e) {
throw (e instanceof RuntimeException) ? (RuntimeException)e : new RuntimeException(e);
} finally {
session.end();
}
});
return this;
}
/**
* Final step in the promise chain - finalizes response and handles errors.
* Automatically attaches/detaches CallSession for the executing thread.
* @param callback BiConsumer that receives result and error
* @return this Response for chaining
*/
public Response promiseLast(BiConsumer<Object, Throwable> callback) {
if(promise == null) {
throw new IllegalStateException("Promise chain not initiated. Call promiseFirst() first.");
}
final CallSession session = request.getSession();
promise = promise.whenComplete((result, error) -> {
session.beginAgain();
try {
callback.accept(result, error);
} catch (Exception e) {
e.printStackTrace();
} finally {
session.end();
}
}).thenApply(v -> null);
return this;
}
/**
* Upgrade HTTP response to WebSocket.
* Called by INVOKE_WEBSOCKET in MethodEndPoint.
*
* @param route The WebSocket route path
* @param appSession User session from CallSession (can be null)
* @return WebSocketSession for this connection
* @throws IOException if upgrade fails
*
* TODO: Implement in ServletResponse using Jakarta WebSocket API
*/
public abstract WebSocketSession upgradeToWebSocket(String route, Session appSession) throws IOException;
}
@@ -22,8 +22,14 @@ import java.text.MessageFormat;
import java.util.Collection;
import java.util.Iterator;
import com.reliancy.rec.JSONEncoder;
import com.reliancy.util.CodeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.reliancy.io.JsonEncoder;
import com.reliancy.util.CodeException;
import com.reliancy.jabba.ui.Rendering;
import com.reliancy.jabba.ui.Template;
/**
* This class will replace the Java writer.
@@ -39,6 +45,11 @@ public class ResponseEncoder implements Appendable,Closeable{
protected OutputStream out;
protected Charset charSet;
protected String errorFmt;
private static final Logger logger = LoggerFactory.getLogger(ResponseEncoder.class);
protected Logger log(){
return logger;
}
public ResponseEncoder(Response r){
this(r,StandardCharsets.UTF_8);
@@ -48,7 +59,7 @@ public class ResponseEncoder implements Appendable,Closeable{
public ResponseEncoder(Response r,Charset chset){
response=r;
//locale=loc;
charSet=StandardCharsets.UTF_8;
charSet=chset;
}
public ResponseEncoder setCharSet(Charset set){
charSet=set;
@@ -56,13 +67,9 @@ public class ResponseEncoder implements Appendable,Closeable{
}
public OutputStream getOutputStream() throws IOException{
if(out!=null) return out;
if(response.getStatus()==null) response.setStatus(Response.HTTP_OK);
if(response.getContentType()==null) response.setContentType("application/octet-stream");
if(response.http_response!=null){
out=response.http_response.getOutputStream();
}else if(response.byte_response!=null){
out=response.byte_response;
}else{
response.commit();
out=response.getOutputStream();
if(out==null){
out=new ByteArrayOutputStream();
}
writer=new OutputStreamWriter(out,charSet);
@@ -70,56 +77,107 @@ public class ResponseEncoder implements Appendable,Closeable{
}
public Writer getWriter() throws IOException{
if(writer!=null) return writer;
if(response.getStatus()==null) response.setStatus(Response.HTTP_OK);
if(response.getContentType()==null) response.setContentType("text/plain;charset=utf-8");
if(response.http_response!=null){
writer=response.http_response.getWriter();
}else if(response.char_response!=null){
writer=response.char_response;
}else if(response.byte_response!=null){
out=response.byte_response;
writer=new OutputStreamWriter(out,charSet);
}else{
response.commit();
writer=response.getWriter();
if(writer==null){
writer=new StringWriter();
}
return writer;
}
public ResponseEncoder writeBytes(byte[] buf,int offset,int len) throws IOException{
getOutputStream().write(buf,offset, len);
public void flush() throws IOException{
if(writer!=null) writer.flush();
if(out!=null) out.flush();
}
public ResponseEncoder writeBytes(byte[] buf,int offset,int len) throws IOException{
OutputStream os=getOutputStream();
try{
response.transitionTo(ResponseState.WRITING);
os.write(buf,offset, len);
}finally{
if(response.getState() == ResponseState.WRITING) {
response.transitionTo(ResponseState.WRITTEN);
}
}
return this;
}
public ResponseEncoder writeString(CharSequence str) throws IOException{
getWriter().append(str);
// Get writer first (this will commit if still in CONFIGURING)
Writer wr=getWriter();
try{
// Now transition to WRITING (state should be COMMITTED at this point)
response.transitionTo(ResponseState.WRITING);
wr.append(str);
}finally{
if(response.getState() == ResponseState.WRITING) {
response.transitionTo(ResponseState.WRITTEN);
}
}
return this;
}
public ResponseEncoder writeStream(InputStream is) throws IOException{
byte[] buf=new byte[2*4096];
int bytesRead=-1;
while((bytesRead=is.read(buf))!=-1){
writeBytes(buf,0,bytesRead);
// Get output stream first (this will commit if still in CONFIGURING)
OutputStream os=getOutputStream();
try{
// Now transition to WRITING (state should be COMMITTED at this point)
response.transitionTo(ResponseState.WRITING);
while((bytesRead=is.read(buf))!=-1){
os.write(buf,0,bytesRead);
}
}finally{
if(response.getState() == ResponseState.WRITING) {
response.transitionTo(ResponseState.WRITTEN);
}
}
return this;
}
public ResponseEncoder writeln(CharSequence msg,Object ... args) throws IOException{
if(args.length==0){
getWriter().append(msg).append("\n");
}else{
String str=MessageFormat.format(msg.toString(),args);
getWriter().append(str).append("\n");
// Get writer first (this will commit if still in CONFIGURING)
Writer wr=getWriter();
try{
// Now transition to WRITING (state should be COMMITTED at this point)
response.transitionTo(ResponseState.WRITING);
if(args.length>0){
msg=MessageFormat.format(msg.toString(),args);
}
wr.append(msg).append("\n");
}finally{
if(response.getState() == ResponseState.WRITING) {
response.transitionTo(ResponseState.WRITTEN);
}
}
return this;
}
public ResponseEncoder writeIterator(Iterator<String> it) throws IOException{
// Get writer first (this will commit if still in CONFIGURING)
Writer wr=getWriter();
while(it.hasNext()) wr.append(it.next());
try{
// Now transition to WRITING (state should be COMMITTED at this point)
response.transitionTo(ResponseState.WRITING);
while(it.hasNext()) wr.append(it.next());
}finally{
if(response.getState() == ResponseState.WRITING) {
response.transitionTo(ResponseState.WRITTEN);
}
}
return this;
}
public ResponseEncoder writeReader(Reader rd) throws IOException{
char[] buffer = new char[2*4096];
int n = 0;
// Get writer first (this will commit if still in CONFIGURING)
Writer wr=this.getWriter();
while (-1 != (n = rd.read(buffer))) {
wr.write(buffer, 0, n);
try{
// Now transition to WRITING (state should be COMMITTED at this point)
response.transitionTo(ResponseState.WRITING);
while (-1 != (n = rd.read(buffer))) {
wr.write(buffer, 0, n);
}
}finally{
if(response.getState() == ResponseState.WRITING) {
response.transitionTo(ResponseState.WRITTEN);
}
}
return this;
}
@@ -130,48 +188,146 @@ public class ResponseEncoder implements Appendable,Closeable{
public String getErrorFormat(){
return this.errorFmt;
}
/** When an error occurs we need properly render exception.
* if html is accepted we try to render a valid response with n error within a template so it fits with the app.
* for all others we set error status code.
* for json,xml and plain we render into a message template for the rest we do nothing.
* this method returns true if a response was generated. in overloaded methods
* if false is returned we can generate response the status is set to 500 already.
* @param ex exception state
* @return this encoder for chaining
* @throws IOException if writing the error response fails
*/
public ResponseEncoder writeError(Throwable ex) throws IOException{
if(errorFmt==null){
this.writeString(ex.toString());
}else{
log().error("error:",ex);
Request req=response.getRequest();
if(response.getStatus()==null) response.setStatus(Response.HTTP_INTERNAL_ERROR);
String accepted_format=req!=null?req.getHeader("Accept"):null;
boolean present=accepted_format!=null;
if(present && (accepted_format.contains("/html") || accepted_format.contains("/xhtml"))){
// we have html request
response.setContentType(HTTP.MIME_HTML);
Template t=Template.find("/templates/error.hbs");
if(t==null){ // no template found
if(ex instanceof IOException) throw ((IOException)ex);
else throw new RuntimeException(ex);
}
Rendering.begin(t).with(ex).end(response);
return this;
}
// next we format a few common and supported messages
if(present && accepted_format.contains("/json")){
response.setContentType(HTTP.MIME_JSON);
String template=getErrorFormat();
if(template==null){
template="'{'\n\t\"status\":\"error\",\n\t\"title\":\"{0}\",\n\t\"message\":\"{1}\"\n'}'\n";
}
StringBuilder title=new StringBuilder();
StringBuilder detail=new StringBuilder();
CodeException.fillUserMessage(ex, detail, title);
String body=MessageFormat.format(
errorFmt,
JSONEncoder.escape(title),
JSONEncoder.escape(detail));
String body=MessageFormat.format(template,JsonEncoder.escape(title),JsonEncoder.escape(detail));
writeString(body);
return this;
}
if(present && accepted_format.contains("/xml")){
response.setContentType(HTTP.MIME_XML);
String template=getErrorFormat();
if(template==null){
template="<response>\n\t<status>error</status>\n\t<title>{0}</title>\n\t<message>{1}</message>\n</response>\n";
}
StringBuilder title=new StringBuilder();
StringBuilder detail=new StringBuilder();
CodeException.fillUserMessage(ex, detail, title);
String body=MessageFormat.format(template,title,detail);
writeString(body);
return this;
}
if(present && accepted_format.contains("text/plain")){
response.setContentType(HTTP.MIME_PLAIN);
String template=getErrorFormat();
if(template==null){
template="status=error\n\ntitle={0}\n\nmessage={1}\n\n";
}
StringBuilder title=new StringBuilder();
StringBuilder detail=new StringBuilder();
CodeException.fillUserMessage(ex, detail, title);
String body=MessageFormat.format(template,title,detail);
writeString(body);
return this;
}
String body=ex.toString();
String template=getErrorFormat();
if(template!=null){
StringBuilder title=new StringBuilder();
StringBuilder detail=new StringBuilder();
CodeException.fillUserMessage(ex, detail, title);
body=MessageFormat.format(template,title,detail);
}
this.writeString(body);
return this;
}
public ResponseEncoder writeObject(Object ret) throws IOException{
if(ret==null) return this;
Writer wr=getWriter();
if(ret instanceof Iterator){
Iterator<?> it=(Iterator<?>)ret;
while(it.hasNext()){
Object obj=it.next();
writeObject(obj);
try{
log().debug("ResponseEncoder.writeObject(): ret={}, retType={}", ret, ret.getClass().getName());
// Get writer first (this will commit if still in CONFIGURING)
Writer wr=getWriter();
// Now transition to WRITING (state should be COMMITTED at this point)
response.transitionTo(ResponseState.WRITING);
log().debug("ResponseEncoder.writeObject(): got writer={}", wr != null ? wr.getClass().getName() : "null");
if(ret instanceof Iterator){
Iterator<?> it=(Iterator<?>)ret;
while(it.hasNext()){
Object obj=it.next();
writeObject(obj);
}
}else if(ret instanceof Collection){
Collection<?> cret=(Collection<?>) ret;
for(Object o:cret) writeObject(o);
}else if(ret instanceof Reader){
writeReader((Reader)ret);
}else if(ret instanceof byte[]){
byte[] bret=(byte[])ret;
writeBytes(bret,0,bret.length);
}else if(ret instanceof Throwable){
writeError((Throwable)ret);
}else{
String str = ret.toString();
log().debug("ResponseEncoder.writeObject(): writing string, length={}", str.length());
wr.append(str);
log().debug("ResponseEncoder.writeObject(): string written");
}
}finally{
if(response.getState() == ResponseState.WRITING) {
response.transitionTo(ResponseState.WRITTEN);
}
}else if(ret instanceof Collection){
Collection<?> cret=(Collection<?>) ret;
for(Object o:cret) writeObject(o);
}else if(ret instanceof Reader){
writeReader((Reader)ret);
}else if(ret instanceof byte[]){
byte[] bret=(byte[])ret;
writeBytes(bret,0,bret.length);
}else{
wr.append(ret.toString());
}
//wr.append("\n");
return this;
}
////// Interface implementations
@Override
public void close() throws IOException {
getWriter().close();
try {
// If we're still writing, mark as written before closing
if(response.getState() == ResponseState.WRITING) {
response.transitionTo(ResponseState.WRITTEN);
}
// Close the writer/stream
if(writer != null) {
writer.close();
} else if(out != null) {
out.close();
}
} catch(IOException e) {
// Ensure state is correct even if close fails
if(response.getState() == ResponseState.WRITING) {
response.transitionTo(ResponseState.WRITTEN);
}
throw e;
}
}
@Override
public Appendable append(CharSequence csq) throws IOException {
@@ -179,12 +335,32 @@ public class ResponseEncoder implements Appendable,Closeable{
}
@Override
public Appendable append(CharSequence csq, int start, int end) throws IOException {
this.getWriter().append(csq,start,end);
// Get writer first (this will commit if still in CONFIGURING)
Writer wr=this.getWriter();
try{
// Now transition to WRITING (state should be COMMITTED at this point)
response.transitionTo(ResponseState.WRITING);
wr.append(csq,start,end);
}finally{
if(response.getState() == ResponseState.WRITING) {
response.transitionTo(ResponseState.WRITTEN);
}
}
return this;
}
@Override
public Appendable append(char c) throws IOException {
this.getWriter().append(c);
// Get writer first (this will commit if still in CONFIGURING)
Writer wr=this.getWriter();
try{
// Now transition to WRITING (state should be COMMITTED at this point)
response.transitionTo(ResponseState.WRITING);
wr.append(c);
}finally{
if(response.getState() == ResponseState.WRITING) {
response.transitionTo(ResponseState.WRITTEN);
}
}
return this;
}
}
@@ -0,0 +1,152 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.jabba;
/**
* Represents the lifecycle state of a Response object.
* Response goes through stages: created -&gt; configuring -&gt; committed -&gt; writing &lt;-&gt; written -&gt; completed
*/
public enum ResponseState {
/** Response object created, nothing configured yet */
CREATED,
/** Headers, status, content type can be set */
CONFIGURING,
/** Response committed to HttpServletResponse (headers locked) */
COMMITTED,
/** Body content is being written */
WRITING,
/** Body content has been written */
WRITTEN,
/** Response fully done */
COMPLETED;
/**
* Check if state allows setting headers/status/content type.
* @return true if headers can be modified
*/
public boolean canConfigure() {
return this == CREATED || this == CONFIGURING;
}
/**
* Check if state allows writing body content.
* @return true if body can be written
*/
public boolean canWrite() {
return this == COMMITTED || this == WRITING || this == WRITTEN;
}
/**
* Check if state allows flushing.
* @return true if response can be flushed
*/
public boolean canFlush() {
return this == WRITING || this == WRITTEN || this == COMMITTED;
}
/**
* Check if response has been written (body content exists).
* @return true if body has been written
*/
public boolean isWritten() {
return this == WRITTEN || this == COMPLETED;
}
/**
* Check if response is committed (headers locked).
* @return true if response is committed
*/
public boolean isCommitted() {
return this == COMMITTED || this == WRITING || this == WRITTEN || this == COMPLETED;
}
/**
* Check if response is completed (fully done).
* @return true if response is completed
*/
public boolean isCompleted() {
return this == COMPLETED;
}
/**
* Transition from this state to a new state. Validates the transition.
* Automatically handles intermediate state transitions (e.g., CREATED -> CONFIGURING when configuring,
* CONFIGURING -> WRITING when writing).
* @param to the target state
* @return the new state if transition is valid
* @throws IllegalStateException if transition is invalid
*/
public ResponseState transitionTo(ResponseState to) {
if(this == to) return this;
// Auto-transition to intermediate states if needed
if(to == CONFIGURING) {
// Allow transition to CONFIGURING from CREATED
if(this == CREATED) {
return CONFIGURING;
}
// If already in CONFIGURING or later, check if we can still configure
if(!this.canConfigure()) {
throw new IllegalStateException("Cannot configure in state: " + this);
}
} else if(to == COMMITTED) {
// Allow transition to COMMITTED from CONFIGURING
if(this == CONFIGURING) {
return COMMITTED;
}
// If already committed or later, stay in current state
if(this == COMMITTED || this == WRITING || this == WRITTEN || this == COMPLETED) {
return this;
}
} else if(to == WRITING) {
// Allow transition to WRITING from COMMITTED, WRITING, or WRITTEN
if(this == COMMITTED) {
return WRITING;
}
if(this == WRITTEN) {
return WRITING; // Can go back to writing to append more content
}
// Check if we can write
if(!this.canWrite()) {
throw new IllegalStateException("Cannot write in state: " + this);
}
}
// Validate direct state transitions
switch(this) {
case CREATED:
if(to != CONFIGURING && to != COMPLETED) {
throw new IllegalStateException("Invalid transition from CREATED to " + to);
}
break;
case CONFIGURING:
if(to != COMMITTED && to != WRITING && to != COMPLETED) {
throw new IllegalStateException("Invalid transition from CONFIGURING to " + to);
}
break;
case WRITING:
if(to != WRITTEN && to != COMPLETED) {
throw new IllegalStateException("Invalid transition from WRITING to " + to);
}
break;
case WRITTEN:
if(to != WRITING && to != COMPLETED) {
throw new IllegalStateException("Invalid transition from WRITTEN to " + to);
}
break;
case COMMITTED:
if(to != WRITING && to != COMPLETED) {
throw new IllegalStateException("Invalid transition from COMMITTED to " + to);
}
break;
case COMPLETED:
throw new IllegalStateException("Cannot transition from COMPLETED state");
}
return to;
}
}
+31 -25
View File
@@ -14,15 +14,20 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.reliancy.jabba.decor.Async;
import com.reliancy.jabba.decor.Routed;
import com.reliancy.jabba.decor.WebSocket;
/** Router is a special Processor which redirects requests to endpoints.
*
* Handles HTTP endpoints.
*/
public class Router extends Processor{
HashMap<String,EndPoint> routes=new HashMap<>(); // route pattern to endpoint
HashMap<String,EndPoint> routes=new HashMap<>(); // HTTP route pattern to endpoint
ArrayList<RouteDetector> detectors=new ArrayList<>(); // route patterns ordered
int[] indexes; // indexes for each route within regex
Pattern regex;
@@ -31,12 +36,6 @@ public class Router extends Processor{
super("Router");
}
@Override
public void before(Request request, Response response) throws IOException {
}
@Override
public void after(Request request, Response response) throws IOException {
}
@Override
public void serve(Request req, Response resp) throws IOException {
//System.out.println(req.http_request);
String verb=req.getVerb();
@@ -44,25 +43,26 @@ public class Router extends Processor{
log().info("serving:{}",path);
Matcher m=match(verb,path);
//Matcher m=rep.match("GET","/helloP");
if(m!=null){
//HashMap<String,String> pms=new HashMap<>();
String rt=evalMatcher(m,req.getPathParams());
//System.out.println(req.getPathParams());
EndPoint ep=getRoute(rt);
if(ep!=null){
ep.process(req, resp);
}else{
log().error("no endpoint for:{}",rt);
resp.setContentType("text/plain;charset=utf-8");
resp.setStatus(Response.HTTP_NOT_FOUND);
resp.getEncoder().writeln("no endpoint for :"+rt);
}
}else{
if(m==null){
log().error("could not resolve path:{}",path);
resp.setContentType("text/plain;charset=utf-8");
resp.setStatus(Response.HTTP_NOT_FOUND);
resp.getEncoder().writeln("could not resolve path:"+path);
//return isAsync()?CompletableFuture.completedFuture(null):null;
return;
}
String rt=evalMatcher(m,req.getPathParams());
EndPoint ep=getRoute(rt);
if(ep==null){
log().error("no endpoint for:{}",rt);
resp.setContentType("text/plain;charset=utf-8");
resp.setStatus(Response.HTTP_NOT_FOUND);
resp.getEncoder().writeln("no endpoint for :"+rt);
//return isAsync()?CompletableFuture.completedFuture(null):null;
return;
}
log().info("Router: matched route={}, endpoint={}", rt, ep != null ? ep.getId() : "null");
ep.process(req, resp);
}
/** Lookup of endpoints by full routing string.
* that includes verb.
@@ -158,25 +158,31 @@ public class Router extends Processor{
}
/**
* Will import endpoints to serve various paths.
* Scans for @Routed (HTTP) annotations.
* We can call this multiple times for multiple targets.
* @param target
* @return
*/
public Router importMethods(Object target){
//RoutedEndPoint ret=new RoutedEndPoint();
LinkedList<Method> routes=new LinkedList<>();
LinkedList<Method> httpRoutes=new LinkedList<>();
Class<?> type=target.getClass();
while (type != null) {
for(Method m : type.getDeclaredMethods()){
//System.out.println("Method:"+m.toString());
if(m.getAnnotation(Routed.class)!=null){
routes.add(0,m);
httpRoutes.add(0,m);
}
}
type = type.getSuperclass();
}
for(Method m:routes){
// Process HTTP routes
for(Method m:httpRoutes){
MethodEndPoint mm=new MethodEndPoint(target,m);
// Check for @Async annotation and set async flag
if(m.getAnnotation(Async.class)!=null){
mm.setAsync(true);
}
addRoute(mm.getVerb(),mm.getPath(),mm);
}
return this;
@@ -0,0 +1,25 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.jabba;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
/**
* Interface for objects that can serve responses for requests.
*/
public interface Servant {
/**
* Process a request and generate a response.
* @param request the request to process
* @param response the response to populate
* @throws IOException if processing fails
*/
void serve(Request request, Response response) throws IOException;
}
@@ -2,6 +2,8 @@ package com.reliancy.jabba;
import java.io.IOException;
import com.reliancy.jabba.decor.Routed;
public class StatusMod implements AppModule{
@Override
public void publish(App app) {
@@ -0,0 +1,269 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.jabba;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* Abstract WebSocket session representing a single client connection.
* Provides callback-based API for handling WebSocket events.
*
* Each connection gets its own session instance with:
* - AppSession from the upgrade request (user context)
* - Callback handlers for text, binary, error, close events
* - Static registry for broadcasting to multiple clients
*/
public abstract class WebSocketSession {
// Static registry of all active WebSocket sessions
private static final Map<String, WebSocketSession> allSessions = new ConcurrentHashMap<>();
// Instance fields
protected String id; // Unique session ID: route + "/" + remoteAddress
protected String route; // WebSocket route path (e.g., "/ws/echo")
protected String remoteAddress; // Client address
protected Session appSession; // User session from HTTP upgrade request
// Callback handlers (set by user code)
protected Consumer<String> textHandler;
protected Consumer<byte[]> binaryHandler;
protected Consumer<Throwable> errorHandler;
protected BiConsumer<Integer, String> closeHandler;
/**
* Constructor - automatically registers session in static registry
*/
protected WebSocketSession(String route, String remoteAddress, Session appSession) {
this.route = route;
this.appSession = appSession;
setRemoteAddress(remoteAddress); // Sets address and builds ID
allSessions.put(this.id, this);
}
/**
* Set remote address and rebuild session ID.
* Allows updating from IP to resolved name later.
*/
public void setRemoteAddress(String remoteAddress) {
// Remove old ID from registry if exists
if (this.id != null) {
allSessions.remove(this.id);
}
this.remoteAddress = remoteAddress;
this.id = route + "/" + remoteAddress;
// Re-register with new ID
allSessions.put(this.id, this);
}
// ========== Send Methods (abstract - implemented by servlet/native) ==========
/**
* Send text message to this client
*/
public abstract void sendText(String message) throws IOException;
/**
* Send binary data to this client
*/
public abstract void sendBinary(byte[] data) throws IOException;
/**
* Close this WebSocket connection
*/
public abstract void close() throws IOException;
/**
* Close with status code and reason
*/
public abstract void close(int code, String reason) throws IOException;
/**
* Check if connection is open
*/
public abstract boolean isOpen();
// ========== Callback Setters (used by application code) ==========
/**
* Set handler for incoming text messages
*/
public void onText(Consumer<String> handler) {
this.textHandler = handler;
}
/**
* Set handler for incoming binary messages
*/
public void onBinary(Consumer<byte[]> handler) {
this.binaryHandler = handler;
}
/**
* Set handler for errors
*/
public void onError(Consumer<Throwable> handler) {
this.errorHandler = handler;
}
/**
* Set handler for connection close
* @param handler receives (closeCode, reason)
*/
public void onClose(BiConsumer<Integer, String> handler) {
this.closeHandler = handler;
}
// ========== Internal Callback Invocation (called by implementation) ==========
/**
* Internal: Dispatch text message to handler
*/
protected void handleText(String message) {
if (textHandler != null) {
try {
textHandler.accept(message);
} catch (Exception e) {
handleError(e);
}
}
}
/**
* Internal: Dispatch binary message to handler
*/
protected void handleBinary(byte[] data) {
if (binaryHandler != null) {
try {
binaryHandler.accept(data);
} catch (Exception e) {
handleError(e);
}
}
}
/**
* Internal: Dispatch error to handler
*/
protected void handleError(Throwable error) {
if (errorHandler != null) {
try {
errorHandler.accept(error);
} catch (Exception e) {
// Log error in error handler?
e.printStackTrace();
}
}
}
/**
* Internal: Dispatch close event to handler and cleanup
*/
protected void handleClose(int code, String reason) {
try {
if (closeHandler != null) {
closeHandler.accept(code, reason);
}
} finally {
// Remove from registry
allSessions.remove(this.id);
}
}
// ========== Getters ==========
public String getId() {
return id;
}
public String getRoute() {
return route;
}
public String getRemoteAddress() {
return remoteAddress;
}
public Session getAppSession() {
return appSession;
}
// ========== Static Registry Methods for Broadcasting ==========
/**
* Get all sessions for a specific route
*/
public static Collection<WebSocketSession> getSessionsForRoute(String route) {
return allSessions.values().stream()
.filter(s -> s.route.equals(route))
.collect(Collectors.toList());
}
/**
* Get all active sessions
*/
public static Collection<WebSocketSession> getAllSessions() {
return allSessions.values();
}
/**
* Broadcast text message to all clients on a route
*/
public static void broadcast(String route, String message) {
getSessionsForRoute(route).forEach(session -> {
try {
session.sendText(message);
} catch (IOException e) {
session.handleError(e);
}
});
}
/**
* Broadcast text message to all connected clients
*/
public static void broadcastAll(String message) {
getAllSessions().forEach(session -> {
try {
session.sendText(message);
} catch (IOException e) {
session.handleError(e);
}
});
}
/**
* Get session by ID
*/
public static WebSocketSession getSession(String id) {
return allSessions.get(id);
}
/**
* Get count of active sessions
*/
public static int getSessionCount() {
return allSessions.size();
}
/**
* Get count of sessions for a route
*/
public static int getSessionCount(String route) {
return (int) allSessions.values().stream()
.filter(s -> s.route.equals(route))
.count();
}
}
@@ -0,0 +1,20 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.jabba.decor;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Annotation to mark methods as async endpoints.
* This annotation can be used in conjunction with @Routed.
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface Async {
}
@@ -1,19 +1,19 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.jabba;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Routed {
String path() default "{method}";
String verb() default "GET|POST|DELETE";
String return_mime() default "";
}
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.jabba.decor;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Routed {
String path() default "{method}";
String verb() default "GET|POST|DELETE";
String return_mime() default "";
}
@@ -0,0 +1,52 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.jabba.decor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Marks a method as a WebSocket endpoint.
* WebSocket endpoints handle bidirectional communication with clients.
* Works in conjunction with @Routed annotation for path mapping.
*
* Example usage:
* <pre>
* {@literal @}Routed(path="/ws/chat")
* {@literal @}WebSocket
* public void handleChat(WebSocketSession session) {
* session.onMessage(msg -> {
* session.send("Echo: " + msg);
* });
* }
* </pre>
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface WebSocket {
/**
* Optional subprotocols supported by this endpoint.
* Example: {"mqtt", "stomp"}
*/
String[] subprotocols() default {};
/**
* Maximum message size in bytes (default 64KB).
* Set to -1 for unlimited.
*/
int maxMessageSize() default 65536;
/**
* Idle timeout in milliseconds (default 5 minutes).
* Connection closes if no messages received within this time.
*/
long idleTimeout() default 300000;
}
@@ -13,6 +13,7 @@ import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.CompletableFuture;
import java.util.Map;
import java.util.regex.Pattern;
@@ -24,8 +25,10 @@ import com.reliancy.jabba.Response;
import com.reliancy.jabba.RouteDetector;
import com.reliancy.jabba.MethodDecorator;
import com.reliancy.jabba.MethodEndPoint;
import com.reliancy.jabba.Config;
import com.reliancy.util.CodeException;
import com.reliancy.util.Handy;
import java.security.SecureRandom;
/**
* SecurityPolicy is a filter/processor that implements various auth protocols but also sources users.
@@ -40,7 +43,7 @@ import com.reliancy.util.Handy;
public class SecurityPolicy extends Processor implements MethodDecorator.Factory{
public static String REALM="reliancy";
public static final String KEY_NAME="jbauth";
protected String secret="sdfklgj 7150 9178-54=09";
protected String secret=null;
protected ArrayList<SecurityProtocol> protocols;
protected SecurityActor admin;
protected SecurityActor guest;
@@ -54,6 +57,29 @@ public class SecurityPolicy extends Processor implements MethodDecorator.Factory
protocols.add(new SecurityProtocol.Basic());
}
protected String getSecret(){
if(secret==null){
// Try to load from config first
Config conf=getConfig();
if(conf!=null){
secret=Config.SECRET_KEY.get(conf,null);
}
// Try environment variable
if(secret==null || secret.isEmpty()){
secret=System.getenv("JABBA_SECRET_KEY");
}
// Try system property
if(secret==null || secret.isEmpty()){
secret=System.getProperty("jabba.secret.key");
}
// Generate secure random secret if still not found
if(secret==null || secret.isEmpty()){
SecureRandom random=new SecureRandom();
byte[] bytes=new byte[32];
random.nextBytes(bytes);
secret=java.util.Base64.getEncoder().encodeToString(bytes);
log().warn("No secret key configured. Generated a random secret. This should be set via SECRET_KEY config, JABBA_SECRET_KEY environment variable, or jabba.secret.key system property for production use.");
}
}
return secret;
}
public SecurityPolicy setSecured(String path,Secured info){
@@ -70,7 +96,7 @@ public class SecurityPolicy extends Processor implements MethodDecorator.Factory
return null;
}
@Override
public void before(Request request, Response response) throws IOException {
public void beforeServe(Request request, Response response) throws IOException {
// we will recover a user here
CallSession css=CallSession.getInstance();
AppSession ass=(AppSession) css.getAppSession();
@@ -104,11 +130,7 @@ public class SecurityPolicy extends Processor implements MethodDecorator.Factory
}
}
@Override
public void after(Request request, Response response) throws IOException {
}
@Override
public void serve(Request request, Response response) throws IOException {
// nothing to do here
public void afterServe(Request request, Response response) throws IOException {
}
/** authenticates or establishes user based on user and password.
* same as loadActor but with first param being admin account.
@@ -213,7 +235,7 @@ public class SecurityPolicy extends Processor implements MethodDecorator.Factory
@Override
public MethodDecorator assertDecorator(MethodEndPoint mep, Annotation ann) {
if(!(ann instanceof Secured)) return null;
System.out.println("Assert decorator for:"+mep.getPath());
log().debug("Assert decorator for:{}",mep.getPath());
String verb=mep.getVerb();
String path=mep.getPath();
String pat=RouteDetector.toPattern(verb, path);
@@ -1,276 +1,370 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.jabba;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.EventListener;
import com.reliancy.jabba.sec.SecurityPolicy;
import com.reliancy.jabba.sec.plain.PlainSecurityStore;
import com.reliancy.jabba.ui.Menu;
import com.reliancy.jabba.ui.MenuItem;
import com.reliancy.jabba.ui.Rendering;
import com.reliancy.jabba.ui.Template;
import com.reliancy.rec.JSONEncoder;
import com.reliancy.util.CodeException;
import com.reliancy.util.Log;
import com.reliancy.util.Resources;
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.MultiPartFormDataCompliance;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* Router is entry point and servlet implementation that dispatches messages to our endpoints.
* It will launch an embedded jetty server.
* It will provide facilities to register endpoints via router.
* Mostly new routes are injected via AppModules which publish themselves.
* JettyApp installs ForwardCustomizer to react to reverse proxy setups.
*
*/
public class JettyApp extends App implements Handler{
enum State{
STOPPED,
FAILED,
STARTING,
STARTED,
STOPPING,
RUNNING
}
protected Connector[] connectors;
protected Server jetty;
private volatile State _state;
public JettyApp() {
super("JettyApp");
jetty = new Server();
jetty.setHandler(this);
_state=State.STOPPED;
this.addShutdownHook();
}
public Connector[] getConnectors(){
if(connectors!=null) return connectors;
// Create HTTP Config
HttpConfiguration httpConfig = new HttpConfiguration();
// Add support for X-Forwarded headers
httpConfig.addCustomizer( new ForwardedRequestCustomizer() );
// Create the http connector
HttpConnectionFactory http11 = new HttpConnectionFactory( httpConfig );
HTTP2ServerConnectionFactory h2c = new HTTP2CServerConnectionFactory(httpConfig);
ServerConnector httpConn = new ServerConnector(jetty,http11,h2c);
httpConn.setReuseAddress(false);
httpConn.setPort(8090);
connectors=new Connector[] {httpConn};
return connectors;
}
/** implementation of jetty handler interface */
@Override
public Server getServer() {
return jetty;
}
@Override
public void setServer(Server arg0) {
jetty=arg0;
}
@Override
public boolean addEventListener(EventListener arg0) {
return false;
}
@Override
public boolean removeEventListener(EventListener arg0) {
return false;
}
protected void setState(State s){
_state=s;
}
@Override
public boolean isFailed() {
return _state==State.FAILED;
}
@Override
public boolean isRunning() {
return _state==State.RUNNING;
}
@Override
public boolean isStarted() {
return _state==State.STARTED;
}
@Override
public boolean isStarting() {
return _state==State.STARTING;
}
@Override
public boolean isStopped() {
return _state==State.STOPPED;
}
@Override
public boolean isStopping() {
return _state==State.STOPPING;
}
@Override
public void start() throws Exception {
_state=State.STARTED;
jetty.setConnectors(getConnectors());
}
@Override
public void stop() throws Exception {
_state=State.STOPPED;
Connector[] connectors=jetty.getConnectors();
if(connectors==null || connectors.length==0) return;
for(Connector c:connectors){
ServerConnector cc=(ServerConnector) c;
//System.out.println("stopping connecor:"+cc);
try{
cc.stop();
cc.getConnectedEndPoints().forEach((endpoint)-> {
//System.out.println("closing endpoint:"+endpoint);
endpoint.close();
});
}finally{
cc.close();
//System.out.println("closing connecor:"+cc.getState());
}
}
}
@Override
public void destroy() {
}
/**
* Our implementation of a handle process.
* In case of exception if we can locate /tempaltes/error.hbs we use it else we re-throw.
*/
@Override
public void handle(String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response)
throws IOException
{
com.reliancy.jabba.Request req=new com.reliancy.jabba.Request(request);
Response resp=new Response(response);
CallSession ss=CallSession.getInstance();
try{
ss.begin(null, req, resp);
process(req,resp);
}catch(Exception ioex){
processError(req,ioex,resp);
}finally{
baseRequest.setHandled(true);
ss.end();
}
}
/** our own interface specific to jetty engine*/
public void begin(Config conf) throws Exception{
// step 2: configure application, might add processors, adjust config
configure(conf);
// step 1: install config then begin by signaling all middleware
super.begin(conf);
// step 2: start jetty
try{
log().info("starting...");
jetty.start();
}catch(Exception ex){
setState(State.FAILED);
if(ex.getCause() instanceof java.net.BindException){
log().error("bind issue",ex);
Thread.sleep(3000);
}else throw ex;
}
}
public void work() throws InterruptedException{
setState(State.RUNNING);
if(jetty!=null) jetty.join();
}
public void end() throws Exception{
super.end();
Log.cleanup(); // release logging in case we deferred
System.gc(); // sweep memory just in caser
}
/** Registers a shutdown hook to interrup jetty.
* ctrl-c works but does not perform our shutdown sequence.
* this code interrupts jetty and then waits for app to finish.
*/
protected final void addShutdownHook(){
final JettyApp app=this;
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
if(app.isRunning()){
try {
app.jetty.stop();
synchronized(app){
app.wait(5000);
}
} catch (Exception e) {
app.log().error("shutdown cleanup:", e);
}
}
}));
}
/** called from begin just before jetty starts.
* this method is called before middleware is notified so we can add or adjust config.
* override to hook up your application.
* normally follows configuraion and does common sense steps.
* might install middleware (processors) which are later passed config.
*/
public void configure(Config conf) throws Exception{
App app=this;
// setup global search path - include workdir first, then get class and app.class
Class<?> cls=getClass();
if(cls!=JettyApp.class) Resources.appendSearch(0,JettyApp.class);
Resources.appendSearch(0,cls);
String work_dir=ArgsConfig.APP_WORKDIR.get(conf);
if(work_dir!=null) Resources.appendSearch(0,work_dir);
//for(Object p:Resources.search_path){
// System.out.println("sp:"+p);
//}
//Template.search_path(work_dir,App.class); -- not needed anymore
// install app session middleware
app.addAppSession();
// set security policy
SecurityPolicy secpol=new SecurityPolicy().setStore(new PlainSecurityStore());
app.setSecurityPolicy(secpol);
// install router
app.setRouter(new Router());
StatusMod ep=new StatusMod();
ep.publish(app);
// install file sever endpoint
FileServer fs=new FileServer("/static","/public");
fs.publish(app);
Menu top_menu=Menu.request(Menu.TOP);
top_menu.add(new MenuItem("home")).addSpacer().add(new MenuItem("login"));
top_menu.setTitle("Jabba3");
}
public static void main( String[] args ) throws Exception{
Config cnf=new ArgsConfig(args).load();
JettyApp app=new JettyApp();
app.run(cnf);
}
}
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.jabba.servlet;
import java.io.IOException;
import com.reliancy.jabba.App;
import com.reliancy.jabba.ArgsConfig;
import com.reliancy.jabba.CallSession;
import com.reliancy.jabba.Config;
import com.reliancy.jabba.FileServer;
import com.reliancy.jabba.Response;
import com.reliancy.jabba.Router;
import com.reliancy.jabba.StatusMod;
import com.reliancy.jabba.sec.SecurityPolicy;
import com.reliancy.jabba.sec.plain.PlainSecurityStore;
import com.reliancy.jabba.ui.Menu;
import com.reliancy.jabba.ui.MenuItem;
import com.reliancy.util.Log;
import com.reliancy.util.Resources;
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.ServletHolder;
import org.eclipse.jetty.ee10.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* Router is entry point and servlet implementation that dispatches messages to our endpoints.
* It will launch an embedded jetty server.
* It will provide facilities to register endpoints via router.
* Mostly new routes are injected via AppModules which publish themselves.
* JettyApp installs ForwardCustomizer to react to reverse proxy setups.
*
*/
public class JettyApp extends App implements Servlet {
enum State{
STOPPED,
FAILED,
STARTING,
STARTED,
STOPPING,
RUNNING
}
protected Connector[] connectors;
protected Server jetty;
protected ServletConfig servletConfig;
private volatile State _state;
public JettyApp() {
super("JettyApp");
jetty = new Server();
_state=State.STOPPED;
this.addShutdownHook();
}
public Connector[] getConnectors(){
if(connectors!=null) return connectors;
// Create HTTP Config
HttpConfiguration httpConfig = new HttpConfiguration();
// Add support for X-Forwarded headers
httpConfig.addCustomizer( new ForwardedRequestCustomizer() );
// Create the http connector
HttpConnectionFactory http11 = new HttpConnectionFactory( httpConfig );
HTTP2ServerConnectionFactory h2c = new HTTP2CServerConnectionFactory(httpConfig);
ServerConnector httpConn = new ServerConnector(jetty,http11,h2c);
httpConn.setReuseAddress(false);
// Get port from config, environment variable, or default to 8090
int port=8090;
Config conf=getConfig();
if(conf!=null){
port=Config.SERVER_PORT.get(conf,8090);
}
// Check environment variable
String envPort=System.getenv("JABBA_SERVER_PORT");
if(envPort!=null && !envPort.isEmpty()){
try{
port=Integer.parseInt(envPort);
}catch(NumberFormatException e){
log().warn("Invalid JABBA_SERVER_PORT environment variable: {}, using default",envPort);
}
}
// Check system property
String sysPort=System.getProperty("jabba.server.port");
if(sysPort!=null && !sysPort.isEmpty()){
try{
port=Integer.parseInt(sysPort);
}catch(NumberFormatException e){
log().warn("Invalid jabba.server.port system property: {}, using default",sysPort);
}
}
httpConn.setPort(port);
log().info("Server configured to listen on port {}",port);
connectors=new Connector[] {httpConn};
return connectors;
}
/** implementation of jetty handler interface */
public Server getServer() {
return jetty;
}
public void setServer(Server arg0) {
jetty=arg0;
}
protected void setState(State s){
_state=s;
}
public boolean isFailed() {
return _state==State.FAILED;
}
public boolean isRunning() {
return _state==State.RUNNING;
}
public boolean isStarted() {
return _state==State.STARTED;
}
public boolean isStarting() {
return _state==State.STARTING;
}
public boolean isStopped() {
return _state==State.STOPPED;
}
public boolean isStopping() {
return _state==State.STOPPING;
}
public void start() throws Exception {
_state=State.STARTING;
jetty.setConnectors(getConnectors());
jetty.start();
_state=State.STARTED;
}
public void stop() throws Exception {
log().info("Stopping Jetty server...");
_state=State.STOPPING;
Connector[] connectors=jetty.getConnectors();
if(connectors==null || connectors.length==0){
_state=State.STOPPED;
log().info("No connectors to stop.");
return;
}
for(Connector c:connectors){
ServerConnector cc=(ServerConnector) c;
try{
int port = ((ServerConnector)c).getPort();
log().info("Closing connector on port {}...", port);
cc.stop();
cc.getConnectedEndPoints().forEach((endpoint)-> {
endpoint.close();
});
}finally{
cc.close();
}
}
_state=State.STOPPED;
log().info("Jetty server stopped.");
}
public void destroy() {
}
// Servlet interface methods
@Override
public void init(ServletConfig config) throws ServletException {
this.servletConfig = config;
}
@Override
public ServletConfig getServletConfig() {
return servletConfig;
}
@Override
public String getServletInfo() {
return "JettyApp - Jabba Framework Servlet";
}
/**
* Our servlet service implementation.
* In case of exception if we can locate /templates/error.hbs we use it else we re-throw.
*/
@Override
public void service(ServletRequest request, ServletResponse response) throws IOException, ServletException {
// Cast to HTTP versions (this servlet only handles HTTP)
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
final com.reliancy.jabba.servlet.ServletRequest req =
new com.reliancy.jabba.servlet.ServletRequest(httpRequest);
final com.reliancy.jabba.servlet.ServletResponse resp =
new com.reliancy.jabba.servlet.ServletResponse(req, httpResponse);
// Use a fresh CallSession per request so async work shares request state
// without reusing stale thread-local session objects across requests.
final CallSession ss=new CallSession();
// install executor just in case we need it, especially for async processing
ss.setExecutor(
jetty.getThreadPool() != null ?
jetty.getThreadPool() : java.util.concurrent.ForkJoinPool.commonPool()
);
req.setSession(ss);
try{
ss.beginFresh(null, req, resp);
process(req,resp);
}catch(Exception ioex){
try{
resp.getEncoder().writeError(ioex);
}catch(IOException e){
resp.setStatus(Response.HTTP_INTERNAL_ERROR);
}
}finally{
// Only mark as handled if not async (async will be completed later)
// Only end session if not async (async will end session when completing)
if(resp.isPromised()==false){
ss.end();
if(!resp.isUpgradedToWebSocket()){
resp.complete();
}
}else{
resp.promiseLast((result, error) -> {
if(result instanceof Exception){
error=(Exception)result;
result=null;
}
if(error!=null){
try{
resp.getEncoder().writeError(error);
}catch(IOException e){
resp.setStatus(Response.HTTP_INTERNAL_ERROR);
}
}else if(result!=null){
// it should never get here we expect null unless we have an error
try{
resp.getEncoder().writeObject(result);
}catch(IOException e){
resp.setStatus(Response.HTTP_INTERNAL_ERROR);
}
}
resp.complete();
});
}
}
}
/** our own interface specific to jetty engine*/
public void begin(Config conf) throws Exception{
// step 1: configure application, might add processors, adjust config
configure(conf);
// step 2: install config then begin by signaling all middleware
super.begin(conf);
// step 3: create servlet context and mount this servlet
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/");
context.addServlet(new ServletHolder(this), "/*");
// step 3a: initialize Jakarta WebSocket support
// IMPORTANT: must be called before context is started
JakartaWebSocketServletContainerInitializer.configure(context, (servletContext, serverContainer) -> {
// Optional: tune WebSocket defaults
// serverContainer.setDefaultMaxSessionIdleTimeout(Duration.ofMinutes(5));
// serverContainer.setDefaultMaxTextMessageBufferSize(64 * 1024);
log().info("WebSocket support initialized");
});
jetty.setHandler(context);
// step 4: set connectors and start jetty
try{
log().info("starting...");
start();
}catch(Exception ex){
setState(State.FAILED);
if(ex.getCause() instanceof java.net.BindException){
log().error("bind issue",ex);
Thread.sleep(3000);
}else throw ex;
}
}
public void work() throws InterruptedException{
setState(State.RUNNING);
log().info("Server is running. Press Ctrl-C to exit.");
if(jetty!=null) jetty.join();
}
public void end() throws Exception{
log().info("JettyApp cleanup starting...");
stop();
log().info("Cleaning up application processors...");
super.end();
log().info("Application cleanup complete.");
Log.cleanup();
System.gc();
}
/** Registers a shutdown hook to interrupt jetty.
* ctrl-c works but does not perform our shutdown sequence.
* this code interrupts jetty and then waits for app to finish.
*/
protected final void addShutdownHook(){
final JettyApp app=this;
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
if(app.isRunning()){
try {
app.jetty.stop();
synchronized(app){
app.wait(5000);
}
} catch (Exception e) {
app.log().error("shutdown cleanup:", e);
}
}
}));
}
/** called from begin just before jetty starts.
* this method is called before middleware is notified so we can add or adjust config.
* override to hook up your application.
* normally follows configuraion and does common sense steps.
* might install middleware (processors) which are later passed config.
*/
public void configure(Config conf) throws Exception{
App app=this;
// setup global search path - include workdir first, then get class and app.class
Class<?> cls=getClass();
if(cls!=JettyApp.class) Resources.appendSearch(0,JettyApp.class);
Resources.appendSearch(0,cls);
String work_dir=ArgsConfig.APP_WORKDIR.get(conf);
if(work_dir!=null) Resources.appendSearch(0,work_dir);
// install app session middleware
app.addAppSession();
// set security policy
SecurityPolicy secpol=new SecurityPolicy().setStore(new PlainSecurityStore());
app.setSecurityPolicy(secpol);
// install router
app.setRouter(new Router());
StatusMod ep=new StatusMod();
ep.publish(app);
// install file sever endpoint
FileServer fs=new FileServer("/static","/public");
fs.publish(app);
Menu top_menu=Menu.request(Menu.TOP);
top_menu.add(new MenuItem("home")).addSpacer().add(new MenuItem("login"));
top_menu.setTitle("Jabba3");
}
public static void main( String[] args ) throws Exception{
Config cnf=new ArgsConfig(args).load();
JettyApp app=new JettyApp();
app.run(cnf);
}
}
@@ -0,0 +1,173 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.jabba.servlet;
import com.reliancy.jabba.Request;
import com.reliancy.util.Handy;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
/**
* Servlet-based implementation of Request.
* Wraps HttpServletRequest to provide request functionality.
*/
public class ServletRequest extends Request {
protected final HttpServletRequest http_request;
protected AsyncContext asyncContext;
public ServletRequest(HttpServletRequest http_request) {
super();
this.http_request = http_request;
}
@Override
public void finish() {
if(finisher != null){
finisher.run();
finisher = null;
}
if(asyncContext != null){
asyncContext.complete();
asyncContext = null;
}
}
@Override
public boolean isAsync() {
return asyncContext != null;
}
/**
* Start asynchronous processing if supported.
* @return true if async is supported and started, false otherwise
*/
public boolean goAsync() {
if(asyncContext == null && http_request.isAsyncSupported()){
asyncContext = http_request.startAsync();
return true;
}
return false;
}
@Override
public String getPath() {
if(pathOverride!=null){
return pathOverride;
}else{
return http_request.getPathInfo();
}
}
@Override
public String getVerb() {
return http_request.getMethod();
}
@Override
public Object getParam(String pname, Object def){
if(pathParams.containsKey(pname)) {
Object val = pathParams.get(pname);
return val;
}
String[] vals=http_request.getParameterValues(pname);
if(vals!=null) {
Object result = vals.length==1?vals[0]:vals;
return result;
}
String hdr=getHeader(pname);
if(hdr!=null) return hdr;
String cook=getCookie(pname,null);
if(cook!=null) return cook;
return def;
}
@Override
public Request setParam(String pname, Object val){
if(pathParams.containsKey(pname)){
pathParams.put(pname,String.valueOf(Handy.nz(val,"")));
}else{
throw new IllegalArgumentException("invalid param name:"+pname);
}
return this;
}
@Override
public String getHeader(String key){
return http_request.getHeader(key);
}
@Override
public String getCookie(String name, String def){
Cookie[] all=http_request.getCookies();
if(all!=null) for(Cookie c:all){
if(name.equalsIgnoreCase(c.getName())) return c.getValue();
}
return def;
}
private static final String[] HEADERS4IP = {
"X-Forwarded-For",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_X_FORWARDED_FOR",
"HTTP_X_FORWARDED",
"HTTP_X_CLUSTER_CLIENT_IP",
"HTTP_CLIENT_IP",
"HTTP_FORWARDED_FOR",
"HTTP_FORWARDED",
"HTTP_VIA",
"REMOTE_ADDR" };
@Override
public String getRemoteAddress() {
for (String header : HEADERS4IP) {
String ip = getHeader(header);
if(ip==null || ip.length()==0 || "unknown".equalsIgnoreCase(ip)) continue;
return ip.contains(",")?ip.split(",",2)[0]:ip;
}
return http_request.getRemoteAddr();
}
@Override
public String getMount(){
String scheme = http_request.getScheme();
String host = http_request.getHeader("Host");
if(host==null || host.trim().isEmpty()){
String serverName = http_request.getServerName();
int serverPort = http_request.getServerPort();
host=serverName+":"+serverPort;
}
String resultPath = scheme + "://" + host;
String contextPath = http_request.getContextPath();
if(contextPath!=null){
resultPath+= contextPath;
}
return resultPath;
}
@Override
public String getProtocol(){
return http_request.getProtocol();
}
@Override
public String getScheme(){
return http_request.getScheme();
}
/**
* Get the underlying HttpServletRequest.
* @return the HttpServletRequest
*/
public HttpServletRequest getHttpServletRequest(){
return http_request;
}
}
@@ -0,0 +1,193 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.jabba.servlet;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import com.reliancy.jabba.HTTP;
import com.reliancy.jabba.Request;
import com.reliancy.jabba.Response;
import com.reliancy.jabba.ResponseState;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
/**
* Servlet-based implementation of Response.
* Wraps HttpServletResponse to provide response functionality.
*/
public class ServletResponse extends Response {
protected final HttpServletResponse http_response;
public ServletResponse(Request request, HttpServletResponse http_response) {
super(request);
this.http_response = http_response;
}
@Override
public OutputStream getOutputStream() throws IOException{
if(http_response!=null) return http_response.getOutputStream();
return byte_response;
}
@Override
public Writer getWriter() throws IOException{
if(http_response!=null) return http_response.getWriter();
return char_response;
}
@Override
public void setContentType(String ctype) {
transitionTo(ResponseState.CONFIGURING);
content_type=ctype;
if(http_response!=null) http_response.setContentType(ctype);
}
@Override
public void setStatus(int status) {
transitionTo(ResponseState.CONFIGURING);
this.status=status;
if(http_response!=null) http_response.setStatus(status);
}
@Override
public String getHeader(String key){
for(HTTP.Header hdr:headers){
if(key.equalsIgnoreCase(hdr.key)) return hdr.value;
}
if(http_response!=null){
return http_response.getHeader(key);
}else{
return null;
}
}
@Override
public Response setHeader(String key, String val){
transitionTo(ResponseState.CONFIGURING);
if(!state.canConfigure()) {
throw new IllegalStateException("Cannot set header in state: " + state);
}
HTTP.Header sel=null;
for(HTTP.Header hdr:headers){
if(key.equalsIgnoreCase(hdr.key)){
sel=hdr;
break;
}
}
if(sel!=null) sel.value=val; else headers.add(new HTTP.Header(key,val));
if(http_response!=null) http_response.setHeader(key,val);
return this;
}
@Override
public Response setCookie(String key, String val, int maxAge, boolean secure){
return setCookie(key, val, maxAge, secure, true);
}
public Response setCookie(String key, String val, int maxAge, boolean secure, boolean httpOnly){
transitionTo(ResponseState.CONFIGURING);
if(!state.canConfigure()) {
throw new IllegalStateException("Cannot set cookie in state: " + state);
}
HTTP.Cookie sel=null;
for(HTTP.Cookie hdr:cookies){
if(key.equalsIgnoreCase(hdr.key)){
sel=hdr;
break;
}
}
if(sel!=null){
sel.value=val;
sel.maxAge=maxAge;
sel.secure=secure;
sel.httpOnly=httpOnly;
} else{
cookies.add(new HTTP.Cookie(key,val,maxAge,secure,httpOnly));
}
if(http_response!=null){
Cookie c=new Cookie(key,val);
c.setMaxAge(maxAge);
c.setSecure(secure);
c.setHttpOnly(httpOnly);
http_response.addCookie(c);
}
return this;
}
@Override
public boolean isCommitted(){
return state.isCommitted();
}
@Override
public void commit() {
if(isCommitted()) return;
if(getState() == ResponseState.CREATED || getState() == ResponseState.CONFIGURING){
if(getStatus()==null) setStatus(Response.HTTP_OK);
if(getContentType()==null) setContentType("text/plain;charset=utf-8");
transitionTo(ResponseState.CONFIGURING);
}
if(http_response!=null && getState() == ResponseState.CONFIGURING){
if(!http_response.isCommitted()){
try {
http_response.flushBuffer();
} catch (IOException e) {
throw new RuntimeException("Failed to commit response", e);
}
}
}
transitionTo(ResponseState.COMMITTED);
}
@Override
public boolean isCompleted(){
return state.isCompleted();
}
@Override
public void complete() {
try {
if(encoder!=null) encoder.flush();
if(http_response!=null) http_response.flushBuffer();
} catch (IOException e) {
throw new RuntimeException("Failed to complete response", e);
}
transitionTo(ResponseState.COMPLETED);
request.finish();
}
/**
* Get the underlying HttpServletResponse.
* @return the HttpServletResponse
*/
public HttpServletResponse getHttpServletResponse(){
return http_response;
}
/**
* Upgrade HTTP response to WebSocket.
*
* TODO: Implementation needed:
* 1. Get HttpServletRequest from request (cast to ServletRequest)
* 2. Get ServerContainer from ServletContext
* 3. Create ServerEndpointConfig programmatically
* 4. Call container.upgradeHttpToWebSocket(request, response, config, pathParams)
* 5. Create ServletWebSocketSession wrapping Jakarta WebSocket Session
* 6. Wire up message handlers to bridge Jakarta events to our callbacks
*
* See: jakarta.websocket.server.ServerContainer
* See: org.eclipse.jetty.ee10.websocket APIs
*/
@Override
public com.reliancy.jabba.WebSocketSession upgradeToWebSocket(String route, com.reliancy.jabba.Session appSession) throws IOException {
upgradedToWebSocket=true;
return ServletWebSocketSession.create(this,route, appSession);
}
}
@@ -0,0 +1,254 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.jabba.servlet;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.websocket.CloseReason;
import jakarta.websocket.DeploymentException;
import jakarta.websocket.Endpoint;
import jakarta.websocket.EndpointConfig;
import jakarta.websocket.MessageHandler;
import jakarta.websocket.server.ServerContainer;
import jakarta.websocket.server.ServerEndpointConfig;
import com.reliancy.jabba.WebSocketSession;
import com.reliancy.jabba.Session;
/**
* Servlet-based implementation of WebSocketSession.
* Wraps Jakarta WebSocket Session to provide WebSocket functionality.
*/
class ServletWebSocketSession extends WebSocketSession {
/** The underlying Jakarta WebSocket session. */
private jakarta.websocket.Session nativeSession;
public ServletWebSocketSession(String route, String remoteAddress, Session appSession) {
super(route, remoteAddress, appSession);
}
// ========== Native Session Accessor ==========
/**
* Get the underlying Jakarta WebSocket session.
* @return the native session, or null if not yet set
*/
public jakarta.websocket.Session getNativeSession() {
return nativeSession;
}
/**
* Set the underlying Jakarta WebSocket session.
* Called after the upgrade completes to wire up the native session.
* @param nativeSession the Jakarta WebSocket session
*/
public void setNativeSession(jakarta.websocket.Session nativeSession) {
this.nativeSession = nativeSession;
}
// ========== Abstract Method Implementations ==========
/**
* Send text message to this client.
*/
@Override
public void sendText(String message) throws IOException {
if (nativeSession == null || !nativeSession.isOpen()) {
throw new IOException("WebSocket session is not open");
}
nativeSession.getBasicRemote().sendText(message);
}
/**
* Send binary data to this client.
*/
@Override
public void sendBinary(byte[] data) throws IOException {
if (nativeSession == null || !nativeSession.isOpen()) {
throw new IOException("WebSocket session is not open");
}
nativeSession.getBasicRemote().sendBinary(ByteBuffer.wrap(data));
}
/**
* Close this WebSocket connection.
*/
@Override
public void close() throws IOException {
if (nativeSession != null && nativeSession.isOpen()) {
nativeSession.close();
}
}
/**
* Close with status code and reason.
*/
@Override
public void close(int code, String reason) throws IOException {
if (nativeSession != null && nativeSession.isOpen()) {
CloseReason.CloseCode closeCode = CloseReason.CloseCodes.getCloseCode(code);
nativeSession.close(new CloseReason(closeCode, reason));
}
}
/**
* Check if connection is open.
*/
@Override
public boolean isOpen() {
return nativeSession != null && nativeSession.isOpen();
}
// ========== Jakarta WebSocket Event Bridge ==========
// These methods would be called by the Jakarta WebSocket endpoint to dispatch events
/**
* Called by Jakarta WebSocket endpoint when text message received.
* Bridges to our callback system.
*/
void onNativeTextMessage(String message) {
handleText(message);
}
/**
* Called by Jakarta WebSocket endpoint when binary message received.
* Bridges to our callback system.
*/
void onNativeBinaryMessage(byte[] data) {
handleBinary(data);
}
/**
* Called by Jakarta WebSocket endpoint when error occurs.
* Bridges to our callback system.
*/
void onNativeError(Throwable error) {
handleError(error);
}
/**
* Called by Jakarta WebSocket endpoint when connection closes.
* Bridges to our callback system.
*/
void onNativeClose(int code, String reason) {
handleClose(code, reason);
}
/**
* Endpoint instance that bridges Jakarta WebSocket callbacks into ServletWebSocketSession.
* Must be public for Jakarta WebSocket to instantiate it.
*/
public static final class BridgingEndpoint extends Endpoint {
private final ServletWebSocketSession wrapper;
public BridgingEndpoint(ServletWebSocketSession wrapper) {
this.wrapper = wrapper;
}
@Override
public void onOpen(jakarta.websocket.Session session, EndpointConfig config) {
wrapper.setNativeSession(session);
// Text messages - use explicit type registration for Jakarta WebSocket API
session.addMessageHandler(String.class, new MessageHandler.Whole<String>() {
@Override
public void onMessage(String message) {
wrapper.onNativeTextMessage(message);
}
});
// Binary messages - use explicit type registration for Jakarta WebSocket API
session.addMessageHandler(ByteBuffer.class, new MessageHandler.Whole<ByteBuffer>() {
@Override
public void onMessage(ByteBuffer bb) {
byte[] data = new byte[bb.remaining()];
bb.get(data);
wrapper.onNativeBinaryMessage(data);
}
});
}
@Override
public void onClose(jakarta.websocket.Session session, CloseReason closeReason) {
wrapper.onNativeClose(closeReason.getCloseCode().getCode(), closeReason.getReasonPhrase());
}
@Override
public void onError(jakarta.websocket.Session session, Throwable thr) {
wrapper.onNativeError(thr);
}
}
/**
* Creates a new websocket session and upgrades the HTTP response to a websocket.
*
* TODO: Implementation needed:
* 1. Get ServerContainer from ServletContext
* 2. Create ServerEndpointConfig programmatically
* 3. Call upgrade on the container
* 4. Wire up Jakarta WebSocket events to our callbacks (handleText, handleBinary, etc.)
*
* @param response the response to upgrade to a websocket
* @param route the route to upgrade to a websocket
* @param appSession the app session to attach to the websocket session
* @return the new websocket session
*/
public static ServletWebSocketSession create(ServletResponse response, String route, Session appSession) {
ServletRequest request = (ServletRequest) response.getRequest();
ServletWebSocketSession session = new ServletWebSocketSession(route, request.getRemoteAddress(), appSession);
HttpServletRequest httpReq = request.getHttpServletRequest();
HttpServletResponse httpResp = response.getHttpServletResponse();
// TODO: Perform the actual WebSocket upgrade here
// ServerContainer container = (ServerContainer) httpReq.getServletContext()
// .getAttribute(ServerContainer.class.getName());
// ... configure endpoint and upgrade ...
// 1) Get ServerContainer from ServletContext using standard Jakarta WebSocket attribute
Object attr = httpReq.getServletContext()
.getAttribute(ServerContainer.class.getName());
if (!(attr instanceof ServerContainer serverContainer)) {
throw new IllegalStateException(
"No jakarta.websocket.server.ServerContainer found in ServletContext. " +
"Did you initialize Jakarta WebSocket in Jetty? " +
"Ensure JettyWebSocketServletContainerInitializer is configured in JettyApp."
);
}
// 2) Create endpoint instance and ServerEndpointConfig that returns THIS instance
BridgingEndpoint endpoint = new BridgingEndpoint(session);
ServerEndpointConfig.Configurator configurator = new ServerEndpointConfig.Configurator() {
@Override
public <T> T getEndpointInstance(Class<T> endpointClass) {
return endpointClass.cast(endpoint);
}
};
ServerEndpointConfig sec = ServerEndpointConfig.Builder
.create(BridgingEndpoint.class, route) // path is required by the builder
.configurator(configurator)
.build();
// 3) Upgrade (this performs the handshake + switches protocols). :contentReference[oaicite:4]{index=4}
// Path params: pass empty unless you need them.
Map<String, String> pathParams = Collections.emptyMap();
try {
serverContainer.upgradeHttpToWebSocket(httpReq, httpResp, sec, pathParams);
} catch (IOException | DeploymentException e) {
// Make sure your response is sane if upgrade fails (often the container already wrote).
throw new RuntimeException("WebSocket upgrade failed", e);
}
return session;
}
}
@@ -1,22 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
/** Similar to a SAX interface used by parsers for XML and JSON to assemble DOM structures.
* Simply gets notified of events during parsing.
* @author amer
*/
public interface DecoderSink {
void beginDocument(Rec init);
Rec endDocument();
void beginElement(String name);
void endElement(String name);
void setKey(String name);
void setValue(CharSequence seq);
}
-158
View File
@@ -1,158 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/** Base class of meta objects.
* We use it to describe certain meta information. We derive from it Slot.
* We define keys list of slots on the header level to describe sub-slots.
*
* This class describes structure of Fields or Entities via the keys array of slots.
* Additionally we provide a number of methods to locate, set or get or remove or add slots.
* However slots could reside in other places such as base classes and so getOwnSlots will return a
* bare list of slots in this object while all other methods will take into account other sources.
* We do this to stay consistent at Rec level when Hdr inheritance comes into play.
*/
public class Hdr {
public static final int FLAG_ARRAY =0x0001;
public static final int FLAG_CHANGED =0x0002;
public static final int FLAG_STORABLE =0x0004;
public static final int FLAG_LOCKED =0x0008;
int flags;
String name;
String label;
Class<?> type;
final ArrayList<Slot> keys;
public Hdr(String name) {
this.name=name;
keys=new ArrayList<>();
}
public Hdr(String name,Class<?> type) {
this.name=name;
this.type=type;
keys=new ArrayList<>();
}
@Override
public String toString(){
StringBuilder ret=new StringBuilder();
ret.append(name).append(":");
ret.append("{")
.append("flags:").append(flags)
.append(",dim:").append(count())
.append("}");
return ret.toString();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLabel() {
return label!=null?label:name;
}
public void setLabel(String name) {
this.label = name;
}
public Class<?> getType() {
return type;
}
public void setType(Class<?> type) {
this.type = type;
}
public int getFlags(){
return flags;
}
public Hdr raiseFlags(int f){
flags|=f;
return this;
}
public Hdr clearFlags(int f){
flags&=~f;
return this;
}
public boolean checkFlags(int f){
return (flags & f)!=0;
}
public <T extends Hdr> T castAs(Class<T> clazz){
return clazz.cast(this);
}
public List<Slot> getOwnSlots(){
return keys;
}
public boolean isOwned(Slot s){
return keys.contains(s);
}
public Iterator<Slot> iterator(int offset){
return keys.listIterator(offset);
}
public int indexOf(String name){
return indexOf(name,0);
}
public int indexOf(String name,int ofs){
Iterator<Slot> it=iterator(ofs);
int index=-1;
while(it.hasNext()){
index+=1;
Slot e=it.next();
//if(e.getName().equalsIgnoreCase(name)) return index;
if(e.equals(name)) return index;
}
return -1;
}
public int indexOf(Slot s,int ofs){
Iterator<Slot> it=iterator(ofs);
int index=-1;
while(it.hasNext()){
index+=1;
Slot e=it.next();
if(e==s) return index;
}
return -1;
}
public Slot makeSlot(String name){
return new Slot(name);
}
/**
* this version will get or create a slot by given name.
* @param name slot name
* @param make wether to create if not present
* @return Slot or null.
*/
public Slot getSlot(String name,boolean make){
int index=indexOf(name);
if(index<0){
return make?makeSlot(name):null;
}else{
return getSlot(index);
}
}
public Slot getSlot(int pos){
return keys.get(pos);
}
public Hdr removeSlot(int pos){
keys.remove(pos);
return this;
}
public Hdr addSlot(Slot s){
keys.add(s);
return this;
}
public Hdr setSlot(int index,Slot s){
keys.set(index,s);
return this;
}
public int count(){
return keys.size();
}
}
-36
View File
@@ -1,36 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
import java.io.IOException;
/**
* Static methods related to JSON format.
*/
public class JSON {
private JSON(){
}
public static final Rec reads(CharSequence seq){
JSONDecoder dec=new JSONDecoder();
dec.beginDocument();
dec.parse(0, seq);
return dec.endDocument();
}
public static final void writes(Rec rec,Appendable sink) throws IOException{
JSONEncoder.encode(rec, sink);
}
public static final String toString(Rec rec){
StringBuffer buf=new StringBuffer();
try {
writes(rec,buf);
} catch (IOException e) {
}
return buf.toString();
}
}
@@ -1,333 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
import java.util.LinkedList;
import com.reliancy.util.Tokenizer;
import com.reliancy.util.Handy;
/** Special class which will tokenize string according to rules for JSON and feed the info to a listener.
* TODO: reuse headers in an array if same structure
* @author amer
*/
public class JSONDecoder implements TextDecoder,DecoderSink {
DecoderSink handler;
String[] inBody;
String[] sets;
String lastToken=null;
StringBuilder out = new StringBuilder();
public JSONDecoder(DecoderSink h){
handler=h;
String delimChars="{}[],;:=";
String escapeChars="'\"";
String whiteChars=" \t\r\f\n";//" \t\r\f\n";
inBody = new String[]{delimChars,escapeChars,whiteChars};
sets=inBody;
}
public JSONDecoder(){
this(null);
handler=this;
}
@Override
public int parse(int offset,CharSequence in){
int noffset=0;
while((noffset = Tokenizer.nextToken(offset, in, out, sets))!=offset){
offset=noffset;
if(out.length()==0) continue;
String token=out.toString();
out.setLength(0);
if("{".equals(token)){
if(lastToken!=null){
if(lastToken.startsWith("/*") || lastToken.startsWith("//")){
handler.setValue(lastToken); // support comments in our stream
}else{
handler.setKey(lastToken); // we consider string before { a key or name unless comment
}
lastToken=null;
}
handler.beginElement("object");
}else if("}".equals(token)){
if(lastToken!=null){
handler.setValue(lastToken);
lastToken=null;
}
handler.endElement("object");
}else if("[".equals(token)){
if(lastToken!=null){
handler.setValue(lastToken);
lastToken=null;
}
handler.beginElement("array");
}else if("]".equals(token)){
if(lastToken!=null){
handler.setValue(lastToken);
lastToken=null;
}
handler.endElement("array");
}else if(",".equals(token) || ";".equals(token)){
if(lastToken!=null){
handler.setValue(lastToken);
lastToken=null;
}
}else if(":".equals(token) || "=".equals(token)){
if(lastToken!=null){
handler.setKey(lastToken);
lastToken=null;
}
}else{
lastToken=token;
}
}
if(lastToken!=null){
handler.setValue(lastToken);
lastToken=null;
}
return offset;
}
Slot KEY=new Slot("__key",String.class);
/** We use a stack structure to manage recusion. */
LinkedList<Rec> stack=new LinkedList<Rec>();
/** will not add white space only nodes. */
boolean whitespaceIgnored=true;
boolean entitycharsIgnored=false;
public boolean isWhitespaceIgnored() {
return whitespaceIgnored;
}
public void setWhitespaceIgnored(boolean whitespaceIgnored) {
this.whitespaceIgnored = whitespaceIgnored;
}
public boolean isEntitycharsIgnored() {
return entitycharsIgnored;
}
public void setEntitycharsIgnored(boolean entitycharsIgnored) {
this.entitycharsIgnored = entitycharsIgnored;
}
public Rec getRoot() {
return stack.getLast();
}
public Rec getSubject(){
if(stack.isEmpty()) return null;
return stack.getFirst();
}
public void pushSubject(Rec n){
stack.push(n);
}
public Rec popSubject(){
Rec child=stack.pop();
Rec parent=getSubject();
if(parent==null) return child;
if(parent.isArray()){
parent.add(child);
}else{
String key=(String) parent.get(KEY,null);
Slot keyslot=parent.getSlot(key);
parent.remove(KEY).set(keyslot,child);
// if array and has key it should bomb
//parent.setArray(false);
}
return child;
}
public void beginDocument() {
beginDocument(null);
}
@Override
public void beginDocument(Rec init) {
sets=inBody;
out.setLength(0);
lastToken=null;
stack.clear();
Rec arr=new Obj(true);
stack.push(arr);
//System.out.println("BeginDoc");
}
@Override
public Rec endDocument() {
// need to set the actual parent
while(stack.getFirst()!=stack.getLast()){
popSubject();
}
// now adjust the root if it is array with only one child - one we added in start document as first element
Rec root=getSubject();
if(root.isArray() && root.count()==1 && root.get(0) instanceof Rec){
// ok we collapse our array from above - since we only have one object
Object bb=root.get(0);
Rec b=(Rec)bb ;
popSubject();
pushSubject(b);
}
//System.out.println("EndDoc");
return getRoot();
}
@Override
public void beginElement(String name) {
Rec element=new Obj("array".equals(name));
//element.setAttr(0);
pushSubject(element);
//System.out.println("BeginElement:"+name);
}
@Override
public void endElement(String name) {
// check if the correct end element is sent
Rec sub=this.getSubject();
if(!sub.isArray()) sub.remove(KEY);
// finally pop the root
popSubject();
//System.out.println("EndElement:"+name);
}
@Override
public void setKey(String name) {
Rec sub=this.getSubject();
String key=(String) sub.get(KEY,null);
if(key!=null){
// something is wrong - our tokizer might have ignored escape char or input has forgotten a delimiter
// we try to split name because it would contain key and value merged
int split=0;
if(name.startsWith("\"")) split=name.indexOf('\"', 1);
if(name.startsWith("'")) split=name.indexOf('\'', 1);
String val=name.substring(0,split+1);
setValue(val);
name=name.substring(split+1);
}
int start=0;int stop=name.length();
while(start<stop && (name.charAt(start)=='"' || name.charAt(start)=='\'')) start++;
while(start<stop && (name.charAt(stop-1)=='"' || name.charAt(stop-1)=='\'')) stop--;
sub.set(KEY, name.subSequence(start, stop));
//System.out.println("BeginAttribute:"+name);
}
@Override
public void setValue(CharSequence seq) {
if(seq==null) return;
Rec sub=this.getSubject();
String key=(String) sub.get(KEY,null);
if(key==null){
if(isWhitespaceIgnored() && Handy.isEmpty(seq)){
// skip empty strings
return;
}
// now key we are adding to body
Object val=interpretString(seq);
sub.add(val);
}else{
// we are setting attribute
Object val=interpretString(seq);
Slot keyslot=sub.getSlot(key);
sub.remove(KEY).set(keyslot,val);
// it should bomb if array and comes with key
//sub.setArray(false); // if it needs to be array why does it have a key
}
//System.out.println("Data:"+seq);
}
public Object interpretString(CharSequence seq){
int start=0;int stop=seq.length();
while(start<stop && seq.charAt(start)=='"' && seq.charAt(stop-1)=='"'){
start++;
stop--;
}
if(start==0 && stop==seq.length()){
// we do not trim single quotes unless double are missing
while(start<stop && seq.charAt(start)=='\'' && seq.charAt(stop-1)=='\''){
start++;
stop--;
}
}
seq=seq.subSequence(start, stop);
Object val=seq;
if(start==0){
String sVal=String.valueOf(seq);
// we did not have quotes - so try to interpet a few things
if("null".equalsIgnoreCase(sVal)){
val=null;
}else
if("true".equalsIgnoreCase(sVal)){
val=Boolean.TRUE;
}else
if("false".equalsIgnoreCase(sVal)){
val=Boolean.FALSE;
}else
if(Handy.isNumeric(sVal)){
if (sVal.indexOf(".") >= 0) {
val = Double.parseDouble(sVal);
} else {
val = Integer.parseInt(sVal);
}
}else if(this.isEntitycharsIgnored()==false && seq!=null && seq.length()>0){
// maybe it is a string after all
val=unescape(seq);
}
}else if(this.isEntitycharsIgnored()==false && seq!=null && seq.length()>0){
// we had quotes so lets decode escaed chars
val=unescape(seq);
}
return val;
}
public static CharSequence unescape(CharSequence str) {
StringBuilder buf = null;
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
if (ch == '\\' && i < (str.length() - 1)) {
i = i + 1;
char ch2 = str.charAt(i);
switch (ch2) {
case '"':
if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):"");
buf.append("\"");
break;
case '\\':
if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):"");
buf.append("\\");
break;
case '/':
if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):"");
buf.append("/");
break;
case 'b':
if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):"");
buf.append("\b");
break;
case 'f':
if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):"");
buf.append("\f");
break;
case 'n':
if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):"");
buf.append("\n");
break;
case 'r':
if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):"");
buf.append("\r");
break;
case 't':
if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):"");
buf.append("\t");
break;
default:
if(buf!=null) buf.append(ch);
}
} else {
if(buf!=null) buf.append(ch);
}
}
return buf!=null?buf.toString():str;
}
}
@@ -1,294 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
import java.io.IOException;
import java.util.List;
import java.util.Map;
public class JSONEncoder{
public JSONEncoder(){
}
/**
* We encode into an appendable various primitives and Rec.
* If appendable null then we just compute expected size.
* keys are not escaped they better not contain any special chars.
* values are quoted and escaped unless we detect a string that looks like a json object those are passed thru.
* in the past we tried to deduce if quoting was needed, but this is not the place to do so because we do not know how many times
* value was escaped so the only thing we can assume is that it needs to be escaped. So feeding a value that is quoted and
* escaped will return back on parse the same and will need to dequoted and descaped once more but that shoudl work fine with
* whoever quoted it in the upstream in the first place.
* @param val property value
* @param o encoding output
* @return length in characters of encoded result
* @throws IOException
*/
public static int encode(Object val,Appendable o) throws IOException {
int len = 0;
/*
// first key
if (key != null) {
if (o != null) {
o.append('"').append(key).append("\":");
}
len += 3 + key.length();
}
*/
// now value
if (val instanceof Object[]) {
Object[] valval = (Object[]) val;
if (o != null) {
o.append('[');
}
int index = 0;
for (Object obj : valval) {
if (index++ > 0) {
len += 1;
if (o != null) {
o.append(",");
}
}
len += encode(obj, o);
}
if (o != null) {
o.append(']');
}
len += 2;
} else if (val instanceof List) {
List<?> valval = (List<?>) val;
if (o != null) {
o.append('[');
}
int index = 0;
for (Object obj : valval) {
if (index++ > 0) {
len += 1;
if (o != null) {
o.append(",");
}
}
len += encode(obj, o);
}
if (o != null) {
o.append(']');
}
len += 2;
} else if (val instanceof Map) {
len+=encodeMap((Map<?,?>)val,o);
} else if (val instanceof Rec) {
len += encodeRec((Rec) val, o);
} else if (val instanceof Number || val instanceof Boolean) {
String str = val.toString();
if (o != null) {
o.append(str);
}
len += str.length();
}else if(val instanceof int[]){
int[] valval = (int[]) val;
if (o != null) {
o.append('[');
}
int index = 0;
for (int obj : valval) {
if (index++ > 0) {
len += 1;
if (o != null) {
o.append(",");
}
}
if(o!=null) o.append(String.valueOf(obj));
len += 1;
}
if (o != null) {
o.append(']');
}
len += 2;
}else if(val instanceof float[]){
float[] valval = (float[]) val;
if (o != null) {
o.append('[');
}
int index = 0;
for (float obj : valval) {
if (index++ > 0) {
len += 1;
if (o != null) {
o.append(",");
}
}
if(o!=null) o.append(String.valueOf(obj));
len += 1;
}
if (o != null) {
o.append(']');
}
len += 2;
}else if (val instanceof Object) {
String str = val.toString();
boolean jsontxt = false;
jsontxt |= str.length() > 0 && str.startsWith("{") && str.endsWith("}");
jsontxt |= str.length() > 0 && str.startsWith("[") && str.endsWith("]");
//boolean quoted=str.length() > 1 && str.startsWith("\"") && str.endsWith("\"");
// embedded json is not quoted and not escaped
// all other text is quoted otherwise we will prevent quoted quotes (those would be swallowed)
// we will not try to be smart if someone added an item that is quoted already it will be escaped and queotes retained
// we must be consistent so that repeated parse and encode works and not too smart here
// we need to put quotes around unless
if (!jsontxt) {
str = escape(str).toString();
if (o != null) {
o.append('"');
}
len += 1;
}
if (o != null) {
o.append(str);
}
len += str.length();
if (!jsontxt) {
if (o != null) {
o.append('"');
}
len += 1;
}
} else if (val == null) {
String str = "null";
if (o != null) {
o.append(str);
}
len += str.length();
}
return len;
}
public static int encodeMap(Map<?,?> valval,Appendable o) throws IOException{
int len=0;
if (o != null) {
o.append('{');
}
int index = 0;
for (Object obj : valval.keySet()) {
if (index++ > 0) {
len += 1;
if (o != null) {
o.append(",");
}
}
String key=obj.toString();
if (o != null) {
o.append('"').append(key).append("\":");
}
len += 3 + key.length();
len += encode(valval.get(obj), o);
}
if (o != null) {
o.append('}');
}
len += 2;
return len;
}
public static int encodeRec(Rec val,Appendable o) throws IOException{
int len=0;
if (o != null) {
o.append(val.isArray()?"[":"{");
}
for (int i=0;i<val.count();i++) {
Slot k=val.getSlot(i);
Object v=val.get(i);
if (i > 0) {
len += 1;
if (o != null) {
o.append(",");
}
}
if(k!=null){
String key=k.getName();
if (o != null) {
o.append('"').append(key).append("\":");
}
len += 3 + key.length();
}
len += encode(v, o);
}
if (o != null) {
o.append(val.isArray()?"]":"}");
}
len += 2;
return len;
}
/**
* @param str
* @return true if the string includes any of the special chars.
*/
public static boolean needsEscaping(String str) {
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
switch (ch) {
case '"':
case '\\':
case '/':
case '\b':
case '\f':
case '\n':
case '\r':
case '\t':
return true;
}
}
return false;
}
/**
* this helper method handle quotes and control chars.
* @param str input string
* @return output after encoding special chars
*/
public static CharSequence escape(CharSequence str) {
StringBuilder buf = null;
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
switch (ch) {
case '"':
if(buf==null) buf=new StringBuilder(str.subSequence(0,i));
buf.append("\\\"");
break;
case '\\':
if(buf==null) buf=new StringBuilder(str.subSequence(0,i));
buf.append("\\\\");
break;
case '/':
if(buf==null) buf=new StringBuilder(str.subSequence(0,i));
buf.append("\\/");
break;
case '\b':
if(buf==null) buf=new StringBuilder(str.subSequence(0,i));
buf.append("\\b");
break;
case '\f':
if(buf==null) buf=new StringBuilder(str.subSequence(0,i));
buf.append("\\f");
break;
case '\n':
if(buf==null) buf=new StringBuilder(str.subSequence(0,i));
buf.append("\\n");
break;
case '\r':
if(buf==null) buf=new StringBuilder(str.subSequence(0,i));
buf.append("\\r");
break;
case '\t':
if(buf==null) buf=new StringBuilder(str.subSequence(0,i));
buf.append("\\t");
break;
default:
if(buf!=null) buf.append(ch);
}
}
return buf!=null?buf:str;
}
}
-160
View File
@@ -1,160 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
import java.util.ArrayList;
import java.util.List;
/**
* Default implementation of a Rec.
* We separate keys and values because Obj could just be an array.
* If object is declated an array keys are nonexistant and rec related methods will return null or crash.
* Our setters return this object to main the calls chainable.
* Also positional calls accept negative values which reference from end backward.
*/
public class Obj implements Rec{
final List<Object> values;
final Hdr meta;
public Obj() {
values=new ArrayList<>();
meta=new Slot(null);
}
public Obj(boolean is_array) {
values=new ArrayList<>();
meta=new Slot(null);
if(is_array) meta.raiseFlags(Hdr.FLAG_ARRAY);
}
public Obj(List<Slot> k,List<Object> v) {
values=v;
meta=new Slot(null);
meta.keys.addAll(k);
}
/**
* This ctor is reserved for derivations with fixed slot definitions.
* This constructor will inspect static Slot members and construct keys that way
* if meta named.
* @param def
*/
protected Obj(Hdr def){
values=new ArrayList<>();
meta=def;
}
@Override
public String toString(){
StringBuilder buf=new StringBuilder();
toString(buf);
return buf.toString();
}
public int toString(StringBuilder buf){
boolean is_arr=isArray();
int length0=buf.length();// length before anything done
//StringBuffer indent=new StringBuffer(); // detect indent
//for(int i=length0;i>0 && Character.isWhitespace(buf.charAt(i));i--){
// indent.append(buf.codePointAt(i));
//}
buf.append(is_arr?"[":"{");
if(is_arr){
for(int pos=0;pos<count();pos++){
if(pos>0) buf.append(",");
Object val=this.get(pos);
if(val instanceof Obj) ((Obj)val).toString(buf);
else if(val!=null) buf.append(val.toString());
else buf.append("null");
}
}else{
for(int pos=0;pos<count();pos++){
if(pos>0) buf.append(",");
Slot s=getSlot(pos);
buf.append(s.getName()+":");
Object val=this.get(pos);
if(val!=null) s.toString(val,buf); else buf.append("null");
}
}
buf.append(is_arr?"]":"}");
return buf.length()-length0;
}
@Override
public Hdr meta(){
return meta;
}
@Override
public boolean isArray(){
return meta==null || meta.checkFlags(Hdr.FLAG_ARRAY);
}
@Override
public int count() {
return values.size();
}
@Override
public Rec set(int pos, Object val) {
if(pos<0) pos=count()+pos;
values.set(pos,val);
return this;
}
@Override
public Object get(int pos) {
if(pos<0) pos=count()+pos;
return values.get(pos);
}
@Override
public Rec add(Object val) {
values.add(val);
if(!isArray()) meta.addSlot(new Slot("arg"+count(),Object.class));
return this;
}
@Override
public Rec remove(int s) {
values.remove(s);
if(!isArray()) meta.removeSlot(s);
return this;
}
@Override
public Rec set(Slot s, Object val) {
if(s==null) throw new IllegalArgumentException("invalid key provided");
if(isArray()) throw new IllegalStateException("array not mappable with:"+s.getName());
int index=s.getPosition(); // try slot position
if(index<0) index=meta.indexOf(s.getName());// fall back to search if slot not set
if(index<0){
values.add(val);
meta.addSlot(s);
}else{
values.set(index,val);
meta.setSlot(index,s);
}
return this;
}
/**
* Returns value by slot key.
* If the underlying rec is a vec/array this method might work if slot is positioned else it will
* return def value.
*/
@Override
public Object get(Slot s, Object def) {
if(s==null) throw new IllegalArgumentException("invalid key provided");
//if(keys==null) throw new IllegalStateException("array not mappable with:"+s.getName());
int index=s.getPosition(); // try slot position
if(index<0 && !isArray()) index=meta.indexOf(s.getName());// fall back to search if slot not set
return index<0?def:values.get(index);
}
@Override
public Rec remove(Slot s) {
int index=s.getPosition(); // try slot position
if(index<0 && !isArray()) index=meta.indexOf(s.getName());// fall back to search if slot not set
if(index>=0) remove(index);
return this;
}
}
-27
View File
@@ -1,27 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
/**
* A record representation like in JSON.
* This is either an array or a map of fields.
* Each field definition we call a slot.
*/
public interface Rec extends Vec{
public Rec set(Slot s,Object val);
public Object get(Slot s,Object def);
public Rec remove(Slot s);
public default Slot getSlot(String name){
Hdr m=meta();
return m!=null?m.getSlot(name,true):null;
}
public default Slot getSlot(int pos){
Hdr m=meta();
return m!=null?m.getSlot(pos):null;
}
}
-73
View File
@@ -1,73 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
/**
* Slot is a definition of a value start with the name.
* We use it to define columns/fields of records.
* It is also used as header of actual records.
*/
public class Slot extends Hdr {
public static interface Initializer{
Object getInitalValue(Slot s,Rec rec);
}
public static final Initializer DEFAULT_INITIALIZER=new Initializer(){
public Object getInitalValue(Slot s,Rec rec) {return s.getInitValue();}
};
int position;
Object defaultValue;
Initializer initValue;
public Slot(String name){
this(name,Object.class);
}
public Slot(String name,Class<?> type){
super(name,type);
this.position=-1;
this.initValue=DEFAULT_INITIALIZER;
}
public boolean equals(String str){
return name.equalsIgnoreCase(str);
}
public int getPosition() {
return position;
}
public Slot setPosition(int position) {
this.position = position;
return this;
}
public Object getInitValue() {
return defaultValue;
}
public Slot setInitValue(Object defaultValue) {
this.defaultValue = defaultValue;
return this;
}
public Initializer getInitVia() {
return initValue;
}
public Slot setInitVia(Initializer initValue) {
this.initValue = initValue;
return this;
}
public int toString(Object val, StringBuilder buf) {
int length0=buf.length();
if(val instanceof Obj) ((Obj)val).toString(buf);
else if(val!=null) buf.append(val.toString());
else buf.append("null");
return buf.length()-length0;
}
public Object get(Rec r,Object def){
return r.get(this, def);
}
public Slot set(Rec r,Object val){
r.set(this, val);
return this;
}
}
@@ -1,19 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
/** An interface used in parser implementation.
*
* @author amer
*/
public interface TextDecoder {
void beginDocument(Rec init);
Rec endDocument();
public int parse(int offset,CharSequence in);
}
-25
View File
@@ -1,25 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
/**
* dimensioned container of values.
* Our setters return this object to make the calls chainable.
* Also positional calls accept negative values which reference from end backward.
*
*/
public interface Vec {
public default boolean isArray(){
return meta().checkFlags(Hdr.FLAG_ARRAY);
}
public Hdr meta();
public int count();
public Rec set(int pos,Object val);
public Object get(int pos);
public Rec add(Object val);
public Rec remove(int s);
}
@@ -1,138 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.util;
import java.util.HashMap;
/** One exception to rule them all.
* This exception works with ResultCode and represents and instance with context information.
* If a ResultCode is deemed parametric then we use provided parameters to update it when generating a message.
*
* @author amer
*/
public class CodeException extends RuntimeException {
protected final int code;
protected final HashMap<String,Object> context=new HashMap<>();
public CodeException(int code) {
this.code = code;
}
public CodeException(Throwable cause, int code) {
super(cause);
this.code = code;
}
@Override
public String toString(){
return getMessage();
}
public int getCode() {
return code;
}
public ResultCode getResultCode(){
return ResultCode.get(code);
}
@Override
public String getMessage() {
ResultCode rcode=getResultCode();
if(rcode!=null){
boolean wrapped=(rcode.getCode()==ResultCode.FAILURE);
String msg=rcode.getMessage();
if(msg.contains("$")){
for(String key:context.keySet()){
Object obj=context.get(key);
if(obj==null) continue;
String val=String.valueOf(obj);
msg=msg.replaceAll("\\$\\{"+key+"\\}",val);
}
}else if(this.getCause()!=null && wrapped){
msg=CodeException.getUserMessage(this.getCause());
}
return msg;
}else{
return "("+String.format("%08X", code)+")";
}
}
@SuppressWarnings("unchecked")
public <T> T get(String name) {
return (T)context.get(name);
}
public CodeException put(String name, String value) {
context.put(name, value);
return this;
}
public static CodeException wrap(Throwable exception) {
if (exception instanceof CodeException) {
CodeException se = (CodeException)exception;
return se;
} else {
return new CodeException(exception,ResultCode.FAILURE);
}
}
public static String getUserMessage(Throwable ex,Object context) {
return getUserMessage(ex);
}
public static String getUserMessage(Throwable ex) {
StringBuilder buf=new StringBuilder();
fillUserMessage(ex,buf,null);
return buf.toString();
}
public static Throwable fillUserMessage(Throwable ex,StringBuilder msg,StringBuilder title) {
Throwable c = ex;
//System.out.println(">>>"+c+"/"+c.getCause());
while(c.getCause()!=null){
Throwable cc= c.getCause();
if(c.getMessage()==null){
c=cc;continue;
}
String cMsg=c.getMessage();
String ccMsg=cc.getMessage();
//System.out.println("!!!"+cMsg+"/"+c.getClass().getName()+"/"+cc.getClass().getName());
boolean wrapped=(c instanceof CodeException) && ((CodeException)c).getCode()==ResultCode.FAILURE;
boolean plain_at=cMsg.equals(c.getClass().getName());
boolean plain_sub=cMsg.equals(cc.getClass().getName());
boolean same_msg=cMsg.equalsIgnoreCase(ccMsg);
//System.out.println("\t"+plain_sub+"#"+cc+"$"+cc.getCause()+"*"+cc.getMessage());
if(plain_at || plain_sub || cMsg.startsWith(cc.getClass().getName()+":") || same_msg || wrapped){
c=cc;
}else{
break;
}
}
//System.out.println("CC:"+c);
// take care of title
String _title=c.getClass().getSimpleName();
if(c instanceof CodeException){
CodeException cc=(CodeException) c;
if(cc.getCause()!=null){
_title=cc.getClass().getSimpleName();
}else{
// we do not have a cause
int code=cc.getCode();
ResultCode rcode=ResultCode.get(code);
if(rcode!=null) _title=rcode.getSource();
}
}
if(title!=null) title.append(_title);
// now take care of detail
String _msg=c.getLocalizedMessage();
if(_msg==null || _msg.trim().isEmpty()){
_msg=c.getClass().getSimpleName();
StackTraceElement[] se=c.getStackTrace();
if(se!=null && se.length>0) _msg+="\n\t at "+se[0].toString();
}
String prefString="Exception:";
String prefString2="Error:";
int prefix=_msg.lastIndexOf(prefString);
if(prefix<0) prefix=_msg.lastIndexOf(prefString2);
if(prefix>0 && _msg.substring(0, prefix).contains(".")) _msg=_msg.substring(prefix+prefString.length());
if(msg!=null) msg.append(_msg);
return c;
}
}
-593
View File
@@ -1,593 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
/**
* Common utility methods.
*/
public final class Handy {
public static final String WHITE=" \t\r\f\n";
/** place left-right around verb.
* @param verb body of text
* @param left to add left of verb
* @param left to add right of verb
* @return adjusted text
* */
public static String wrap(String verb, String left, String right) {
if(verb==null) verb="";
if(verb.startsWith(left) && verb.endsWith(right)) return verb;
return left+verb.trim()+right;
}
/** remove left-right around verb.
* @param verb body of text
* @param left to remove left of verb
* @param left to remove right of verb
* @return adjusted text
**/
public static String unwrap(String verb, String left, String right) {
if(verb==null) return verb;
String ret=verb.trim();
if(ret.startsWith(left) && ret.endsWith(right)){
ret=ret.substring(left.length());
ret=ret.substring(0,ret.length()-right.length());
}
return ret;
}
/** remove any chars elements from left of verb.
* @param verb body of text
* @return adjusted text
*/
public static String trimLeft(String verb,String chars){
while(verb.length()>0 && chars.indexOf(verb.charAt(0))!=-1){
verb=verb.substring(1);
}
return verb;
}
/** remove any chars elements from right of verb.
* @param verb body of text
* @return adjusted text
*/
public static String trimRight(String verb,String chars){
while(verb.length()>0 && chars.indexOf(verb.charAt(verb.length()-1))!=-1){
verb=verb.substring(0,verb.length()-1);
}
return verb;
}
/** remove any chars elements from right and right of verb.
* @param verb body of text
* @return adjusted text
*/
public static String trimBoth(String verb,String chars){
verb=trimLeft(verb, chars);
verb=trimRight(verb, chars);
return verb;
}
/** remove any chars elements from right and right of verb symetrically. trims whitespace first. */
public static String trimEvenly(String verb,String chars){
verb=trimBoth(verb," \t\n\r\f");
while(verb.length()>1){
char left=verb.charAt(0);
char right=verb.charAt(verb.length()-1);
if(left!=right) break; // left-right not even
if(chars.indexOf(left)<0) break; // even but not in chars list
verb=verb.substring(1,verb.length()-1);
}
return verb;
}
public static <T> T nz(T val, T def){
return val!=null?val:def;
}
/** Convert incoming value to an expected class.
* @param clazz expected class
* @param val observed value
* @return val converted to type clazz.
*/
public static Object normalize(Class<?> clazz, Object val ) {
if(val==null) return null; // we are null
if(clazz.isAssignableFrom(val.getClass())) return val; // we are assignable
if(val instanceof String){
String value=(String) val;
if(value.isEmpty() || value.equals("''") || value.equals("\"\"")) return null;
if( Boolean.class==( clazz ) || boolean.class==( clazz ) ) return Boolean.parseBoolean( value );
if( Byte.class==( clazz ) || byte.class==( clazz ) ) return Byte.parseByte( value );
if( Short.class==( clazz ) || short.class==( clazz ) ) return Short.parseShort( value );
if( Integer.class==( clazz ) || int.class==( clazz ) ) return Integer.parseInt( value );
if( Long.class==( clazz ) || long.class==( clazz )) return Long.parseLong( value );
if( Float.class==( clazz ) || float.class==( clazz ) ) return Float.parseFloat( value );
if( Double.class==( clazz ) || double.class==( clazz )) return Double.parseDouble( value );
}
if(clazz==String.class || clazz==CharSequence.class){
return String.valueOf(val);
}
return val;
}
/**
* This method is a bit more complex because it locks onto two delimiters one for grouping and other
* for decimal point and chooses those from a list of [space],'`. which are used all over the world in different places.
* Returns true if the string only contains digits and numeric characters.
* This should match 1000000 also 1,000,000 and also 1,000,000.00 but it is still not possible to differentiate between 1,000 =1000 in us
* from 1000,00 whch is used in europe. So it is difficult to normalize the string so it could process any number.
* @param str string to test
* @return trie if string looks numeric or is null/empty
*/
public static final boolean isNumeric(String str){
int strLen;
if (str == null || (strLen = str.length()) == 0) {
return true;
}
String delims=" ,.'`";
int delimNumber=0; // 0 we load, 1 we let one more, 2 we exit on second occurance
char delimUsed=0; // char used as delim
boolean delimLast=false;
int digitCount=0;
for (int i = 0; i < strLen; i++) {
char ch=str.charAt(i);
boolean accept=Character.isDigit(ch);
if(accept) digitCount++;
accept=accept || (ch=='-' && i==0);
accept=accept || (ch=='+' && i==0);
if(delims.indexOf(ch)>=0){
accept=!delimLast; // prevent delims following each other
delimLast=true;
if(delimNumber==0){
delimNumber=1;
delimUsed=ch;
}else if(delimNumber==1){
if(delimUsed!=ch) delimNumber=2;
delimUsed=ch;
}else{
// we have seen two different delim and whatever is coming here is breaking numeric format like second delim second time or some otehr
accept=false;
}
}else{
delimLast=false;
}
if(!accept) return false;
}
return digitCount>0;
}
/**
* @return true if the string is null, empty or contains only white space.
*/
public static boolean isBlank(CharSequence str) {
int strLen;
if (str == null || (strLen = str.length()) == 0) {
return true;
}
for (int i = 0; i < strLen; i++) {
if ((Character.isWhitespace(str.charAt(i)) == false)) {
return false;
}
}
return true;
}
/**
* Provides a unified notion of what constitutes an empty value.
* For any object if it is null.
* For string also if it is blank
* For arrays and lists and collection and maps also if no entries or keys exist.
* @param value anything
* @return true if any of the above matches
*/
public static boolean isEmpty(Object value){
if(value==null) return true;
if(value instanceof CharSequence){
return isBlank((CharSequence)value);
}
Class<?> cls=value.getClass();
if(cls.isArray()) {
if(value instanceof Object[]){
Object[] arr=(Object[]) value;
if(arr.length==0) return true;
for(int i=0;i<arr.length;i++) if(isEmpty(arr[i])==false) return false;
return true;
}if(value instanceof byte[]){
return ((byte[])value).length==0;
}else if(value instanceof short[]){
return ((short[])value).length==0;
}else if(value instanceof int[]){
return ((int[])value).length==0;
}else if(value instanceof long[]){
return ((long[])value).length==0;
}else if(value instanceof float[]){
return ((float[])value).length==0;
}else if(value instanceof double[]){
return ((double[])value).length==0;
}else{
return false;
}
}
if(value instanceof Collection){
Collection<?> c=(Collection<?>)value;
if(c.isEmpty()) return true;
for(Object o:c){
if(isEmpty(o)==false) return false;
}
return true;
}
return false;
}
/** Attempts to take a compact string and beautify it.
* Will uppercase the first letter. Will also expand CamelCase.
* Also will replace _ with empty space.
* @param str
* @return nicely formatted string ready for display
*/
public static final String prettyPrint(String str){
if(str==null) return "";
boolean fix=false;
char prevCh=0;
if(str.startsWith("org.") || str.startsWith("net.") || str.startsWith("com.") || str.startsWith("java.")){
str=str.substring(1+str.lastIndexOf('.')); // we strip class name paths
}
for(int i=0;i<str.length();i++){
char currCh=str.charAt(i);
if(i==0 && Character.isLowerCase(currCh)) fix=true;
if(Character.isLowerCase(prevCh) && Character.isUpperCase(currCh)) fix=true;
if(Character.isUpperCase(prevCh) && Character.isUpperCase(currCh) && i<(str.length()-1) && Character.isLowerCase(str.charAt(i+1))) fix=true;
if(!Character.isLetter(currCh)) fix=true;
prevCh=currCh;
}
if(!fix) return str;
StringBuilder bufs=new StringBuilder();
boolean toUC=false;
for(int i=0;i<str.length();i++){
char currCh=str.charAt(i);
if(currCh=='_') currCh=' ';
prevCh=bufs.length()>0?bufs.charAt(bufs.length()-1):currCh;
if(Character.isWhitespace(currCh)){
if(!Character.isWhitespace(prevCh)){
bufs.append(' ');
}
continue; // ignore repeated whitespace otherwise emit space
}
if(bufs.length()==0){
toUC=true;
}else if((!Character.isUpperCase(prevCh) && ("-+/%*".indexOf(prevCh)==-1 || Character.isLetter(prevCh))) && Character.isUpperCase(currCh)){
// non uc (a not one of operands) behind, uc ahead
bufs.append(" ");
}else if(Character.isLetter(prevCh) && Character.isDigit(currCh)){
// letter behind, digit ahead
bufs.append(" ");
}else if(Character.isUpperCase(prevCh) && Character.isUpperCase(currCh) && i<(str.length()-1) && Character.isLowerCase(str.charAt(i+1))){
// behind me uppercase infrom uppercase then lowercase
bufs.append(" ");
}
bufs.append(toUC?Character.toUpperCase(currCh):currCh);
toUC=false;
}
while(bufs.length()>0 && Character.isWhitespace(bufs.charAt(bufs.length()-1))){
// trims whitespace from end
bufs.setLength(bufs.length()-1);
}
return bufs.toString();
}
/** Attempts to take a user string and compact it to camel case.
* @param value more or less presentable string
* @return nicely compact string
*/
public static String toCamelCase(String value) {
if(value==null || value.trim().isEmpty()) return "";
StringBuilder sb = new StringBuilder();
//final char delimChar = ' ';
boolean flip = false;
for (int charInd = 0; charInd < value.length(); charInd++) {
char ch = value.charAt(charInd);
if (Character.isWhitespace(ch)) {
flip = true;
}else if(flip){
flip = false;
if(ch==Character.toLowerCase(ch)) sb.append("_");
sb.append(ch);
}else{
sb.append(ch);
}
}
return sb.toString();
}
public static byte[] deflate(byte[] content) throws IOException{
Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION,true);
deflater.setInput(content);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(content.length);
deflater.finish();
byte[] buffer = new byte[1024];
while (!deflater.finished()) {
int count = deflater.deflate(buffer); // returns the generated code... index
outputStream.write(buffer, 0, count);
}
outputStream.close();
byte[] output = outputStream.toByteArray();
return output;
}
public static byte[] inflate(byte[] contentBytes) throws IOException, DataFormatException{
Inflater inflater = new Inflater(true);
inflater.setInput(contentBytes);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(contentBytes.length);
byte[] buffer = new byte[1024];
while (!inflater.finished()) {
int count = inflater.inflate(buffer);
outputStream.write(buffer, 0, count);
}
outputStream.close();
byte[] output = outputStream.toByteArray();
return output;
}
public static void shuffle(Object[] e){
Random rn = new Random();
for(int i=0;i<e.length;i++){
int other=rn.nextInt(e.length);
Object tmp=e[i];
e[i]=e[other];
e[other]=tmp;
}
}
public static String encodeBase64(byte[] data){
return Base64.getEncoder().encodeToString(data);
}
public static byte[] decodeBase64(String data){
return Base64.getDecoder().decode(data);
}
/** Simple XOR encryption of a map of key-value pairs.
* We randomize the order of key value pairs to make the string more unpredictable.
* Returned string is base64 and web safe
* @param key encryption key
* @param m map of param-value pairs to encrypt values.
* @return a string of encoded map param-value pairs which were then encrypted
*/
public static final String encrypt(String key,Map<String,String> m){
String ret=null;
Object[] es=m.keySet().toArray();
shuffle(es); // we shuffle entries to confuse the string a bit
StringBuilder buf=new StringBuilder();
for(int i=0;i<es.length;i++){
Object e=es[i];
if(i>0) buf.append("\n");
buf.append(e).append(":").append(m.get(e));
}
ret=encryptString(key,buf.toString());
return ret;
}
/**
* This method will encrypt a string and return BASE 64 string that is web safe.
* TO make the string web safe we replace + with - and / with _
* Must revert this change on the reverse.
* @param key
* @param ret
* @return
*/
public static final String encryptString(String key,String ret){
try{
byte[] bkey=key.getBytes("UTF-8");
byte[] bstr=ret.getBytes("UTF-8");
for(int i=0;i<bstr.length;i++){
bstr[i]=(byte)(bstr[i] ^ bkey[i%bkey.length]);
}
// now need to encode this
ret=encodeBase64(bstr);
ret=ret.replace('+','-');
ret=ret.replace('/','_');
ret=ret.replace('=','.');
ret=ret.replace("\n","");
}catch(Exception e){
ret="";
}
return ret;
}
/**Reverses the effects of encrypt.
* Also changes
* @param key
* @param m
* @return values decrypted and parsed into key-value pair along newline.
*/
public static final Map<String,String> decrypt(String key,String m){
m=decryptString(key,m);
Map<String,String> ret=new HashMap<>();
//System.out.println("Output:"+m);
Tokenizer tokz=new Tokenizer(m);
tokz.setDelimChars("\n");
tokz.setWhiteChars(null);
for(String t=tokz.nextToken();t!=null;t=tokz.nextToken()){
if("\n".equals(t)) continue;
String[] kv=t.split(":",2);
ret.put(kv[0],kv.length>1?kv[1]:null);
}
return ret;
}
public static final String decryptString(String key,String m){
try{
//m=URLDecoder.decode(m, "UTF-8");
m=m.replace('-','+');
m=m.replace('_','/');
m=m.replace('.','=');
byte[] bkey=key.getBytes("UTF-8");
byte[] bstr=decodeBase64(m);
for(int i=0;i<bstr.length;i++){
bstr[i]=(byte)(bstr[i] ^ bkey[i%bkey.length]);
}
m=new String(bstr,"UTF-8");
}catch(Exception e){
}
return m;
}
/**
* Generates a hash string with the algorithm name prefixed.
* @param message text to hash
* @param algorithm algorithm to use
* @return hash digest
*/
public static String hashString(String message, String algorithm) throws NoSuchAlgorithmException, UnsupportedEncodingException{
if(message==null) return message;
MessageDigest digest = MessageDigest.getInstance(algorithm);
byte[] hashedBytes = digest.digest(message.getBytes("UTF-8"));
return algorithm.toLowerCase()+":"+encodeBase64(hashedBytes);
}
/** hash text using sha256. */
public static String hashSHA256(String message){
try{
return hashString(message,"SHA-256");
}catch(Exception ex){
return "sha-256:"+Integer.toHexString(message.hashCode());
}
}
public static String hashMD5(String input){
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] messageDigest = md.digest(input.getBytes());
BigInteger no = new BigInteger(1, messageDigest);
String hashtext = no.toString(16);
while (hashtext.length() < 32) {
hashtext = "0" + hashtext;
}
return hashtext;
}catch (NoSuchAlgorithmException e) { // For specifying wrong message digest algorithms
throw new RuntimeException(e);
}
}
public static String toHexString(byte[] hash){
char[] HEX_CHARS = "0123456789ABCDEF".toCharArray();
StringBuilder sb = new StringBuilder(hash.length * 2);
for (byte b : hash) {
sb.append(HEX_CHARS[(b & 0xF0) >> 4]);
sb.append(HEX_CHARS[b & 0x0F]);
}
return sb.toString();
}
/**
* Finds first occurrence of sub inside body with and without case.
* We implement this search via a FSM and ignore the case.
* @param body text to search
* @param sub subsequence to find
* @param offset offset from 0
* @return offset of next occurance starting at offiset
*/
public static final int indexOf(CharSequence body,CharSequence sub,int offset){
if(body==null) return -1;
int state=0;
int blen=body.length();
int slen=sub.length();
boolean ignorecase=true;
for(int index=offset;index<blen;index++){
char bC=body.charAt(index);
char sC=sub.charAt(state);
if(ignorecase){
bC=Character.toLowerCase(bC);
sC=Character.toLowerCase(sC);
}
if(bC==sC) state+=1; else state=0;
if(state>=slen) return index-slen+1; // we found a match
}
return -1;
}
/**
* Will trim the string from left and right and remove any of the symbols.
* @param trim text to strip
* @param sym set of characters to trim
*/
public static String trim(String trim,String sym) {
if(trim==null || trim.length()==0) return trim;
int start=0;
int end=trim.length();
while(start<trim.length() && sym.indexOf(trim.charAt(start))!=-1) start++;
while(0<end && sym.indexOf(trim.charAt(end-1))!=-1) end--;
if(start==0 && end==trim.length()) return trim;
return start<end?trim.substring(start, end):"";
}
/** will copy contents of a list into a fixed length array */
public static String[] asArray(List<String> all) {
if(all==null) return null;
String[] ret=new String[all.size()];
all.toArray(ret);
return ret;
}
public static String toString(Object...args){
StringBuilder buf=new StringBuilder();
if(args.length>1){
buf.append("[");
for(int i=0;i<args.length;i++) buf.append(i==0?"":",").append(toString(args[i]));
buf.append("]");
}else if(args.length==1){
Object arg=args[0];
if(arg instanceof Iterable){
java.util.Iterator<?> it=((Iterable<?>)arg).iterator();
buf.append("[");
while(it.hasNext()) buf.append(buf.length()>1?",":"").append(it.next());
buf.append("]");
}else
if(arg instanceof java.util.Map){
java.util.Map<?,?> marg=(Map<?,?>) arg;
buf.append("{");
for(java.util.Map.Entry<?,?>e:marg.entrySet()){
buf.append(e.getKey().toString()).append(":").append(toString(e.getValue()));
}
buf.append("}");
}else{
buf.append(String.valueOf(arg));
}
}
return buf.toString();
}
/** splitting without using regex.
* leading or trailing delims will produce empty tokens.
* if you need to split on a set of single chars please use tokenizer.
* @param delim delim string
* @param str body of text to chop
* @param delim_count maximal number of splits or -1 for all
* @return array of tokens
*/
public static String[] split(String delim,String str,int delim_count) {
ArrayList<String> ret=new ArrayList<>();
int delimLen=delim!=null?delim.length():0;
int len=str.length();
int index=0;
int delimCnt=0; // track splits
int delimAt=0; // last delim position
while(index<len){
if(delim_count>0 && delimCnt>=delim_count) break; // reached limit of delims
delimAt=delimLen>0?str.indexOf(delim, index):index+1;
if(delimAt<0) break; // no more delims
// we got a hit
delimCnt+=1;
ret.add(str.substring(index, delimAt)); // add token
index=delimAt+delimLen;
}
if(index<len || delimAt>0){
// add remainder (no more delim or delim at end)
ret.add(str.substring(index));
}
return ret.toArray(new String[ret.size()]);
}
/** split a string as often as possible. */
public static String[] split(String delim,String str) {
return split(delim,str,-1);
}
@SafeVarargs
public static <T> Iterator<T> chainIterators(Iterator<T>...its){
return new JointIterator<T>(its);
}
}
@@ -1,35 +0,0 @@
package com.reliancy.util;
import java.util.Iterator;
import java.util.NoSuchElementException;
/** Chains multiple iterators to act as one.
*
*/
public class JointIterator<T> implements Iterator<T> {
final Iterator<T> iterators[];
int cursor;
@SafeVarargs
public JointIterator(Iterator<T> ...its){
this.iterators=its;
cursor=0;
}
@Override
public boolean hasNext() {
while(cursor<iterators.length){
if(iterators[cursor].hasNext()) return true;
cursor+=1; // cursor exhausted got to next iterator
}
return false;
}
@Override
public T next() {
if(cursor<iterators.length){
return iterators[cursor].next();
}else{
throw new NoSuchElementException();
}
}
}
@@ -1,79 +0,0 @@
package com.reliancy.util;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
/** Least recently used cache is a useful map of sorts.
* It has a fixed capacity and it forgets least used entries if new are added.
* If an allocator is installed it is consulted on cache miss.
* If a disposer is installed is is consulted on cache overflow.
* We can provide the same object that implements both and make use of a pool.
*/
public class LRUCache<K,V>{
public static interface Allocator<K,V>{
V request(K key);
}
public static interface Disposer<K,V>{
void release(K key,V val);
}
final Map<K,V> data;
int capacity;
final LinkedList<K> order=new LinkedList<>();
Allocator<K,V> allocator;
Disposer<K,V> disposer;
public LRUCache(int capacity,Map<K,V> backend){
this.capacity=capacity;
data=backend!=null?backend:new HashMap<K,V>();
}
public LRUCache(int capacity){
this(capacity,null);
}
public LRUCache<K,V> setAllocator(Allocator<K,V> a){
allocator=a;
return this;
}
public LRUCache<K,V> setDisposer(Disposer<K,V> a){
disposer=a;
return this;
}
public int size() {
return data.size();
}
public boolean containsKey(Object key) {
return data.containsKey(key);
}
public boolean containsValue(Object value) {
return data.containsValue(value);
}
public V get(K key) {
V ret=data.get(key);
if(ret!=null){
//cache is hit
order.remove(key);
order.addFirst(key);
}else{
//cache is missed
ret=allocator!=null?allocator.request(key):null;
}
return ret;
}
public V put(K key, V value) {
if(order.size()>=capacity){
// capacity is reached
K last=order.removeLast();
data.remove(last);
if(disposer!=null) disposer.release(key, value);
}
order.addFirst(key);
return data.put(key,value);
}
public V remove(Object key) {
order.remove(key);
return data.remove(key);
}
public void clear() {
order.clear();
data.clear();
}
}
-69
View File
@@ -1,69 +0,0 @@
package com.reliancy.util;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
/** Logging support based on JUL.
* We implement a deferred logmanager that survives shutdownhook until we release.
*/
public class Log {
static {
// must be called before any Logger method is used.
System.setProperty("java.util.logging.manager", DeferredMgr.class.getName());
System.setProperty("java.util.logging.SimpleFormatter.format","%1$tF %1$tT %4$-7s [%3$s] %5$s%6$s%n");
}
public static class DeferredMgr extends LogManager {
@Override public void reset() { /* don't reset yet. */ }
private void resetFinally() { super.reset(); }
}
public static Logger setup(){
Logger root_logger=Logger.getLogger("");
return root_logger;
}
public static void cleanup(){
LogManager mgr=LogManager.getLogManager();
if(mgr instanceof DeferredMgr){
((DeferredMgr)mgr).resetFinally();
}
}
public static void setLevel(Logger logger,String level_name){
if(level_name==null || level_name.isEmpty()) level_name="ERROR";
level_name=level_name.toUpperCase();
switch(level_name){
case "v":{
level_name="WARN";
break;
}
case "vv":{
level_name="INFO";
break;
}
case "vvv":{
level_name="DEBUG";
break;
}
}
switch(level_name){
case "WARN":{
level_name="WARNING";
break;
}
case "DEBUG":{
level_name="FINER";
break;
}
case "ERROR":{
level_name="SEVERE";
break;
}
}
Level lvl=Level.parse(level_name);
logger.setLevel(lvl);
for (Handler h : logger.getHandlers()) {
h.setLevel(lvl);
}
}
}
-440
View File
@@ -1,440 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.util;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
/** Path to a resource almost identical to a URL.
* It might but should not hold any handles. It holds the address and
* possibly takes care of looking up addresses.
* The richest syntax is:
* <pre> {@code PROTOCOL://USER:PWD@MACHINE:PORT/DATABASE?key=val&... } </pre>
* Properties are held in their own string and need to be decoded.
*
* We use forward slash for path delimitation of database portion. For the rest we preserve other slashes to allow windows domain\\user or server\\instance.
* Special chars are [/@?,:;]
* if any are found at the end of protocol we skip ://
* if any are found at the beginning of properties we skip ?
* if any are found at the beginning of database we skip /
*
* In windows we have volume or drive letters which are postfixed by colon : then slashes. We treat the volume as part of database.
* We store the database without first slash to allow specification of relative paths.
* To render it as absolute set protocol or host to empty string instead of null. Then a slash will be prefixed and you will get absolute path.
* @author amer
*/
public class Path {
static final String SYMBOLS="/@?,:;";
String connectstring;
String protocol; ///< protocol guides the interpretation of the other elements in conn, string
String userid; ///< authentication
String password; ///< authorization
String host; ///< machine or computer
String port; ///< access to computer
String database; ///< name of database or filename
String properties; ///< properties are what follows ? in a url
public Path(String connect,boolean do_parse) {
if(do_parse){
parse(connect);
}else{
connectstring=connect;
}
}
public Path(String connect) {
this(connect,true);
}
public Path(Path in) {
connectstring=in.connectstring;
protocol=in.protocol;
userid=in.userid;
password=in.password;
host=in.host;
port=in.port;
database=in.database;
properties=in.properties;
}
@Override
public String toString() {
return assemble();
}
/**
* Converts the path to a file.
* If absolute is true
* @param absolute if true forms absolute path else will return relative path
* @return
*/
public File toFile(boolean absolute){
String proto=getProtocol();
String host=getHost();
try{
setProtocol(absolute?"":null);
setHost(absolute?"":null);
String path=toString();
return new File(path);
}finally{
setProtocol(proto);
setHost(host);
}
}
public URL toURL() throws MalformedURLException{
String path=toString();
if(Handy.isBlank(getHost()) && path.contains("://") && !path.contains(":///")) path=path.replace("://",":///");
return new URL(path);
}
public Path clear(){
connectstring=null;
protocol=null;
userid=null;
password=null;
host=null;
port=null;
database=null;
properties=null;
return this;
}
public String assemble() {
if(connectstring!=null) return connectstring;
// assemble the connect string
StringBuilder buf=new StringBuilder();
//boolean absolute=false;
if(!Handy.isBlank(protocol)){
buf.append(protocol);
if(SYMBOLS.indexOf(protocol.charAt(protocol.length()-1))<0) buf.append("://");
}
if(!Handy.isBlank(host)){
if(userid!=null && password!=null){
buf.append(userid).append(":").append(password).append("@");
}
buf.append(host);
if(port!=null) buf.append(":").append(port);
}
if(!Handy.isBlank(database)){
if(buf.length()>0 && SYMBOLS.indexOf(database.charAt(0))<0){
// we got something in front so we need to use slash
buf.append("/");
}else if(protocol!=null || host!=null){
boolean winvol=database.length()>2 && database.charAt(1)==':' && (database.charAt(2)=='/' || database.charAt(2)=='\\');
// we got nothing in front but if host or protocol empty but not null we treat as absolute
if(!winvol) buf.append("/");
}
buf.append(database);
}
if(properties!=null){
if(SYMBOLS.indexOf(properties.charAt(0))<0) buf.append("?");
buf.append(properties);
}
connectstring=buf.toString();
return connectstring;
}
public Path parse(String connect) {
clear();
if (connect == null) {
return this;
}
this.connectstring=connect;
// first get protocol - everything up to : which is not followed by a symbol (includes :// but also c:/
int oldst=0;
int st=0;
for(int i=0;i<(connectstring.length()-1);i++){
char curr=connectstring.charAt(i);
if(curr==':'){
oldst=st;
st=i;
}else if(SYMBOLS.indexOf(curr)!=-1){
if(curr=='@') st=oldst; // this will back out one : if protocl search ended with @ indicating a server
break;
}
}
if(st==1){ st=0;} // this will supress single letter protocols i.e. c:/ ued in windows as part of database/file
if(2==(st-oldst)) st=oldst;
if(st>0){
this.protocol=connectstring.substring(0,st);
while(SYMBOLS.indexOf(connectstring.charAt(st))!=-1) st++; // advance over symbols
}
// next assume the rest is a file/database
database = connectstring.substring(st);
// now check for user id and password
st = database.indexOf('@');
boolean checkhost=st>=0;
if(!Handy.isBlank(protocol)){
checkhost=!protocol.contains(":file") && !protocol.contains(":mem") && !protocol.equals("file") && !protocol.equals("mem");;
}
if (st != -1) {
userid = database.substring(0, st);
if(userid.contains("%4")) try{userid=URLDecoder.decode(userid,"UTF-8");}catch(Exception e){}
database = database.substring(st + 1);
// now try to split user id into password if possible
st = userid.indexOf(':');
if (st != -1) {
password = userid.substring(st + 1);
userid = userid.substring(0, st);
}
}
// ok next try to split up machine if possible (only for absolute urls)
st = database.indexOf(':');
if(st<0) st=database.indexOf('/');
if (st != -1 && checkhost) {
boolean portfollows=database.charAt(st)==':';
host = database.substring(0, st);
// now try to recover port
if(portfollows){
int st2 = database.indexOf(':',st+1);
if(st2<0) st2 = database.indexOf('/',st+1);
if (st2 != -1 && st2>(st+1)) { // we have a port
port = database.substring(st + 1,st2);
st=st2;
}else{
// no port we have : then / - which is used in windows to indicate volume and we treat as part of database
st=-1;
host="";
}
}
database = database.substring(st + 1);
}
database=fixSlashes(database);
// finally split the properties from database
st = database.indexOf('?');
if(st==-1) st=database.indexOf(';');
if (st != -1) { // we have properties
properties = database.substring(st);
database = database.substring(0, st);
}
return this;
}
/**
* Absolute ResourcePath will have a protocol
*/
public boolean isAbsolute() {
return (protocol != null || host!=null);
}
/// will clear host and protocol using empty string thereby making database absolute path
public Path setAbsolute(){
setHost("");
setProtocol("");
return this;
}
public String getDatabase() {
return database;
}
public Path setDatabase(String database) {
this.database = database;
connectstring=null;
return this;
}
public String getHost() {
return host;
}
public Path setHost(String host) {
this.host = host;
connectstring=null;
return this;
}
public String getPassword() {
return password;
}
public Path setPassword(String password) {
this.password = password;
connectstring=null;
return this;
}
public String getPort() {
return port;
}
public Path setPort(String port) {
this.port = port;
connectstring=null;
return this;
}
public String getProtocol() {
return protocol;
}
public Path setProtocol(String protocol) {
this.protocol = protocol;
connectstring=null;
return this;
}
public String getUserid() {
return userid;
}
public Path setUserid(String userid) {
this.userid = userid;
connectstring=null;
return this;
}
public String getProperties() {
return properties;
}
public Path setProperties(String userid) {
this.properties = userid;
connectstring=null;
return this;
}
public String getBase() {
return Path.getBase(database);
}
public String getExtension() {
return Path.getExtension(database);
}
public String getPathItem() {
return Path.getPathItem(database);
}
/** Ensures that we use forward slashes and that single dot is not present mid or and the end.
*
* @param path a unix or windows or uri path
* @return a path with forward slashes
*/
public static String fixSlashes(String path) {
if(path==null || path.length()==0) return path;
path=path.replace("\\", "/");
path=path.replace("/./","/");
while(true){
if(path.endsWith("/")) path=path.substring(0,path.length()-1);
else if(path.endsWith("/.")) path=path.substring(0,path.length()-2);
else break;
}
return path;
}
/** returns database path given path and file.
* We assume the path uses forward backslash for delimitation.
*/
public static String getBase(String path) {
int st1 = path.lastIndexOf('/');
int st2 = path.lastIndexOf('\\');
int st=st2>st1?st2:st1;
if (st == -1) {
return null;
}
return path.substring(0, st);
}
public static String getExtension(String path) {
int st=Math.max(path.lastIndexOf('/'),path.lastIndexOf('\\'));
int st2 = path.lastIndexOf('.');
if (st2 == -1 || (st>0 && st2<st)) {
return null;
}
return path.substring(st2 + 1);
}
public static String getPathItem(String path) {
path=path.replace('\\','/');
int st11 = path.lastIndexOf('/');
int st1=1+st11;
int st2 = path.lastIndexOf('.');
if (st2 <0) {
st2 = path.length();
}
return path.substring(st1, st2);
}
/**
* Assuming that url starts with base will return string beyond base in url.
* @param base
* @param url
*/
public static String getRemainder(String base,String url){
if(base.length()>=url.length()) return null;
return url.substring(base.length());
}
/**
* unites two paths.
* @param base
* @param url
*/
public static String getUnion(String base,String url){
if(base==null || base.isEmpty()){
return url;
}
if(url==null || url.isEmpty()){
return base;
}
//if(url.startsWith(base)) return url;
if(Handy.indexOf(url,base,0)==0) return url;
StringBuilder ret=new StringBuilder();
ret.append(base);
if(!base.endsWith("/") && !url.startsWith("/")) ret.append("/");
if(base.endsWith("/") && url.startsWith("/")) ret.setLength(ret.length()-1);
ret.append(url);
return ret.toString();
}
/**
* method will split paths used in linux and windows.
* in particular for windows it checks if a single letter precedes a colon in which case it considers it a volume
* and does not split there.
* @param _paths paths joined with colon or semi-colon
* @return array of paths
*/
public static String[] splitPaths(String _paths){
String[] paths=_paths.replaceAll("(;|:|^)([a-zA-Z]):","$1$2##").split("[:;]");
for (int i = 0; i < paths.length; i++) {
String path=paths[i];
path = path.replace("##",":");
path=path.replace("/./","/");
path=path.replace("//","/");
path=path.replace("\\.\\","\\");
path=path.replace("\\\\","\\");
paths[i]=path;
}
return paths;
}
/**
* Returns a list of key,value pairs in the order they occur in the string str.
* @param str
*/
public static String[] splitProperties(String str) {
if(str.startsWith("?")) str=str.substring(1);
if(str.startsWith(";")) str=str.substring(1);
return str.split("&");
}
public static String[] splitKeyValue(String str) {
String[] t=Handy.split("=",str,1);
if(t==null || t.length==0) return null;
t[0]=Handy.trim(t[0],"'\"");
try {
t[1]=URLDecoder.decode(t[1],"UTF-8");
t[1]=Handy.trim(t[1],"'\"");
return t;
} catch (Exception e) {
if(t.length<2){
return new String[]{t[0],null};
}else{
return t;
}
}
}
public static String[] split(String str) {
return Handy.split("/",str);
}
}
@@ -1,217 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.util;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.HttpURLConnection;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/** Static utility with helper methods to read or write resources.
* The place where we host a global search path often used by others
* such as Template or FileServe (unless overriden.)
*/
public class Resources {
public static interface PathRewrite{
public String rewritePath(String path,Object context);
}
public static Object[] search_path;
/** appends one+ paths to search at position pos.
* neg pos substracts from end
*/
public static Object[] appendSearch(int pos,Object ...src){
//search_history.clear();
if(search_path==null) search_path=new Object[0];
if(pos<0) pos=search_path.length+pos+1;
if(pos<0 || pos>search_path.length) throw new IndexOutOfBoundsException("at:"+pos);
Object[] new_path=new Object[search_path.length+src.length];
// first left side of old search path
if(pos>0){
System.arraycopy(search_path, 0, new_path, 0, pos);
}
// next new sources
if(src.length>0){
System.arraycopy(src, 0, new_path, pos, src.length);
}
// lastly right side of old search path
System.arraycopy(search_path,pos, new_path,pos+src.length, search_path.length-pos);
search_path=new_path;
return search_path;
}
/** returns first good URL for path over sp or search_path.
* Along the way it optionally rewrites path to adjust to search path context.
* When possible it records lastModified timestamp in search_history for later lookup.
* @param remap rewrite rule if any
* @param path path to locate
* @param sp search path reverts to search_path if not specified
* @return URL that can be read.
*/
public static URL findFirst(PathRewrite remap,String path,Object ... sp){
String path0=path;
if(sp==null || sp.length==0) sp=search_path;
for(Object base:sp){
if(remap!=null) path=remap.rewritePath(path0,base);
if(base instanceof Class){
URL ret=((Class<?>)base).getResource(path);
return ret;
}else if(base instanceof String){
File ff=new File(base.toString(),path);
if(ff.exists()){
try {
URL ret=ff.toURI().toURL();
//search_history.put(path0,ff.lastModified());
return ret;
} catch (MalformedURLException e) {
continue;
}
}
}else if(base instanceof File){
File ff=new File((File)base,path);
if(ff.exists()){
try {
URL ret=ff.toURI().toURL();
//search_history.put(path,ff.lastModified());
return ret;
} catch (MalformedURLException e) {
continue;
}
}
}else if(base instanceof URL){
try {
URL ret=new URL((URL)base,path);
String proto=ret.getProtocol();
if(proto.equals("http") || proto.equals("https")){
HttpURLConnection huc = null;
try{
huc=(HttpURLConnection) ret.openConnection();
huc.setRequestMethod("HEAD");
int responseCode = huc.getResponseCode();
if(responseCode==HttpURLConnection.HTTP_OK){
//search_history.put(path,huc.getLastModified());
return ret;
}
}finally{
if(huc!=null) huc.disconnect();
}
}
if(proto.startsWith("jar")){
JarURLConnection juc = null;
juc=(JarURLConnection) ret.openConnection();
if(juc.getJarEntry()!=null){
//search_history.put(path,juc.getLastModified());
return ret;
}
}
if(proto.equals("file")){
File f=new File(ret.getPath());
if(f.exists()){
//search_history.put(path,f.lastModified());
return ret;
}
}
} catch (MalformedURLException e) {
continue;
} catch (IOException e2) {
continue;
}
}
}
return null;
}
/** if recorded in previous searches returns time modified. */
// public static Long lastModified(String path){
// Long ret=search_history.get(path);
// return ret;
// }
public static String toString(URL url) throws IOException{
return toString(url,StandardCharsets.UTF_8);
}
public static String toString(URL url,Charset chs) throws IOException{
try(InputStream is=url.openStream()){
return readChars(is,chs).toString();
}
}
public static byte[] toBytes(URL url) throws IOException{
try(InputStream is=url.openStream()){
return readBytes(is);
}
}
public static long copy(InputStream input, OutputStream output, byte[] buffer) throws IOException {
long count = 0;
int n = 0;
while (-1 != (n = input.read(buffer))) {
output.write(buffer, 0, n);
count += n;
}
return count;
}
public static long copy(Reader input, Writer output, char[] buffer) throws IOException {
long count = 0;
int n = 0;
while (-1 != (n = input.read(buffer))) {
output.write(buffer, 0, n);
count += n;
}
return count;
}
/** Reads a stream in one pass and returns bytes.
* Uses internally Handy.copy and a 4K buffer.
*/
public static final byte[] readBytes(InputStream str) throws IOException{
ByteArrayOutputStream bout=new ByteArrayOutputStream();
Resources.copy(str, bout, new byte[4096]);
return bout.toByteArray();
}
public static final CharSequence readChars(InputStream str) throws IOException{
return readChars(str,StandardCharsets.UTF_8);
}
public static final CharSequence readChars(InputStream str,Charset chset) throws IOException{
BufferedReader rdr=new BufferedReader(new InputStreamReader(str,chset));
StringBuilder ret=new StringBuilder();
for(String line=rdr.readLine();line!=null;line=rdr.readLine()){
ret.append(line).append("\n");
}
return ret;
}
public static CharSequence readChars(Class<?> cls,String name){
InputStream io=cls.getResourceAsStream(name);
try{
return readChars(io);
}catch(Exception e){
return null;
}finally{
if(io!=null) try{io.close();}catch(Exception e){}
}
}
public static void writeChars(CharSequence seq,OutputStream out,Charset chset) throws IOException{
OutputStreamWriter dout=new OutputStreamWriter(out,chset);
dout.append(seq);
dout.flush();
}
public static void writeChars(CharSequence seq,OutputStream out) throws IOException{
writeChars(seq,out,StandardCharsets.UTF_8);
}
public static void writeBytes(int offset,int len,byte[] seq,OutputStream out) throws IOException{
out.write(seq,offset, len);
}
}
@@ -1,131 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.util;
import java.util.HashMap;
/** Utility class to handle error codes and error messages.
* Error codes are integers that describe outcome of an operation.
*
* In cases when return codes are used and not exceptions thrown it is a mess to keep track of what they mean.
* With this class we define a uniform way of managing return codes and allow for additional information.
* This additional information can be text that could be localized and give better user information about what happened.
*
* First we distinguish between success and failure. Any code that is negative is failure.
* Default success code is 0 and provides no additional info. Any positive code is a warning or info and possibly carries extra meaning and
* or description.
*
* success,failure,pending
* source
* parametric
*/
public class ResultCode {
public static final byte TYPE_SUCCESS=0x0;
public static final byte TYPE_PENDING=0x1;
public static final byte TYPE_FAILURE=0xF;
final int code;
final String message;
final String source;
public ResultCode(byte type,short value,String src,String message) {
this.message=message;
this.source=src;
this.code=ResultCode.getCode(type,value,source!=null?source.hashCode():0);
}
public int getCode() {
return code;
}
public byte getType() {
return getType(code);
}
public int getValue() {
return getValue(code);
}
public String getSource() {
return source;
}
public String getMessage() {
return message;
}
@Override
public String toString() {
int code=getCode();
String context=getSource();
String message=getMessage();
if(context!=null){
return context+"("+String.format("%08X", code)+"):"+message;
}else{
return "("+String.format("%08X", code)+"):"+message;
}
}
protected static final HashMap<Integer,ResultCode> codes=new HashMap<>();
public static final int getCode(byte type,int value,int source){
int st=(type <<28) & 0xF0000000;
int sc=(source <<8) & 0x0FFFFF00;
int vl=(value) & 0x000000FF;
return (int) (st | sc | vl);
}
public static final byte getType(int code){
return (byte)((code>>28) & 0x0F);
}
public static final boolean testType(int code,byte st){
return getType(code)==st;
}
public static final boolean isSuccess(int code){
return testType(code,TYPE_SUCCESS);
}
public static final boolean isFailure(int code,int st){
return testType(code,TYPE_FAILURE);
}
public static final boolean isPending(int code,int st){
return testType(code,TYPE_PENDING);
}
public static final int getValue(int code){
return (int)(code & 0x000000FF);
}
public static final int getSource(int code){
return (int)((code & 0x0FFFFF00)>>8);
}
public static final synchronized ResultCode get(int code){
if(codes==null) return null;
return (ResultCode)codes.get(code);
}
public static final synchronized ResultCode put(ResultCode c){
ResultCode old=(ResultCode) codes.get(c.getCode());
codes.put(c.getCode(),c);
return old;
}
public static final int define(byte type,int value,Class<?> source,String message){
return define(type,value,source!=null?source.getSimpleName():null,message);
}
public static final int define(byte type,int value,String source,String message){
int code=getCode(type,value,source!=null?source.hashCode():0);
ResultCode c=get(code);
if(c!=null){
System.err.println("Result code redefinition(consider different value or source):"+c);
return code;
}
c=new ResultCode(type, (short) value,source,message);
put(c);
return code;
}
public static final int defineSuccess(int value,Class<?> source,String message){
return define(TYPE_SUCCESS,value,source,message);
}
public static final int defineFailure(int value,Class<?> source,String message){
return define(TYPE_FAILURE,value,source,message);
}
public static final int definePending(int value,Class<?> source,String message){
return define(TYPE_PENDING,value,source,message);
}
public static final int SUCCESS=ResultCode.defineSuccess(0,null,"Success");
public static final int FAILURE=ResultCode.defineFailure(0,null,"Failure");
public static final int PENDING=ResultCode.definePending(0,null,"Pending");
}
@@ -1,267 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.util;
import java.util.ArrayList;
import java.util.Iterator;
/** A utility to help us tokenize text along delimChars.
* This class is a little better than the java version because it allows for escaped delimChars.
* Delimiters are escaped with a slash, also single and double quotes supress delimiting when encountered.
* @author amer
*/
public class Tokenizer implements Iterable<String>,Iterator<String>{
public static final String WHITECHARS=" \t\r\f\n";
public static final String DELIMCHARS=" ,:;=<>{}[]()";
int offset;
CharSequence input;
String delimChars=DELIMCHARS;
String escapeChars="'\"";
String whiteChars=WHITECHARS;
public Tokenizer(CharSequence input){
this.input=input;
}
public Tokenizer(CharSequence input,int offset){
this.input=input;
this.offset=offset;
}
public CharSequence getInput() {
return input;
}
public int getOffset() {
return offset;
}
public Tokenizer setOffset(int offset) {
this.offset = offset;
return this;
}
public Tokenizer setInput(CharSequence input) {
this.input = input;
return this;
}
public boolean hasMoreTokens(){
if(offset>=input.length()) return false;
for(int i=offset;i<input.length();i++){
char ch=input.charAt(i);
if(Tokenizer.isElementOf(ch,whiteChars)==-1) return true;
}
return false;
}
public String nextToken(){
final StringBuilder out=new StringBuilder();
if(nextToken(out)){
return out.toString();
}else{
return null;
}
}
public boolean nextToken(StringBuilder out){
String[] sets={delimChars,escapeChars,whiteChars};
int noffset=nextToken(offset,input,out,sets);
if(noffset==offset){
return false;
}else{
offset=noffset;
return true;
}
}
public String getDelimChars() {
return delimChars;
}
public Tokenizer setDelimChars(String delimChars) {
this.delimChars = delimChars;
return this;
}
public String getEscapeChars() {
return escapeChars;
}
public Tokenizer setEscapeChars(String escapeChars) {
this.escapeChars = escapeChars;
return this;
}
public String getWhiteChars() {
return whiteChars;
}
public Tokenizer setWhiteChars(String whiteChars) {
this.whiteChars = whiteChars;
return this;
}
/**
* Utility method which collects all tokens and returns an array of them.
* Use it for small length strings when parsing user input.
* @param withdelims if tru returns delimiters as well
*/
public String[] getTokens(boolean withdelims){
final ArrayList<String> buf=new ArrayList<String>();
final StringBuilder out=new StringBuilder();
boolean lastSkipped=false;
while(this.nextToken(out)){
String tok=out.toString();
out.setLength(0);
if(!withdelims && tok.length()==1 && isElementOf(tok.charAt(0),delimChars)!=-1){
if(lastSkipped) buf.add("");
lastSkipped=true;
continue;
}
buf.add(tok);
lastSkipped=false;
}
return buf.toArray(new String[buf.size()]);
}
@Override
public Iterator<String> iterator() {
return this;
}
@Override
public boolean hasNext() {
return this.hasMoreTokens();
}
@Override
public String next() {
return this.nextToken();
}
public static int isElementOf(char ch,String d){
if(d==null) return -1;
return d.indexOf(ch);
}
/**Returns the next token and updated offset.
* This is an inline tokenizer for text parsing and the workhorse of the class.
* It stops when it encounters a delimiter. It treats delimChars as tokens too.
* It advances the offset whenever it was able to move be it delimiter or not.
* We should not have to adjust it for repeated calls except for special cases.
* @param offset
* @param sets various char sets 0-delimiters,1-escape chars,3-white chars
* @param input input chars
* @param out value of the token
* @return offset after processing
*/
public static int nextToken(int offset,CharSequence input, StringBuilder out,String[] sets){
String delimChars=(sets!=null && sets.length>=1)?sets[0]:",:;=<>{}[]()";
String escapeChars=(sets!=null && sets.length>=2)?sets[1]:null;
String whiteChars=(sets!=null && sets.length>=3)?sets[2]:null;
int escChar=-1; // if not -1 then we are escaping
char lastChar=0;
char curChar=0;
int lastOffset=offset;
int isWhiteChar=-1;
int isDelimChar=-1;
boolean weakEscape=false;
int controlCount=0; // counts number of \\ to prevent shortcuit on even number
while(offset<input.length()){ // only scan until the end
lastChar=curChar;
curChar=input.charAt(offset++); // from here on offset is ahead
isWhiteChar=isElementOf(curChar,whiteChars);
isDelimChar=isElementOf(curChar,delimChars);
// determine if we should ignore testing for exit
int isEscapeChar=isElementOf(curChar,escapeChars);
controlCount=(lastChar=='\\')?controlCount+1:0; // control count counts number of \\ to
if((controlCount%2)==1){
isDelimChar=isEscapeChar=-1; // shortcircuit delimiting or escaping if prev char was \\ but only unevent number of times
}
if(escChar==-1){ // should we enter escaping
if(isEscapeChar!=-1){
// will enter escChar but only once
escChar=isEscapeChar;
}
}else{ // should we exit escaping
if(weakEscape==false) isDelimChar=-1; // shortcircuit delim signal if in escape mode and not weak
// exit back to normal if escape found second time
if(isEscapeChar==escChar){
// special rule:if oldchar==curchar and next is not delim or whitespace we ignore escape char
boolean isletter=offset<input.length() && !(isElementOf(input.charAt(offset),delimChars)!=-1 || isElementOf(input.charAt(offset),whiteChars)!=-1);
if(lastChar==curChar && isletter){
// we are special enter weak escaping (where delimiter is not ignored)
// this will correct spurios double quotes but will recover forgotter delimiters between two parts
// we stay in escape mode but listen for delims
weakEscape=true;
}else{
escChar=-1; // we are exiting escaping
}
}
}
// emit chars and test for exit
if(escChar>=0 || isWhiteChar==-1 || isDelimChar!=-1){
// emit if escaping or if delimiter or not white char
out.append(curChar);
}
if(isDelimChar!=-1) break; // exit delimiter found
}
if(isDelimChar!=-1 && lastOffset<(offset-1)){
// fix end of out to not have a delimiter if it has any other string
offset-=1;out.setLength(out.length()-1);
}
return offset;
}
/**Returns the next token and updated offset.
* An improved inline tokenizer using various rules to control delimiting, escaping and text swallowing.
* We supply an array of events or if none is provided a default delimiter event is constructed.
* After that the events are used to control tokenization. We enter a loop and feed the input to
* the events if one or more are armed or triggered (state >=0) we defer emiting chars to output until we determine what to do.
* For events that do escape we just defer until end of escape is detected, for delimit we return back and
* for supress we just swallow the input without emitting it.
*/
/*
public static int nextToken(TokenizerRule state,int offset,CharSequence input, StringBuilder out){
int emitCount=0;
int oldOffset=offset;
while(offset<input.length()){
int st=state.consume(offset, input);
st=(st==TokenizerRule.DO_DEFER && offset==(input.length()))?TokenizerRule.DO_EMIT:st;
switch(st){
case TokenizerRule.DO_EMIT:
// we can emit what we got so far
if(oldOffset<=offset){
offset++;
out.append(input,oldOffset,offset);
emitCount+=(offset-oldOffset);
oldOffset=offset;
}
break;
case TokenizerRule.DO_DEFER:
// we need to defer emitting
offset++;
break;
case TokenizerRule.DO_SKIP:
// we just skip over this input
offset++;
oldOffset=offset;
break;
case TokenizerRule.DO_EXITBEFORE:
if(emitCount>0){
offset-=(state.getSize()-1);
}
case TokenizerRule.DO_EXITAFTER:
if(emitCount==0 && oldOffset<=offset){
// if there is anything left
offset++;
out.append(input,oldOffset,offset);
emitCount+=(offset-oldOffset);
oldOffset=offset;
}
state.clear();
default:
return st>=0?st:offset;
}
}
return offset;
}
*/
}
@@ -1,137 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Date;
import com.reliancy.rec.JSON;
import org.junit.BeforeClass;
import org.junit.Test;
public class TerminalTest {
@Entity.Info(
name="dbo.Maps"
)
public static class Maps extends DBO{
public static Field map_id=Field.Int("map_id").setPk(true);
public static Field map_name=Field.Str("map_name");
public static Field created=Field.DateTime("created");
public static Field active=Field.Bool("active");
static{
//Entity.publish(Maps.class);
}
}
@Entity.Info(
name="public.securable"
)
public static class Securable extends DBO{
public static Field id=Field.Int("id").setPk(true).setAutoIncrement(true);
public static Field kind=Field.Str("kind");
public static Field name=Field.Str("name");
public static Field display_name=Field.Str("display_name");
public static Field created=Field.DateTime("created_on");
public static Field is_essential=Field.Bool("is_essential");
static{
//Entity.publish(Maps.class);
}
}
@Entity.Info(
name="public.product"
)
public static class Product extends Securable{
public static Field valid_since=Field.DateTime("valid_since");
public static Field valid_until=Field.DateTime("valid_until");
public static Field short_info=Field.Str("short_info");
}
static SQLTerminal t;
@BeforeClass
public static void beforeAllTestMethods() {
System.out.println("Invoked once before all test methods");
String url=System.getenv("DB_URL");
System.out.println("DB URL:"+url);
t=new SQLTerminal(url);
}
/**
* jdbc connectivity
* @throws IOException
* @throws SQLException
*/
@Test
public void connection() throws IOException, SQLException{
try(Connection c=t.getConnection()){
System.out.println("Connection:"+c);
try (Statement stmt = c.createStatement()) {
// use stmt here
String sql = "SELECT * from \"dbo\".\"Maps\"";
try (ResultSet resultSet = stmt.executeQuery(sql)) {
// use resultSet here
while (resultSet.next()) {
System.out.println("ROw:"+resultSet.getInt("Map_id")+":"+resultSet.getString("Map_name"));
}
}
}
}
}
@Test
public void simpleCRUD() throws IOException, SQLException{
System.out.println("SimpleCRUD");
try(Action act=t.begin().load(Maps.class).execute()){
for(DBO o:act){
System.out.println("DBO:"+o);
}
}
Entity.retract(Maps.class);
}
@Test
public void complexCRUD() throws IOException, SQLException{
System.out.println("ComplexCRUD");
// Reading
try(Action act=t.begin().load(Product.class).execute()){
for(DBO o:act){
System.out.println("DBO:"+o);
}
}
//Saving
Product p=new Product();
p.setStatus(DBO.Status.USED);
Product.id.set(p,35);
Product.kind.set(p,Product.class.getSimpleName());
Product.name.set(p,"myproduct");
Product.created.set(p,new Date());
Product.short_info.set(p,"a sweet melody:"+new java.sql.Timestamp(System.currentTimeMillis()));
Product.display_name.set(p,"first entry");
System.out.println("Update P0:"+JSON.toString(p));
t.save(p);
System.out.println("Update P1:"+JSON.toString(p));
// Creating
Product pp=new Product();
Product.kind.set(pp,Product.class.getSimpleName());
Product.name.set(pp,"myproduct");
Product.created.set(pp,new Date());
Product.short_info.set(pp,"a sweet melody:");
Product.display_name.set(pp,"created entry:"+new java.sql.Timestamp(System.currentTimeMillis()));
t.save(pp);
System.out.println("Create PP0:"+JSON.toString(pp));
pp=t.load(Product.class, Product.id.get(pp,null));
System.out.println("Returning:"+pp);
// Deleting
t.delete(pp);
//Entity.retract(Maps.class);
}
}
@@ -12,14 +12,18 @@ public class ArgsConfigTest {
ArgsConfig args=new ArgsConfig("prog","--verbose","--key","value","cmd");
try {
args.load();
ArgsConfig.Property<String> env_user=new ArgsConfig.Property<>("USER",String.class);
// Cross-platform username check: USER on Unix/Linux/Mac, USERNAME on Windows
String osName = System.getProperty("os.name").toLowerCase();
boolean isWindows = osName.contains("win");
ArgsConfig.Property<String> env_user = new ArgsConfig.Property<>(
isWindows ? "USERNAME" : "USER", String.class);
ArgsConfig.Property<String> sys_user=new ArgsConfig.Property<>("user.name",String.class);
ArgsConfig.Property<Boolean> verbose=new ArgsConfig.Property<>("verbose",Boolean.class);
String usr_val1=args.getProperty(env_user,"None1");
String usr_val2=args.getProperty(sys_user,"None2");
System.out.println("Env User:"+usr_val1);
System.out.println("Sys User:"+usr_val2);
assertTrue(usr_val1.equals(usr_val2));
assertTrue("Environment username should match system username", usr_val1.equals(usr_val2));
System.out.println("Positional:"+args.getProperty(Config.APP_ARGS, null));
System.out.println("Verbose:"+verbose.get(args));
for(ArgsConfig.Property<?> p:args){
@@ -0,0 +1,305 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.jabba;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import com.reliancy.jabba.decor.Async;
import com.reliancy.jabba.decor.Routed;
import com.reliancy.jabba.servlet.JettyApp;
/**
* Test async endpoint support.
*/
public class AsyncTest {
public static class TestApp extends JettyApp {
@Override
public void configure(Config conf) throws Exception {
super.configure(conf);
// Import routes from this app - router is set by super.configure()
Router router = getRouter();
if(router != null){
router.importMethods(this);
router.compile();
} else {
// Router not set yet, set it ourselves
Router newRouter = new Router();
newRouter.importMethods(this);
newRouter.compile();
setRouter(newRouter);
}
}
@Routed(path="/async")
public CompletableFuture<String> asyncEndpoint() {
return CompletableFuture.supplyAsync(() -> {
try {
// Simulate long-running operation
Thread.sleep(100);
return "Async result";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
});
}
@Routed(path="/sync")
public String syncEndpoint() {
return "Sync result";
}
@Routed(path="/asyncWithParam")
public CompletableFuture<String> asyncWithParam(int delay) {
return CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(delay);
return "Delayed: " + delay + "ms";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
});
}
@Routed(path="/asyncAnnotation")
@Async
public String asyncWithAnnotation(String input, int value) {
// Regular method with @Async annotation - should be detected as async
return "Processed: " + input + " (" + value + ")";
}
}
@Test
public void testAsyncEndpointDetection() throws Exception {
TestApp app = new TestApp();
// Test async endpoint directly
java.lang.reflect.Method asyncMethod = TestApp.class.getMethod("asyncEndpoint");
MethodEndPoint asyncEp = new MethodEndPoint(app, asyncMethod);
assertTrue("Endpoint should be detected as async", asyncEp.isAsync());
// Test sync endpoint directly
java.lang.reflect.Method syncMethod = TestApp.class.getMethod("syncEndpoint");
MethodEndPoint syncEp = new MethodEndPoint(app, syncMethod);
assertFalse("Endpoint should be detected as sync", syncEp.isAsync());
// Test async with params
java.lang.reflect.Method asyncParamMethod = TestApp.class.getMethod("asyncWithParam", int.class);
MethodEndPoint asyncParamEp = new MethodEndPoint(app, asyncParamMethod);
assertTrue("Endpoint with params should be detected as async", asyncParamEp.isAsync());
}
@Test
public void testCompletableFutureReturnType() throws Exception {
TestApp app = new TestApp();
java.lang.reflect.Method method = TestApp.class.getMethod("asyncEndpoint");
MethodEndPoint endpoint = new MethodEndPoint(app, method);
assertTrue("Should detect CompletableFuture return type", endpoint.isAsync());
}
@Test
public void testAsyncAnnotation() throws Exception {
TestApp app = new TestApp();
// Test method with @Async annotation and regular args/return type
java.lang.reflect.Method asyncAnnotMethod = TestApp.class.getMethod("asyncWithAnnotation", String.class, int.class);
MethodEndPoint asyncAnnotEp = new MethodEndPoint(app, asyncAnnotMethod);
// Should be detected as async because of @Async annotation
assertTrue("Endpoint with @Async annotation should be detected as async", asyncAnnotEp.isAsync());
// Verify it has regular return type (not CompletableFuture)
assertFalse("Return type should not be CompletableFuture",
CompletableFuture.class.isAssignableFrom(asyncAnnotEp.method.getReturnType()));
// Verify it has regular parameters
assertEquals("Should have 2 parameters", 2, asyncAnnotEp.method.getParameterCount());
}
private TestApp app;
private int testPort;
private String baseUrl;
@Before
public void setUp() throws Exception {
// Use a random port to avoid conflicts
testPort = 18090 + (int)(Math.random() * 1000);
baseUrl = "http://localhost:" + testPort;
app = new TestApp();
ArgsConfig config = new ArgsConfig();
Config.SERVER_PORT.set(config, testPort);
config.load();
app.begin(config);
// Wait for server to be started (not necessarily running, which requires work() to be called)
int attempts = 0;
while(!app.isStarted() && attempts < 20){
Thread.sleep(100);
attempts++;
}
if(!app.isStarted()){
throw new Exception("Server failed to start on port " + testPort);
}
// Give server a moment to be ready
Thread.sleep(200);
}
@After
public void tearDown() throws Exception {
if(app != null){
try {
if(app.isRunning()){
app.end();
// Give server a moment to stop
Thread.sleep(300);
}
} catch (Exception e) {
// Ignore cleanup errors
}
app = null;
}
}
/**
* Helper method to make HTTP GET request
*/
private String httpGet(String path) throws Exception {
URL url = new URL(baseUrl + path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
conn.setReadTimeout(15000); // Longer timeout for async operations
int responseCode = conn.getResponseCode();
if(responseCode == HttpURLConnection.HTTP_OK){
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuilder response = new StringBuilder();
String line;
while((line = in.readLine()) != null){
response.append(line);
}
in.close();
return response.toString();
}else{
// Read error stream for more info
String errorMsg = "HTTP request failed with code: " + responseCode;
try {
BufferedReader err = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
String errLine;
while((errLine = err.readLine()) != null){
errorMsg += "\n" + errLine;
}
err.close();
} catch (Exception e) {
// Ignore
}
throw new Exception(errorMsg);
}
}
@Test
public void testSyncEndpointIntegration() throws Exception {
// Test synchronous endpoint first to verify basic connectivity
String result = httpGet("/sync");
assertEquals("Sync endpoint should return correct result", "Sync result", result);
}
@Test
public void testAsyncEndpointIntegration() throws Exception {
// Test CompletableFuture return type endpoint
long startTime = System.currentTimeMillis();
String result = httpGet("/async");
long duration = System.currentTimeMillis() - startTime;
assertEquals("Async endpoint should return correct result", "Async result", result);
// Should take at least 100ms (the sleep time in the endpoint)
assertTrue("Async endpoint should take time", duration >= 90);
}
@Test
public void testAsyncWithParamIntegration() throws Exception {
// Test async endpoint with parameters
long startTime = System.currentTimeMillis();
String result = httpGet("/asyncWithParam?delay=50");
long duration = System.currentTimeMillis() - startTime;
assertTrue("Result should contain delay info", result.contains("Delayed: 50ms"));
// Should take at least 50ms
assertTrue("Async endpoint with delay should take time", duration >= 40);
}
@Test
public void testAsyncAnnotationIntegration() throws Exception {
// Test @Async annotation endpoint
String result = httpGet("/asyncAnnotation?input=test&value=42");
assertEquals("Async annotation endpoint should return correct result",
"Processed: test (42)", result);
}
@Test
public void testAsyncNonBlocking() throws Exception {
// Test that async endpoints don't block the server
// First verify the endpoint works with a single request
String singleResult = httpGet("/async");
assertEquals("Single async request should work", "Async result", singleResult);
// Make multiple concurrent requests
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
try {
return httpGet("/async");
} catch (Exception e) {
e.printStackTrace();
return "ERROR: " + e.getMessage();
}
});
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
try {
return httpGet("/async");
} catch (Exception e) {
e.printStackTrace();
return "ERROR: " + e.getMessage();
}
});
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> {
try {
return httpGet("/async");
} catch (Exception e) {
e.printStackTrace();
return "ERROR: " + e.getMessage();
}
});
// Wait for all to complete
CompletableFuture.allOf(future1, future2, future3).join();
// All should succeed
String result1 = future1.get();
String result2 = future2.get();
String result3 = future3.get();
assertEquals("First request should succeed", "Async result", result1);
assertEquals("Second request should succeed", "Async result", result2);
assertEquals("Third request should succeed", "Async result", result3);
}
}
+12 -6
View File
@@ -4,8 +4,10 @@ import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import com.reliancy.jabba.decor.Routed;
import com.reliancy.jabba.sec.NotAuthentic;
import com.reliancy.jabba.sec.Secured;
import com.reliancy.jabba.servlet.JettyApp;
import com.reliancy.jabba.sec.SecurityActor;
import com.reliancy.jabba.sec.SecurityPolicy;
import com.reliancy.jabba.sec.plain.PlainSecurityStore;
@@ -22,7 +24,10 @@ import com.reliancy.util.Resources;
*/
public class DemoApp extends JettyApp implements AppModule{
public static void main( String[] args ) throws Exception{
Config cnf=new ArgsConfig(args).load();
ArgsConfig cnf=new ArgsConfig(args);
cnf.setProperty(Config.SERVER_PORT,8088);
cnf.setProperty(Config.LOG_LEVEL,"DEBUG"); // Set BEFORE load()
cnf.load();
JettyApp app=new DemoApp();
app.run(cnf);
}
@@ -57,11 +62,13 @@ public class DemoApp extends JettyApp implements AppModule{
// install file sever endpoint
FileServer fs=new FileServer("/static","/public");
fs.publish(app);
// publish DemoApp's own routes
this.publish(app);
Menu top_menu=Menu.request(Menu.TOP);
top_menu.add(new MenuItem("home")).addSpacer().add(new MenuItem("login"));
top_menu.setTitle("Jabba3");
app.getRouter().compile();
System.out.println(app.getRouter().regex);
log().debug("Router regex:{}",app.getRouter().regex);
}
@Override
public void publish(App app) {
@@ -74,7 +81,7 @@ public class DemoApp extends JettyApp implements AppModule{
String ret="";
try {
Template t=Template.find("/templates/login.hbs");
System.out.println("Template:"+t);
log().debug("Template:{}",t);
ret = t.render(context).toString();
} catch (IOException e) {
e.printStackTrace();
@@ -127,11 +134,10 @@ public class DemoApp extends JettyApp implements AppModule{
// here we need to process login and redirect
AppSession ass=AppSession.getInstance();
try{
System.out.println("Post login");
log().debug("Post login");
String userid=(String)req.getParam("userid",null);
String pwd=(String)req.getParam("password",null);
System.out.println("SS:"+ass);
System.out.println("P:"+userid+"/"+pwd);
log().debug("Session:{}",ass);
SecurityPolicy secpol=ass.getApp().getSecurityPolicy();
SecurityActor user=secpol.authenticate(userid, pwd);
if(user==null) throw new NotAuthentic("invalid credentials");
@@ -0,0 +1,342 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.jabba;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import com.reliancy.jabba.decor.Routed;
import com.reliancy.jabba.servlet.JettyApp;
/**
* Integration tests for JettyApp regular (non-async) functionality.
*/
public class JettyAppTest {
public static class SimpleTestApp extends JettyApp implements AppModule {
@Override
public void configure(Config conf) throws Exception {
super.configure(conf);
// Set up router and import methods
Router router = getRouter();
if(router == null){
router = new Router();
setRouter(router);
}
router.importMethods(this);
router.compile();
}
@Override
public void publish(App app) {
app.getRouter().importMethods(this);
}
@Routed(path="/test")
public String test() {
return "test response";
}
@Routed(path="/testPlain")
public void testPlain(Request req, Response resp) throws java.io.IOException {
resp.getEncoder().writeln("plain response");
}
@Routed(path="/testParam/{id:int}")
public String testParam(int id) {
return "param: " + id;
}
@Routed(path="/testQuery")
public String testQuery(String name) {
return "query: " + name;
}
@Routed(path="/testNoArg")
public String testNoArg() {
return "no arg response";
}
}
public static class SpaTestApp extends JettyApp {
private final String staticRoot;
public SpaTestApp(String staticRoot) {
this.staticRoot = staticRoot;
}
@Override
public void configure(Config conf) throws Exception {
super.configure(conf);
Router router = getRouter();
if(router == null){
router = new Router();
setRouter(router);
}
router.importMethods(this);
new FileServer("/","",staticRoot)
.setIndexFile("index.html")
.setFallbackFile("index.html")
.publish(this);
router.compile();
}
@Routed(path="/api/ping")
public String ping() {
return "api pong";
}
}
private JettyApp app;
private Path spaRoot;
private int testPort;
private String baseUrl;
@Before
public void setUp() throws Exception {
// Use a random port to avoid conflicts
testPort = 18090 + (int)(Math.random() * 1000);
baseUrl = "http://localhost:" + testPort;
app = new SimpleTestApp();
ArgsConfig config = new ArgsConfig();
Config.SERVER_PORT.set(config, testPort);
config.load();
app.begin(config);
// Wait for server to be started
int attempts = 0;
while(!app.isStarted() && attempts < 20){
Thread.sleep(100);
attempts++;
}
if(!app.isStarted()){
throw new Exception("Server failed to start on port " + testPort);
}
// Give server a moment to be ready
Thread.sleep(200);
}
@After
public void tearDown() throws Exception {
if(app != null){
try {
if(app.isStarted()){
app.end();
// Give server a moment to stop
Thread.sleep(300);
}
} catch (Exception e) {
// Ignore cleanup errors
}
app = null;
}
if(spaRoot != null){
try {
Files.walk(spaRoot)
.sorted((a,b) -> b.compareTo(a))
.forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
}
});
} catch (IOException e) {
}
spaRoot = null;
}
}
/**
* Helper method to make HTTP GET request
*/
private String httpGet(String path) throws Exception {
URL url = new URL(baseUrl + path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
int responseCode = conn.getResponseCode();
if(responseCode == HttpURLConnection.HTTP_OK){
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuilder response = new StringBuilder();
String line;
while((line = in.readLine()) != null){
response.append(line);
}
in.close();
return response.toString();
}else{
// Read error stream for more info
String errorMsg = "HTTP request failed with code: " + responseCode;
try {
BufferedReader err = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
String errLine;
while((errLine = err.readLine()) != null){
errorMsg += "\n" + errLine;
}
err.close();
} catch (Exception e) {
// Ignore
}
throw new Exception(errorMsg);
}
}
private HttpURLConnection httpRequest(String method, String path, String accept) throws Exception {
URL url = new URL(baseUrl + path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod(method);
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
if(accept != null){
conn.setRequestProperty("Accept", accept);
}
conn.connect();
return conn;
}
private String readBody(HttpURLConnection conn) throws Exception {
BufferedReader reader;
if(conn.getResponseCode() >= 400){
reader = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
}else{
reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
}
StringBuilder response = new StringBuilder();
String line;
while((line = reader.readLine()) != null){
response.append(line);
}
reader.close();
return response.toString();
}
@Test
public void testSimpleStringReturn() throws Exception {
String result = httpGet("/test");
assertEquals("Simple string return should work", "test response", result);
}
@Test
public void testPlainRequestResponse() throws Exception {
String result = httpGet("/testPlain");
assertTrue("Plain request/response should work", result.contains("plain response"));
}
@Test
public void testPathParameter() throws Exception {
String result = httpGet("/testParam/42");
assertEquals("Path parameter should work", "param: 42", result);
}
@Test
public void testQueryParameter() throws Exception {
String result = httpGet("/testQuery?name=testvalue");
assertEquals("Query parameter should work", "query: testvalue", result);
}
@Test
public void testNoArgMethod() throws Exception {
String result = httpGet("/testNoArg");
assertEquals("No-arg method should work", "no arg response", result);
}
@Test
public void testFileServerCanServeSpaIndexAndFallback() throws Exception {
if(app != null){
app.end();
Thread.sleep(300);
}
spaRoot = Files.createTempDirectory("jabba-spa");
Files.writeString(spaRoot.resolve("index.html"), "<html><body>spa shell</body></html>");
Files.writeString(spaRoot.resolve("app.js"), "console.log('spa');");
app = new SpaTestApp(spaRoot.toString());
ArgsConfig config = new ArgsConfig();
Config.SERVER_PORT.set(config, testPort);
config.load();
app.begin(config);
int attempts = 0;
while(!app.isStarted() && attempts < 20){
Thread.sleep(100);
attempts++;
}
Thread.sleep(200);
assertTrue("Root path should serve index file", readBody(httpRequest("GET", "/", "text/html")).contains("spa shell"));
assertTrue("Nested frontend route should fallback to index file", readBody(httpRequest("GET", "/dashboard/settings", "text/html")).contains("spa shell"));
assertEquals("API route should still reach routed endpoint", "api pong", httpGet("/api/ping"));
assertTrue("Direct asset request should serve asset file", httpGet("/app.js").contains("console.log('spa');"));
HttpURLConnection headRoot = httpRequest("HEAD", "/", "text/html");
assertEquals("HEAD should succeed on root file", HttpURLConnection.HTTP_OK, headRoot.getResponseCode());
assertEquals("HEAD should report HTML content type", "text/html", headRoot.getContentType());
assertNotNull("HEAD should expose ETag", headRoot.getHeaderField("ETag"));
assertNotNull("HEAD should expose Last-Modified", headRoot.getHeaderField("Last-Modified"));
assertEquals("HEAD should set nosniff", "nosniff", headRoot.getHeaderField("X-Content-Type-Options"));
assertEquals("Index should default to no-cache", "no-cache", headRoot.getHeaderField("Cache-Control"));
assertTrue("HEAD should report content length", headRoot.getContentLengthLong() > 0);
HttpURLConnection assetGet = httpRequest("GET", "/app.js", "*/*");
assertEquals("Asset should be served successfully", HttpURLConnection.HTTP_OK, assetGet.getResponseCode());
assertEquals("Asset should use asset cache policy", "public, max-age=3600", assetGet.getHeaderField("Cache-Control"));
assertNotNull("Asset should expose Last-Modified", assetGet.getHeaderField("Last-Modified"));
HttpURLConnection apiMiss = httpRequest("GET", "/api/missing", "application/json");
assertEquals("API-style missing route should stay 404 instead of SPA fallback", HttpURLConnection.HTTP_NOT_FOUND, apiMiss.getResponseCode());
HttpURLConnection traversal = httpRequest("GET", "/%2E%2E/secret.txt", "text/html");
assertEquals("Traversal attempts should be rejected", HttpURLConnection.HTTP_BAD_REQUEST, traversal.getResponseCode());
}
@Test
public void testFileServerCachesSmallAssetsInMemory() throws Exception {
Path cacheRoot = Files.createTempDirectory("jabba-cache");
try {
Files.writeString(cacheRoot.resolve("small.js"), "console.log('small');");
StringBuilder large = new StringBuilder();
for(int i=0;i<400;i++){
large.append("0123456789abcdef");
}
Files.writeString(cacheRoot.resolve("large.js"), large.toString());
FileServer server = new FileServer("/", "", cacheRoot.toString())
.setMemoryCacheContentLimit(256);
FileServer.Bucket bucket = server.getBucket("/");
FileServer.CachedAsset small = bucket.getAsset("small.js", server);
assertNotNull("Small asset should resolve", small);
assertNotNull("Small asset should keep bytes in memory", small.content);
assertTrue("Small asset length should be tracked", small.contentLength > 0);
FileServer.CachedAsset largeAsset = bucket.getAsset("large.js", server);
assertNotNull("Large asset should resolve", largeAsset);
assertNull("Large asset should not keep bytes in memory", largeAsset.content);
assertTrue("Large asset length should be tracked", largeAsset.contentLength > 256);
} finally {
Files.walk(cacheRoot)
.sorted((a,b) -> b.compareTo(a))
.forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
}
});
}
}
}
@@ -13,6 +13,9 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.Test;
import com.reliancy.jabba.decor.Routed;
import com.reliancy.jabba.servlet.JettyApp;
/**
* Unit test for simple App.
*/
@@ -0,0 +1,163 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.jabba;
import static org.junit.Assert.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import com.reliancy.jabba.decor.Routed;
import com.reliancy.jabba.sec.SecurityPolicy;
import com.reliancy.jabba.sec.plain.PlainSecurityStore;
import com.reliancy.util.Handy;
/**
* Security tests for authentication and routing.
*/
public class SecurityTest {
/** Minimal test implementation of Response for testing */
static class TestResponse extends Response {
private int status = 200;
public TestResponse(Request request) {
super(request);
}
@Override public void setContentType(String type) {}
@Override public void setStatus(int status) { this.status = status; }
@Override public String getHeader(String name) {
for(HTTP.Header header : headers) {
if(header.key.equalsIgnoreCase(name)) return header.value;
}
return null;
}
@Override public Response setHeader(String name, String value) {
headers.add(new HTTP.Header(name.toLowerCase(), value));
return this;
}
public String getCookie(String name) {
for(HTTP.Cookie cookie : cookies) {
if(cookie.key.equals(name)) return cookie.value;
}
return null;
}
@Override public Response setCookie(String name, String value, int maxAge, boolean secure) {
cookies.add(new HTTP.Cookie(name, value, maxAge, secure, false));
return this;
}
@Override public boolean isCommitted() { return false; }
@Override public void commit() {}
@Override public boolean isCompleted() { return false; }
@Override public void complete() {}
@Override public java.io.OutputStream getOutputStream() throws IOException { return null; }
@Override public java.io.Writer getWriter() throws IOException { return null; }
@Override public com.reliancy.jabba.WebSocketSession upgradeToWebSocket(String route, com.reliancy.jabba.Session appSession) throws IOException {
throw new UnsupportedOperationException("WebSocket not supported in test");
}
}
@Test
public void testSecretKeyFromEnvironment() throws Exception {
// Test that secret key can be loaded from environment using reflection
String originalKey = System.getenv("JABBA_SECRET_KEY");
try {
System.setProperty("jabba.secret.key", "test-secret-key-12345");
SecurityPolicy policy = new SecurityPolicy();
java.lang.reflect.Method getSecretMethod = SecurityPolicy.class.getDeclaredMethod("getSecret");
getSecretMethod.setAccessible(true);
String secret = (String) getSecretMethod.invoke(policy);
assertNotNull("Secret should not be null", secret);
assertFalse("Secret should not be empty", secret.isEmpty());
} finally {
if (originalKey != null) {
System.setProperty("jabba.secret.key", originalKey);
} else {
System.clearProperty("jabba.secret.key");
}
}
}
@Test
public void testAESEncryption() {
// Test AES encryption/decryption
String key = "test-secret-key-for-encryption-12345678901234567890";
Map<String, String> data = new HashMap<>();
data.put("user", "testuser");
data.put("pass", "testpass");
String encrypted = Handy.encrypt(key, data);
assertNotNull("Encrypted data should not be null", encrypted);
assertFalse("Encrypted data should not be empty", encrypted.isEmpty());
Map<String, String> decrypted = Handy.decrypt(key, encrypted);
assertEquals("Decrypted user should match", "testuser", decrypted.get("user"));
assertEquals("Decrypted pass should match", "testpass", decrypted.get("pass"));
}
@Test
public void testInputValidation() throws Exception {
// Test input validation in MethodEndPoint using reflection to access protected method
MethodEndPoint endpoint = new MethodEndPoint(new TestEndpoint(),
TestEndpoint.class.getMethod("testMethod", String.class));
java.lang.reflect.Method validateMethod = MethodEndPoint.class.getDeclaredMethod(
"validateInput", Object.class, Class.class, String.class);
validateMethod.setAccessible(true);
// Test normal input
Object valid = validateMethod.invoke(endpoint, "normal string", String.class, "testParam");
assertEquals("Normal string should pass validation", "normal string", valid);
// Test null input
Object nullVal = validateMethod.invoke(endpoint, null, String.class, "testParam");
assertNull("Null input should return null", nullVal);
// Test very long string (should be truncated)
StringBuilder longStr = new StringBuilder();
for (int i = 0; i < 100001; i++) {
longStr.append("a");
}
Object longInput = validateMethod.invoke(endpoint, longStr.toString(), String.class, "testParam");
assertNotNull("Long input should not be null", longInput);
assertTrue("Long input should be truncated", ((String)longInput).length() <= 100000);
}
@Test
public void testCookieSecurity() throws IOException {
// Test that cookies are set with HttpOnly flag
Response response = new TestResponse((Request)null);
response.setCookie("test", "value", 3600, true, true);
// Verify cookie was added
assertNotNull("Cookie should be added", response.getCookie("test"));
assertEquals("Cookie value should match", "value", response.getCookie("test"));
}
@Test
public void testResponseHeaderLookup() {
// Test that header lookup works correctly (bug fix verification)
Response response = new TestResponse((Request)null);
response.setHeader("Content-Type", "application/json");
String header = response.getHeader("content-type");
assertEquals("Header lookup should be case-insensitive", "application/json", header);
}
// Test endpoint class for testing
public static class TestEndpoint {
@Routed
public String testMethod(String param) {
return param;
}
}
}
@@ -0,0 +1,363 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.jabba;
import java.net.URI;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.Session.Listener;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import com.reliancy.jabba.decor.Routed;
import com.reliancy.jabba.decor.WebSocket;
import com.reliancy.jabba.servlet.JettyApp;
/**
* Integration tests for WebSocket functionality.
* Tests the new WebSocket architecture using @WebSocket + @Routed annotations
* and WebSocketSession argument-based endpoints.
*/
public class WebSocketTest {
/**
* Test application with WebSocket endpoints using new API.
*/
public static class TestWebSocketApp extends JettyApp {
private int messageCount = 0;
public TestWebSocketApp() {
super();
}
@Override
public void configure(Config conf) throws Exception {
super.configure(conf);
// Import methods from this class to the router
Router router = getRouter();
if(router != null) {
router.importMethods(this);
router.compile();
}
}
// Echo endpoint - sends back what it receives
@Routed(path="/ws/echo")
@WebSocket
public void echoEndpoint(com.reliancy.jabba.WebSocketSession session) {
session.onText(msg -> {
try {
session.sendText("Echo: " + msg);
} catch (Exception e) {
e.printStackTrace();
}
});
}
// Simple endpoint that counts messages
@Routed(path="/ws/counter")
@WebSocket
public void counterEndpoint(com.reliancy.jabba.WebSocketSession session) {
session.onText(msg -> {
try {
messageCount++;
session.sendText("Message #" + messageCount);
} catch (Exception e) {
e.printStackTrace();
}
});
}
// Endpoint with immediate response on connect
@Routed(path="/ws/session")
@WebSocket
public void sessionEndpoint(com.reliancy.jabba.WebSocketSession session) {
session.onText(msg -> {
try {
session.sendText("Connected: " + session.getId());
} catch (Exception e) {
e.printStackTrace();
}
});
}
// HTTP endpoint for comparison
@Routed(path="/test")
public String testHttp() {
return "HTTP works";
}
public int getMessageCount() {
return messageCount;
}
}
/**
* Simple WebSocket client for testing (using Jetty 12 Session.Listener API).
*/
public static class TestWebSocketClient implements Session.Listener.AutoDemanding {
private final BlockingQueue<String> messages = new LinkedBlockingQueue<>();
private final CompletableFuture<Session> connectFuture = new CompletableFuture<>();
private final CompletableFuture<Void> closeFuture = new CompletableFuture<>();
private Session session;
@Override
public void onWebSocketOpen(Session session) {
this.session = session;
connectFuture.complete(session);
}
@Override
public void onWebSocketText(String message) {
messages.add(message);
}
@Override
public void onWebSocketClose(int statusCode, String reason) {
closeFuture.complete(null);
}
@Override
public void onWebSocketError(Throwable cause) {
cause.printStackTrace();
}
public void send(String message) throws Exception {
if (session != null && session.isOpen()) {
// Jetty 12 API: Session.sendText() directly
session.sendText(message, null);
}
}
public String receiveMessage(long timeout, TimeUnit unit) throws InterruptedException {
return messages.poll(timeout, unit);
}
public void close() {
if (session != null) {
session.close();
}
}
public CompletableFuture<Session> getConnectFuture() {
return connectFuture;
}
public CompletableFuture<Void> getCloseFuture() {
return closeFuture;
}
}
private TestWebSocketApp app;
private WebSocketClient wsClient;
private int testPort;
private String baseWsUrl;
@Before
public void setUp() throws Exception {
// Use a random port to avoid conflicts
testPort = 18090 + (int)(Math.random() * 1000);
baseWsUrl = "ws://localhost:" + testPort;
// Start test app
app = new TestWebSocketApp();
ArgsConfig config = new ArgsConfig();
Config.SERVER_PORT.set(config, testPort);
config.load();
app.begin(config);
// Wait for server to start
int attempts = 0;
while(!app.isStarted() && attempts < 20){
Thread.sleep(100);
attempts++;
}
if(!app.isStarted()){
throw new Exception("Server failed to start on port " + testPort);
}
Thread.sleep(200);
// Create WebSocket client
wsClient = new WebSocketClient();
wsClient.start();
}
@After
public void tearDown() throws Exception {
if (wsClient != null) {
try {
wsClient.stop();
} catch (Exception e) {
// Ignore
}
}
if(app != null){
try {
if(app.isStarted()){
app.end();
Thread.sleep(300);
}
} catch (Exception e) {
// Ignore cleanup errors
}
app = null;
}
}
@Test
public void testWebSocketEchoEndpoint() throws Exception {
TestWebSocketClient client = new TestWebSocketClient();
// Connect to echo endpoint
URI uri = new URI(baseWsUrl + "/ws/echo");
wsClient.connect(client, uri);
// Wait for connection
Session session = client.getConnectFuture().get(5, TimeUnit.SECONDS);
assertNotNull("Connection should be established", session);
assertTrue("Session should be open", session.isOpen());
// Send a message
client.send("Hello WebSocket");
// Receive echo response
String response = client.receiveMessage(5, TimeUnit.SECONDS);
assertNotNull("Should receive response", response);
assertEquals("Should echo back message", "Echo: Hello WebSocket", response);
// Send another message
client.send("Test 123");
response = client.receiveMessage(5, TimeUnit.SECONDS);
assertEquals("Should echo second message", "Echo: Test 123", response);
// Close connection
client.close();
client.getCloseFuture().get(5, TimeUnit.SECONDS);
}
@Test
public void testWebSocketCounterEndpoint() throws Exception {
TestWebSocketClient client = new TestWebSocketClient();
// Connect to counter endpoint
URI uri = new URI(baseWsUrl + "/ws/counter");
wsClient.connect(client, uri);
// Wait for connection
Session session = client.getConnectFuture().get(5, TimeUnit.SECONDS);
assertNotNull("Connection should be established", session);
// Send multiple messages
client.send("msg1");
String response1 = client.receiveMessage(5, TimeUnit.SECONDS);
assertTrue("First response should contain counter", response1.contains("Message #"));
client.send("msg2");
String response2 = client.receiveMessage(5, TimeUnit.SECONDS);
assertTrue("Second response should contain counter", response2.contains("Message #"));
// Counter should have incremented
assertNotEquals("Responses should be different", response1, response2);
// Close connection
client.close();
client.getCloseFuture().get(5, TimeUnit.SECONDS);
}
@Test
public void testWebSocketSessionEndpoint() throws Exception {
TestWebSocketClient client = new TestWebSocketClient();
// Connect to session endpoint
URI uri = new URI(baseWsUrl + "/ws/session");
wsClient.connect(client, uri);
// Wait for connection
Session session = client.getConnectFuture().get(5, TimeUnit.SECONDS);
assertNotNull("Connection should be established", session);
// Send a message to trigger the response
client.send("ping");
// Should receive message with session ID
String response = client.receiveMessage(5, TimeUnit.SECONDS);
assertNotNull("Should receive connection message", response);
assertTrue("Message should contain 'Connected'", response.startsWith("Connected:"));
// Close connection
client.close();
client.getCloseFuture().get(5, TimeUnit.SECONDS);
}
@Test
public void testMultipleWebSocketClients() throws Exception {
TestWebSocketClient client1 = new TestWebSocketClient();
TestWebSocketClient client2 = new TestWebSocketClient();
// Connect both clients to echo endpoint
URI uri = new URI(baseWsUrl + "/ws/echo");
wsClient.connect(client1, uri);
wsClient.connect(client2, uri);
// Wait for connections
Session session1 = client1.getConnectFuture().get(5, TimeUnit.SECONDS);
Session session2 = client2.getConnectFuture().get(5, TimeUnit.SECONDS);
assertNotNull("Client 1 should connect", session1);
assertNotNull("Client 2 should connect", session2);
assertTrue("Client 1 session should be open", session1.isOpen());
assertTrue("Client 2 session should be open", session2.isOpen());
// Send messages from both clients
client1.send("From Client 1");
client2.send("From Client 2");
// Receive responses
String response1 = client1.receiveMessage(5, TimeUnit.SECONDS);
String response2 = client2.receiveMessage(5, TimeUnit.SECONDS);
assertEquals("Client 1 should receive its echo", "Echo: From Client 1", response1);
assertEquals("Client 2 should receive its echo", "Echo: From Client 2", response2);
// Close connections
client1.close();
client2.close();
client1.getCloseFuture().get(5, TimeUnit.SECONDS);
client2.getCloseFuture().get(5, TimeUnit.SECONDS);
}
@Test
public void testHttpStillWorksWithWebSocket() throws Exception {
// Verify HTTP endpoints still work when WebSocket is enabled
java.net.URL url = new java.net.URL("http://localhost:" + testPort + "/test");
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
int responseCode = conn.getResponseCode();
assertEquals("HTTP endpoint should work", 200, responseCode);
java.io.BufferedReader in = new java.io.BufferedReader(
new java.io.InputStreamReader(conn.getInputStream()));
String response = in.readLine();
in.close();
assertEquals("HTTP response should be correct", "HTTP works", response);
}
}
@@ -1,40 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
import java.io.IOException;
import org.junit.Test;
public class ObjTest {
/**
* Plain CRUD
* @throws IOException
*/
@Test
public void crudVec() throws IOException
{
Obj o=new Obj();
Obj a=new Obj(true);
System.out.println("O1:"+o);
System.out.println("A1:"+a);
a.add(1).add("three");
o.add(1).add("three").set(new Slot("arr"),new String[]{"a","b","c"});
System.out.println("O2meta:"+o.isArray()+"/"+o.meta());
System.out.println("O2:"+o);
System.out.println("A2:"+a);
o.set(o.getSlot("car"),"bar");
System.out.println("O3:"+o);
StringBuilder json=new StringBuilder();
JSON.writes(o,json);
System.out.println("ENC:"+json);
Rec dec=JSON.reads(json);
System.out.println("DEC:"+dec);
}
}
@@ -1,40 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.util;
import java.io.IOException;
import org.junit.Test;
public class HandyTest {
/**
* Plain CRUD
* @throws IOException
*/
@Test
public void splitting() throws IOException
{
System.out.println("Splitting test...");
String tst1="One,Two,Three";
System.out.println(tst1+" over ,");
for(String s:Handy.split(",",tst1)){
System.out.println("\tt:"+s);
}
//System.out.println(tst1+" over ");
//for(String s:Handy.split("",tst1)){
// System.out.println("\tt:"+s);
//}
String tst2="AND A AND B ANDAND D AND";
System.out.println(tst2+" over AND");
for(String s:Handy.split("AND",tst2)){
System.out.println("\tt:"+s);
}
}
}