4 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
50 changed files with 1120 additions and 5899 deletions
Vendored
+3
View File
@@ -39,3 +39,6 @@ gradle-app.setting
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
# do not consider bstore as part - will be its own repo
bstore/
+6 -24
View File
@@ -9,7 +9,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.reliancy</groupId>
<artifactId>jabba</artifactId>
<version>2.0.0-SNAPSHOT</version>
<version>3.0.0-SNAPSHOT</version>
<licenses>
<license>
<name>The Apache License, Version 2.0</name>
@@ -20,25 +20,25 @@
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>12.0.15</version>
<version>12.0.32</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>jetty-http2-server</artifactId>
<version>12.0.15</version>
<version>12.0.32</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee10</groupId>
<artifactId>jetty-ee10-servlet</artifactId>
<version>12.0.15</version>
<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.15</version>
<version>12.0.32</version>
<scope>runtime</scope>
</dependency>
<dependency>
@@ -59,24 +59,6 @@
<version>4.4.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.3.232</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.4</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.1.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
@@ -86,7 +68,7 @@
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>jetty-websocket-jetty-client</artifactId>
<version>12.0.15</version>
<version>12.0.32</version>
<scope>test</scope>
</dependency>
</dependencies>
+41 -52
View File
@@ -7,14 +7,12 @@ Jabba is a java library that gets its inspiration from Python Flask. It will exp
* running a test via: gradle test
* 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:
Jabba depends on the following libraries:
* org.eclipse.jetty:jetty-server - http servlet container
* org.slf4j:slf4j-simple - logging facility
* org.eclipse.jetty:jetty-server - HTTP servlet container
* org.slf4j:slf4j-jdk14 - 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)
* 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.
@@ -44,7 +42,7 @@ This is a sonatype nexus repository manager we host here at reliancy.
* ~~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.
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.
@@ -119,50 +117,42 @@ For additional validation, implement custom validation in your endpoint methods.
# 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.
They are:
Within this repository the main areas are:
* 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 - utility methods maximally independent
* `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
## 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.
If you need the data/storage layer itself, use `bstore-j` directly. Jabba intentionally does
not carry its own SQL implementation anymore.
You can then do something like:
``` java
for(String token:new Tokenizer(bodyOftext)){
System.out.println("Word:"+token);
}
## 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);
```
## rec
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.
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.
One example would be notation like:
``` java
db.query(Product.class).where(Product.first_name.equals("Bla"))
```
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.
## 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.
Please note one thing about SQL in particular. SQL and related RDBMS systems 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 thousands 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 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.
@@ -310,21 +300,20 @@ public class App extends JettyApp{
work_dir="../var";
}
Template.search_path(work_dir,App.class);
JettyApp app=new JettyApp();
App app=new App();
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);
Router router=app.getRouter();
router.importMethods(app);
// it helps to support static file serving too
FileServer fs=new FileServer("/static",work_dir+"/public");
fs.exportRoutes(app.getRouter());
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");
app.run(new FileConfig());
router.compile();
app.begin(new FileConfig());
//System.out.println("Goodbye World!");
}
// case 1: simplest endpoint (path from method name)
+4 -5
View File
@@ -12,7 +12,7 @@ apply from: 'extra.gradle'
project.buildDir = 'target'
group='com.reliancy'
version = '2.0.0-SNAPSHOT'
version = '3.0.0-SNAPSHOT'
application{
mainClass=(group+'.'+name+'.JettyApp')
}
@@ -27,7 +27,8 @@ tasks.withType(JavaCompile) {
}
dependencies {
def jettyVersion="12.0.15"
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}"
@@ -37,9 +38,7 @@ dependencies {
//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.h2database:h2:2.3.232'
implementation 'org.postgresql:postgresql:42.7.4'
implementation 'com.zaxxer:HikariCP:5.1.0'
implementation "com.reliancy:bstore-j:${bstoreVersion}"
testImplementation "junit:junit:4.13.2"
testImplementation "org.eclipse.jetty.websocket:jetty-websocket-jetty-client:${jettyVersion}"
}
-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");
}
}
-194
View File
@@ -1,194 +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);
// Only set ID if not already set (allows explicit database column name mapping)
// Use Field's name (database column name) if available, otherwise use Java field name
if(slot.getId()==null || slot.getId().isEmpty()){
String dbName=slot.getName(); // This is the name passed to Field constructor (e.g., "created_on")
if(dbName!=null && !dbName.isEmpty()){
slot.setId(dbName);
}else{
slot.setId(sf_name); // Fallback to Java field 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);
}
}
-281
View File
@@ -1,281 +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?" ":",");
// Use getId() if set (database column name), otherwise use getName()
String colName=(f.getId()!=null && !f.getId().isEmpty())?f.getId():f.getName();
append(alias).append(".").id(colName);
}
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();
// Use getId() if set (database column name), otherwise use getName()
String ePkName=(ePk.getId()!=null && !ePk.getId().isEmpty())?ePk.getId():ePk.getName();
String bPkName=(bPk.getId()!=null && !bPk.getId().isEmpty())?bPk.getId():bPk.getName();
append(eAlias).append(".").id(ePkName);
append("=");
append(bAlias).append(".").id(bPkName);
}
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();
// Use getId() if set (database column name), otherwise use getName()
String fieldName=(field.getId()!=null && !field.getId().isEmpty())?field.getId():field.getName();
String fname=wrap(fieldName);
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)){
String pkName=(pk.getId()!=null && !pk.getId().isEmpty())?pk.getId():pk.getName();
append(delim).id(pkName);
ext.append(delim).append("?");
delim=",";
}
for(int index=0;index<supplied.size();index++){
Field f=supplied.get(index);
if(index>0) delim=",";
String fName=(f.getId()!=null && !f.getId().isEmpty())?f.getId():f.getName();
append(delim).id(fName);
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);
String fName=(f.getId()!=null && !f.getId().isEmpty())?f.getId():f.getName();
id(fName).append("=?");
}
where();
Field pk=entity.getPk();
String pkName=(pk.getId()!=null && !pk.getId().isEmpty())?pk.getId():pk.getName();
id(pkName).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;
}
}
@@ -58,6 +58,9 @@ public class AppSessionFilter extends Processor{
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());
+211 -30
View File
@@ -9,6 +9,7 @@ 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;
@@ -17,7 +18,6 @@ import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
/** FileServer is an module and endpoint that exposes multiple URLs thru which files are served.
@@ -30,6 +30,35 @@ import org.slf4j.Logger;
* Bucket is there to process input/output given verbs over resources under it.
*/
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.
@@ -41,12 +70,15 @@ public class FileServer extends EndPoint implements AppModule,Resources.PathRewr
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,Long> hit_history=new LRUCache<>(2*Runtime.getRuntime().availableProcessors());
LRUCache<String,CachedAsset> hit_history=new LRUCache<>(64);
public FileBucket(String prefix){
this.prefix=prefix;
@@ -58,7 +90,7 @@ public class FileServer extends EndPoint implements AppModule,Resources.PathRewr
@Override
public String asContained(String path) {
if(!path.startsWith(prefix)) return null; // not contained
String local_path=path.replace(prefix,"");
String local_path=path.substring(prefix.length());
if(extAllowed.length==0) return local_path;
for(String ext:extAllowed){
if(path.endsWith(ext)){
@@ -82,28 +114,64 @@ public class FileServer extends EndPoint implements AppModule,Resources.PathRewr
public Object[] getDomain(){
return (domain!=null && domain.length>0)?domain:Resources.search_path;
}
public InputStream openSource(String local_path,FileServer user) throws IOException{
@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; // skip if rpath not located
if(f==null) return null;
URLConnection conn=f.openConnection();
hit_history.put(local_path,conn.getLastModified()); // pull last modified for signature
return conn.getInputStream();
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){
Long last_modified=hit_history.get(local_path);
if(last_modified==null) return null;
String sig=String.valueOf(last_modified);
return Handy.hashMD5(sig);
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;
@@ -124,6 +192,30 @@ public class FileServer extends EndPoint implements AppModule,Resources.PathRewr
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.
*/
@@ -142,30 +234,24 @@ public class FileServer extends EndPoint implements AppModule,Resources.PathRewr
Logger logger=log();
boolean atDebug=logger.isDebugEnabled();
if(atDebug) logger.debug("{}:{}",verb,path);
if(HTTP.VERB_GET.equals(verb)){
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
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);
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;
}
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{
@@ -219,6 +305,101 @@ public class FileServer extends EndPoint implements AppModule,Resources.PathRewr
}
public void publish(App app) {
Router rep=app.getRouter();
for(Bucket b:buckets) rep.addRoute("GET",b.getPrefix()+".*",this);
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;
}
}
@@ -45,6 +45,7 @@ public abstract class Response {
protected final ArrayList<HTTP.Header> headers=new ArrayList<>();
protected final ArrayList<HTTP.Cookie> cookies=new ArrayList<>();
protected CompletableFuture<Object> promise;
protected boolean upgradedToWebSocket;
protected Response(Request request) {
this.request = request;
@@ -158,6 +159,10 @@ public abstract class Response {
return promise!=null;
}
public boolean isUpgradedToWebSocket() {
return upgradedToWebSocket;
}
/**
* Initiate an async promise chain using supplyAsync.
* Gets executor from request's CallSession.
@@ -25,7 +25,7 @@ import java.util.Iterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.reliancy.rec.JSONEncoder;
import com.reliancy.io.JsonEncoder;
import com.reliancy.util.CodeException;
import com.reliancy.jabba.ui.Rendering;
import com.reliancy.jabba.ui.Template;
@@ -89,10 +89,10 @@ public class ResponseEncoder implements Appendable,Closeable{
if(out!=null) out.flush();
}
public ResponseEncoder writeBytes(byte[] buf,int offset,int len) throws IOException{
OutputStream os=getOutputStream();
try{
response.transitionTo(ResponseState.WRITING);
getOutputStream().write(buf,offset, len);
os.write(buf,offset, len);
}finally{
if(response.getState() == ResponseState.WRITING) {
response.transitionTo(ResponseState.WRITTEN);
@@ -227,7 +227,7 @@ public class ResponseEncoder implements Appendable,Closeable{
StringBuilder title=new StringBuilder();
StringBuilder detail=new StringBuilder();
CodeException.fillUserMessage(ex, detail, title);
String body=MessageFormat.format(template,JSONEncoder.escape(title),JSONEncoder.escape(detail));
String body=MessageFormat.format(template,JsonEncoder.escape(title),JsonEncoder.escape(detail));
writeString(body);
return this;
}
@@ -213,7 +213,9 @@ public class JettyApp extends App implements Servlet {
new com.reliancy.jabba.servlet.ServletRequest(httpRequest);
final com.reliancy.jabba.servlet.ServletResponse resp =
new com.reliancy.jabba.servlet.ServletResponse(req, httpResponse);
final CallSession ss=CallSession.getInstance();
// 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 ?
@@ -234,7 +236,9 @@ public class JettyApp extends App implements Servlet {
// 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){
@@ -186,6 +186,7 @@ public class ServletResponse extends Response {
*/
@Override
public com.reliancy.jabba.WebSocketSession upgradeToWebSocket(String route, com.reliancy.jabba.Session appSession) throws IOException {
upgradedToWebSocket=true;
return ServletWebSocketSession.create(this,route, appSession);
}
}
@@ -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);
}
}
@@ -8,9 +8,12 @@ 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;
@@ -69,8 +72,37 @@ public class JettyAppTest {
return "no arg response";
}
}
public static class SpaTestApp extends JettyApp {
private final String staticRoot;
private SimpleTestApp app;
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;
@@ -113,6 +145,20 @@ public class JettyAppTest {
}
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;
}
}
/**
@@ -151,6 +197,33 @@ public class JettyAppTest {
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 {
@@ -181,5 +254,89 @@ public class JettyAppTest {
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) {
}
});
}
}
}
@@ -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);
}
}
}