diff --git a/README.md b/README.md index f1c9b0d..fa868f4 100644 --- a/README.md +++ b/README.md @@ -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: - -* org.eclipse.jetty:jetty-server - http servlet container -* org.slf4j:slf4j-simple - logging facility -* com.github.jknack:handlebars - handlebars templating -* com.h2database:h2 - java native comprehensive sql database (so comprehensive it can emulate postgres) -* org.postgresql:postgresql - proven and logical sql server implementation (no hacks like the identity column) -* com.zaxxer:HikariCP - database connection pooling (trust me you need it) +Jabba depends on the following libraries: + +* org.eclipse.jetty:jetty-server - HTTP servlet container +* org.slf4j:slf4j-jdk14 - logging facility +* com.github.jknack:handlebars - handlebars templating +* com.reliancy:bstore-j - storage, record, and utility foundation used by Jabba Normally maven or gradle will auto-resolve and download these dependencies. If you build the libraries yourself there is "fat jabba" task that with combine all dependencies in one jar/zip file. @@ -43,8 +41,8 @@ This is a sonatype nexus repository manager we host here at reliancy. * ~~Asynchronous request processing with CompletableFuture support~~ * ~~WebSocket support for real-time bidirectional communication~~ -With above things complete we ~~will~~ have a library that can be used for new webapps. -Don't have a profile page yet (which goes into app templates) and the dbo layer is basic not like sql alchemy but mostly things are in place. +With above things complete we ~~will~~ have a library that can be used for new webapps. +Don't have a profile page yet (which goes into app templates) but mostly things are in place. At this point we could use jabba to spawn new apps. @@ -119,53 +117,45 @@ For additional validation, implement custom validation in your endpoint methods. # Code Structure -There are 4 major modules all located under com.reliancy. - -They 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 - -## 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. - -You can then do something like: -``` java -for(String token:new Tokenizer(bodyOftext)){ - System.out.println("Word:"+token); -} -``` - -## 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. +Jabba now focuses on web-application concerns and depends on `bstore-j` for the shared +`com.reliancy.util`, `com.reliancy.rec`, and `com.reliancy.dbo` foundations. + +Within this repository the main areas are: + +* `com.reliancy.jabba` - request routing, middleware, sessions, security, and app lifecycle +* `com.reliancy.jabba.servlet` - Jetty/Jakarta servlet integration +* `com.reliancy.jabba.ui` - server-side rendering helpers and UI support classes +* `com.reliancy.io` - Jabba-local protocol encoding and decoding helpers + +If you need the data/storage layer itself, use `bstore-j` directly. Jabba intentionally does +not carry its own SQL implementation anymore. + +## Static Files And SPA Hosting + +Jabba's `FileServer` can now serve both classic static assets and single-page applications. +It supports: + +* root document serving such as `/ -> index.html` +* SPA fallback routing for browser navigation +* `ETag`, `Last-Modified`, and `Content-Length` +* `HEAD` support +* cache-control tuning for assets, index, and fallback responses +* in-memory caching for hot small assets + +Example: + +```java +new FileServer("/", "", work_dir + "/public") + .setIndexFile("index.html") + .setFallbackFile("index.html") + .setAssetCacheControl("public, max-age=3600") + .setIndexCacheControl("no-cache") + .publish(app); +``` + +## jabba + +Finally the center of the library is the module that implements an HTTP servlet (jetty handler actually). Entire machinery is added to perform marshalling and unmarshalling of HTTP requests into and out of java methods. Along the way we also deal with sessions and security and errors and also server side templating. Ideally your app will be a set of REST endpoints that are used by ReactJS or similar front end GUIs but in case you like server-side templating it is available. You application can be any POJO, the entry point is JettyApp from there a request processor is installed which parses your class and discovers all the endpoints. We chain request processors landing on a MethodEndPoint. Middleware is injected, you guessed it, as a request processor in the middle of the chain. Examples include session management, security policy. @@ -310,23 +300,22 @@ public class App extends JettyApp{ work_dir="../var"; } Template.search_path(work_dir,App.class); - JettyApp app=new JettyApp(); - app.addAppSession(); - SecurityPolicy secpol=new SecurityPolicy().setStore(new PlainSecurityStore()); - app.setSecurityPolicy(secpol); - // this is where method parsing happens and app could be any POJO - RoutedEndPoint rep=new RoutedEndPoint().importMethods(app); - app.setRouter(rep); - // it helps to support static file serving too - FileServer fs=new FileServer("/static",work_dir+"/public"); - fs.exportRoutes(app.getRouter()); - // setup menu if you 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()); - //System.out.println("Goodbye World!"); - } + App app=new App(); + app.addAppSession(); + SecurityPolicy secpol=new SecurityPolicy().setStore(new PlainSecurityStore()); + app.setSecurityPolicy(secpol); + Router router=app.getRouter(); + router.importMethods(app); + // it helps to support static file serving too + new FileServer("/static","",work_dir+"/public").publish(app); + // setup menu if you are going to use templates + Menu top_menu=Menu.request(Menu.TOP); + top_menu.add(new MenuItem("home")).addSpacer().add(new MenuItem("login")); + top_menu.setTitle("Jabba"); + router.compile(); + app.begin(new FileConfig()); + //System.out.println("Goodbye World!"); + } // case 1: simplest endpoint (path from method name) @Routed() public String hello(){ @@ -428,4 +417,4 @@ public class App extends JettyApp{ } } -``` \ No newline at end of file +``` diff --git a/build.gradle b/build.gradle index e2568f5..eb615fa 100644 --- a/build.gradle +++ b/build.gradle @@ -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') } @@ -26,23 +26,22 @@ tasks.withType(JavaCompile) { options.compilerArgs << '-parameters' } -dependencies { - def jettyVersion="12.0.15" - implementation "org.eclipse.jetty:jetty-server:${jettyVersion}" - implementation "org.eclipse.jetty.http2:jetty-http2-server:${jettyVersion}" - implementation "org.eclipse.jetty.ee10:jetty-ee10-servlet:${jettyVersion}" - implementation "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server:${jettyVersion}" - implementation "jakarta.servlet:jakarta.servlet-api:6.0.0" +dependencies { + def jettyVersion="12.0.15" + def bstoreVersion="1.0.0-SNAPSHOT" + implementation "org.eclipse.jetty:jetty-server:${jettyVersion}" + implementation "org.eclipse.jetty.http2:jetty-http2-server:${jettyVersion}" + implementation "org.eclipse.jetty.ee10:jetty-ee10-servlet:${jettyVersion}" + implementation "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server:${jettyVersion}" + implementation "jakarta.servlet:jakarta.servlet-api:6.0.0" implementation "org.slf4j:slf4j-jdk14:2.0.16" - //implementation "org.slf4j:slf4j-simple:2.0.16" - //implementation 'com.hubspot.jinjava:jinjava:2.5.10' - implementation 'com.github.jknack:handlebars:4.4.0' - implementation 'com.h2database:h2:2.3.232' - implementation 'org.postgresql:postgresql:42.7.4' - implementation 'com.zaxxer:HikariCP:5.1.0' - testImplementation "junit:junit:4.13.2" - testImplementation "org.eclipse.jetty.websocket:jetty-websocket-jetty-client:${jettyVersion}" -} + //implementation "org.slf4j:slf4j-simple:2.0.16" + //implementation 'com.hubspot.jinjava:jinjava:2.5.10' + implementation 'com.github.jknack:handlebars:4.4.0' + implementation "com.reliancy:bstore-j:${bstoreVersion}" + testImplementation "junit:junit:4.13.2" + testImplementation "org.eclipse.jetty.websocket:jetty-websocket-jetty-client:${jettyVersion}" +} sourceSets { main { diff --git a/src/main/java/com/reliancy/dbo/Action.java b/src/main/java/com/reliancy/dbo/Action.java deleted file mode 100644 index b63243b..0000000 --- a/src/main/java/com/reliancy/dbo/Action.java +++ /dev/null @@ -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,SiphonIterator{ - 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 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 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 it=null; - if(itms!=null){ - it=new SiphonIterator() { - 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 itms){ - SiphonIterator it=null; - if(itms!=null){ - it=new SiphonIterator() { - private final Iterator 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 itms){ - if(items==itms) return this; - if(items!=null){ - try { - items.close(); - } catch (Exception e) { - } - } - items=itms; - return this; - } - protected SiphonIterator getItems(){ - return items; - } - @Override - public Iterator 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; - } -} diff --git a/src/main/java/com/reliancy/dbo/Bag.java b/src/main/java/com/reliancy/dbo/Bag.java deleted file mode 100644 index b16c940..0000000 --- a/src/main/java/com/reliancy/dbo/Bag.java +++ /dev/null @@ -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 extends Observable implements Collection{ - /** event to send to observers. */ - public static final class BagChanged{ - 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 bag; - final int operation; - final Object[] arguments; - - public BagChanged(Bag p,int op,Object ... args){ - bag=p; - operation=op; - arguments=args; - } - public Bag getBag() { - return bag; - } - public int getOperation() { - return operation; - } - public Object[] getArguments() { - return arguments; - } - } - final ArrayList items=new ArrayList<>(); - - public Bag(){ - } - public Bag(Iterable o){ - this(o.iterator()); - } - public Bag(Iterator 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 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 listIterator(){ - return listIterator(0); - } - public ListIterator listIterator(int offset){ - return items.listIterator(offset); - } - @Override - public Iterator iterator() { - return items.iterator(); - } - - @Override - public Object[] toArray() { - return toArray(new Object[size()]); - } - - @Override - public T[] toArray(T[] a) { - return items.toArray(a); - } - - @Override - public boolean add(E e) { - if(items.contains(e)) return true; - if(countObservers()>0){ - BagChanged evt=new Bag.BagChanged<>(this,BagChanged.ADD,e); - setChanged(); - notifyObservers(evt); - } - return items.add(e); - } - public Bag append(E e){ - add(e); - return this; - } - @Override - public boolean remove(Object o) { - if(!contains(o)) return false; - if(countObservers()>0){ - BagChanged evt=new Bag.BagChanged<>(this,BagChanged.REMOVE,o); - setChanged(); - notifyObservers(evt); - } - return items.remove(o); - } - - @Override - public boolean addAll(Collection c) { - if(countObservers()>0){ - BagChanged 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 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); - } - -} diff --git a/src/main/java/com/reliancy/dbo/Check.java b/src/main/java/com/reliancy/dbo/Check.java deleted file mode 100644 index a390f14..0000000 --- a/src/main/java/com/reliancy/dbo/Check.java +++ /dev/null @@ -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 { - 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{ - 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 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; - } - -} diff --git a/src/main/java/com/reliancy/dbo/DBO.java b/src/main/java/com/reliancy/dbo/DBO.java deleted file mode 100644 index d992f26..0000000 --- a/src/main/java/com/reliancy/dbo/DBO.java +++ /dev/null @@ -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 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"); - } - -} diff --git a/src/main/java/com/reliancy/dbo/Entity.java b/src/main/java/com/reliancy/dbo/Entity.java deleted file mode 100644 index 6a417c2..0000000 --- a/src/main/java/com/reliancy/dbo/Entity.java +++ /dev/null @@ -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 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 vals=registry.values(); - while(vals.remove(ent)){} - } - public static final void retract(Class 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 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 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)base); - position0=base_ent.count(); - } - java.lang.reflect.Field[] declaredFields = cls.getDeclaredFields(); - ArrayList 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 iterator(int offset){ - if(offset>0) throw new IllegalArgumentException("Offset not supported"); - final Entity ent=this; - return new Iterator(){ - 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 cls=getType(); - DBO ret=(DBO) cls.newInstance(); - ret.setType(this).setTerminal(t).setStatus(DBO.Status.NEW); - return ret; - } -} diff --git a/src/main/java/com/reliancy/dbo/Field.java b/src/main/java/com/reliancy/dbo/Field.java deleted file mode 100644 index ba66c43..0000000 --- a/src/main/java/com/reliancy/dbo/Field.java +++ /dev/null @@ -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); - } - -} diff --git a/src/main/java/com/reliancy/dbo/FieldSlice.java b/src/main/java/com/reliancy/dbo/FieldSlice.java deleted file mode 100644 index 4a07a35..0000000 --- a/src/main/java/com/reliancy/dbo/FieldSlice.java +++ /dev/null @@ -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,Iterable{ - 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 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); - } -} diff --git a/src/main/java/com/reliancy/dbo/SQL.java b/src/main/java/com/reliancy/dbo/SQL.java deleted file mode 100644 index 7055307..0000000 --- a/src/main/java/com/reliancy/dbo/SQL.java +++ /dev/null @@ -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 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 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;i0) 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 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 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;index0) 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 supplied){ - update(); - append(SQL.WS).id(entity.getName()).append(" SET "); - for(int index=0;index 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 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;pindex0; - } -} diff --git a/src/main/java/com/reliancy/dbo/SQLReader.java b/src/main/java/com/reliancy/dbo/SQLReader.java deleted file mode 100644 index 7a391a3..0000000 --- a/src/main/java/com/reliancy/dbo/SQLReader.java +++ /dev/null @@ -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{ - 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 params=new ArrayList<>(); - sql.check_export(tr.filter, params); - for(int pindex=0;pindex> sql2java=new HashMap<>(); - final HashMap,Integer> java2sql=new HashMap<>(); - public Map,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> 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; - - } -} diff --git a/src/main/java/com/reliancy/dbo/SQLWriter.java b/src/main/java/com/reliancy/dbo/SQLWriter.java deleted file mode 100644 index 2ad0304..0000000 --- a/src/main/java/com/reliancy/dbo/SQLWriter.java +++ /dev/null @@ -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 supplied=new ArrayList(); - protected final ArrayList generated=new ArrayList(); - 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 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;index0 && !generated.isEmpty()){ - try (ResultSet keys = stmt.getGeneratedKeys()) { - if(keys.next()){ - for(int i=0;i0; - } -} diff --git a/src/main/java/com/reliancy/dbo/SiphonIterator.java b/src/main/java/com/reliancy/dbo/SiphonIterator.java deleted file mode 100644 index 74c38c1..0000000 --- a/src/main/java/com/reliancy/dbo/SiphonIterator.java +++ /dev/null @@ -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 extends Iterator, Closeable { -} \ No newline at end of file diff --git a/src/main/java/com/reliancy/dbo/Terminal.java b/src/main/java/com/reliancy/dbo/Terminal.java deleted file mode 100644 index 81f163a..0000000 --- a/src/main/java/com/reliancy/dbo/Terminal.java +++ /dev/null @@ -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 load(Class 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(); - } - } -} diff --git a/src/main/java/com/reliancy/io/Handy.java b/src/main/java/com/reliancy/io/Handy.java new file mode 100644 index 0000000..2ef6b95 --- /dev/null +++ b/src/main/java/com/reliancy/io/Handy.java @@ -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; + } +} diff --git a/src/main/java/com/reliancy/io/JsonDecoder.java b/src/main/java/com/reliancy/io/JsonDecoder.java new file mode 100644 index 0000000..e0699ff --- /dev/null +++ b/src/main/java/com/reliancy/io/JsonDecoder.java @@ -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 decodeObject(CharSequence input) { + Object value = decode(input); + if (value instanceof Map map) { + @SuppressWarnings("unchecked") + Map typed = (Map) map; + return typed; + } + throw new IllegalArgumentException("JSON payload is not an object"); + } + + public static List decodeArray(CharSequence input) { + Object value = decode(input); + if (value instanceof List list) { + @SuppressWarnings("unchecked") + List typed = (List) 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 readObject() { + expect('{'); + LinkedHashMap 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 readArray() { + expect('['); + ArrayList 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); + } +} diff --git a/src/main/java/com/reliancy/io/JsonEncoder.java b/src/main/java/com/reliancy/io/JsonEncoder.java new file mode 100644 index 0000000..94e2589 --- /dev/null +++ b/src/main/java/com/reliancy/io/JsonEncoder.java @@ -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; + } +} diff --git a/src/main/java/com/reliancy/jabba/AppSessionFilter.java b/src/main/java/com/reliancy/jabba/AppSessionFilter.java index fcdf3a3..55edb09 100644 --- a/src/main/java/com/reliancy/jabba/AppSessionFilter.java +++ b/src/main/java/com/reliancy/jabba/AppSessionFilter.java @@ -55,12 +55,15 @@ public class AppSessionFilter extends Processor{ css.setAppSession(ss); } @Override - public void afterServe(Request request, Response response) throws IOException { - CallSession css=CallSession.getInstance(); - AppSession ss=(AppSession) css.getAppSession(); - // Determine if request is HTTPS - boolean isSecure="https".equalsIgnoreCase(request.getProtocol()) || - "https".equalsIgnoreCase(request.getScheme()); + public void afterServe(Request request, Response response) throws IOException { + CallSession css=CallSession.getInstance(); + AppSession ss=(AppSession) css.getAppSession(); + if(ss==null){ + return; + } + // Determine if request is HTTPS + boolean isSecure="https".equalsIgnoreCase(request.getProtocol()) || + "https".equalsIgnoreCase(request.getScheme()); // Set secure=true for HTTPS, HttpOnly=true always for security response.setCookie(KEY_NAME,ss.id,15*60,isSecure,true); } diff --git a/src/main/java/com/reliancy/jabba/FileServer.java b/src/main/java/com/reliancy/jabba/FileServer.java index 08ad458..b443ac1 100644 --- a/src/main/java/com/reliancy/jabba/FileServer.java +++ b/src/main/java/com/reliancy/jabba/FileServer.java @@ -5,20 +5,20 @@ Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3. You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html. You may not use this file except in compliance with the License. */ -package com.reliancy.jabba; -import com.reliancy.util.Handy; -import com.reliancy.util.LRUCache; -import com.reliancy.util.Resources; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URL; -import java.net.URLConnection; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.concurrent.CompletableFuture; -import org.slf4j.Logger; +package com.reliancy.jabba; +import com.reliancy.util.Handy; +import com.reliancy.util.LRUCache; +import com.reliancy.util.Resources; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Iterator; +import org.slf4j.Logger; /** FileServer is an module and endpoint that exposes multiple URLs thru which files are served. * First it will be just get(tting), later @@ -29,41 +29,73 @@ import org.slf4j.Logger; * Please note Router is for routing. * Bucket is there to process input/output given verbs over resources under it. */ -public class FileServer extends EndPoint implements AppModule,Resources.PathRewrite{ - /** Bucket interface to abstract i/o and provide easier extensibility. - * asContainer matches path and then returns local-to-packet path. - * signature returns a hash over lastModified or content that reflects modification. - */ - public static interface Bucket{ - String getPrefix(); - String asContained(String path); - boolean equals(String pref); - InputStream openSource(String local_path,FileServer user) throws IOException; - OutputStream openSink(String local_path,FileServer user) throws IOException; - String signature(String local_path); - } - public static class FileBucket implements Bucket{ - final String prefix; - String[] extAllowed; - Object[] domain; - LRUCache hit_history=new LRUCache<>(2*Runtime.getRuntime().availableProcessors()); - - public FileBucket(String prefix){ - this.prefix=prefix; - extAllowed=new String[]{}; - domain=new Object[]{}; +public class FileServer extends EndPoint implements AppModule,Resources.PathRewrite{ + public static class CachedAsset{ + final URL source; + final long lastModified; + final long contentLength; + final byte[] content; + final String signature; + + public CachedAsset(URL source,long lastModified,long contentLength,byte[] content){ + this.source=source; + this.lastModified=lastModified; + this.contentLength=contentLength; + this.content=content; + if(lastModified>0 || contentLength>=0){ + signature=Handy.hashMD5(lastModified+":"+contentLength); + }else if(content!=null){ + signature=Handy.hashMD5(new String(content,java.nio.charset.StandardCharsets.ISO_8859_1)); + }else{ + signature=null; + } + } + public InputStream openSource() throws IOException{ + if(content!=null) return new ByteArrayInputStream(content); + if(source==null) return null; + return source.openStream(); + } + public boolean hasContent(){ + return content!=null; + } + } + /** Bucket interface to abstract i/o and provide easier extensibility. + * asContainer matches path and then returns local-to-packet path. + * signature returns a hash over lastModified or content that reflects modification. + */ + public static interface Bucket{ + String getPrefix(); + String asContained(String path); + boolean equals(String pref); + InputStream openSource(String local_path,FileServer user) throws IOException; + OutputStream openSink(String local_path,FileServer user) throws IOException; + String signature(String local_path); + Long lastModified(String local_path); + Long contentLength(String local_path); + CachedAsset getAsset(String local_path,FileServer user) throws IOException; + } + public static class FileBucket implements Bucket{ + final String prefix; + String[] extAllowed; + Object[] domain; + LRUCache hit_history=new LRUCache<>(64); + + public FileBucket(String prefix){ + this.prefix=prefix; + extAllowed=new String[]{}; + domain=new Object[]{}; } @Override public final String getPrefix(){return prefix;} - @Override - public String asContained(String path) { - if(!path.startsWith(prefix)) return null; // not contained - String local_path=path.replace(prefix,""); - if(extAllowed.length==0) return local_path; - for(String ext:extAllowed){ - if(path.endsWith(ext)){ - return local_path; - } + @Override + public String asContained(String path) { + if(!path.startsWith(prefix)) return null; // not contained + String local_path=path.substring(prefix.length()); + if(extAllowed.length==0) return local_path; + for(String ext:extAllowed){ + if(path.endsWith(ext)){ + return local_path; + } } return null; } @@ -79,35 +111,71 @@ public class FileServer extends EndPoint implements AppModule,Resources.PathRewr domain=sp; return this; } - public Object[] getDomain(){ - return (domain!=null && domain.length>0)?domain:Resources.search_path; - } - public InputStream openSource(String local_path,FileServer user) throws IOException{ - Object[] sp=getDomain(); - URL f=Resources.findFirst(user,local_path, sp); - if(f==null) return null; // skip if rpath not located - URLConnection conn=f.openConnection(); - hit_history.put(local_path,conn.getLastModified()); // pull last modified for signature - return conn.getInputStream(); - } - public OutputStream openSink(String local_path,FileServer user) throws IOException{ - return null; - } - public String signature(String local_path){ - Long last_modified=hit_history.get(local_path); - if(last_modified==null) return null; - String sig=String.valueOf(last_modified); - return Handy.hashMD5(sig); - } - } - final ArrayList buckets=new ArrayList<>(); - String diskPrefix; // will be prefixed to source if file - String classPrefix; // will be prefixed to source if class - String urlPrefix; // will be prefixed to source if URL - public FileServer(String url_path,String offset,Object ... source){ - super(null); - diskPrefix=classPrefix=offset; - addBucket(new FileBucket(url_path).setDomain(source)); + public Object[] getDomain(){ + return (domain!=null && domain.length>0)?domain:Resources.search_path; + } + @Override + public CachedAsset getAsset(String local_path,FileServer user) throws IOException{ + Object[] sp=getDomain(); + URL f=Resources.findFirst(user,local_path, sp); + if(f==null) return null; + URLConnection conn=f.openConnection(); + long lastModified=conn.getLastModified(); + long contentLength=conn.getContentLengthLong(); + CachedAsset cached=hit_history.get(local_path); + if(cached!=null + && cached.lastModified==lastModified + && cached.contentLength==contentLength){ + return cached; + } + byte[] content=null; + if(user.shouldCacheContent(local_path,contentLength)){ + try(InputStream is=conn.getInputStream()){ + content=is.readAllBytes(); + contentLength=content.length; + } + } + CachedAsset asset=new CachedAsset(f,lastModified,contentLength,content); + hit_history.remove(local_path); + hit_history.put(local_path,asset); + return asset; + } + public InputStream openSource(String local_path,FileServer user) throws IOException{ + CachedAsset asset=getAsset(local_path,user); + return asset!=null?asset.openSource():null; + } + public OutputStream openSink(String local_path,FileServer user) throws IOException{ + return null; + } + public String signature(String local_path){ + CachedAsset asset=hit_history.get(local_path); + return asset!=null?asset.signature:null; + } + @Override + public Long lastModified(String local_path){ + CachedAsset asset=hit_history.get(local_path); + return asset!=null && asset.lastModified>0?asset.lastModified:null; + } + @Override + public Long contentLength(String local_path){ + CachedAsset asset=hit_history.get(local_path); + return asset!=null && asset.contentLength>=0?asset.contentLength:null; + } + } + final ArrayList buckets=new ArrayList<>(); + String diskPrefix; // will be prefixed to source if file + String classPrefix; // will be prefixed to source if class + String urlPrefix; // will be prefixed to source if URL + String indexFile; + String fallbackFile; + String assetCacheControl="public, max-age=3600"; + String indexCacheControl="no-cache"; + String fallbackCacheControl="no-cache"; + long memoryCacheContentLimit=256*1024; + public FileServer(String url_path,String offset,Object ... source){ + super(null); + diskPrefix=classPrefix=offset; + addBucket(new FileBucket(url_path).setDomain(source)); } public FileServer(){ super(null); @@ -120,10 +188,34 @@ public class FileServer extends EndPoint implements AppModule,Resources.PathRewr classPrefix=prefix; return this; } - public FileServer setURLOffset(String offset){ - urlPrefix=offset; - return this; - } + public FileServer setURLOffset(String offset){ + urlPrefix=offset; + return this; + } + public FileServer setIndexFile(String path){ + indexFile=path; + return this; + } + public FileServer setFallbackFile(String path){ + fallbackFile=path; + return this; + } + public FileServer setAssetCacheControl(String value){ + assetCacheControl=value; + return this; + } + public FileServer setIndexCacheControl(String value){ + indexCacheControl=value; + return this; + } + public FileServer setFallbackCacheControl(String value){ + fallbackCacheControl=value; + return this; + } + public FileServer setMemoryCacheContentLimit(long value){ + memoryCacheContentLimit=value; + return this; + } /** * we prefix our path for disk and class contexts. */ @@ -136,41 +228,35 @@ public class FileServer extends EndPoint implements AppModule,Resources.PathRewr return path; } @Override - public void serve(Request request, Response response) throws IOException { - String verb=request.getVerb(); - String path=request.getPath(); - Logger logger=log(); - boolean atDebug=logger.isDebugEnabled(); - if(atDebug) logger.debug("{}:{}",verb,path); - if(HTTP.VERB_GET.equals(verb)){ - for(Bucket bucket:buckets){ - String local_path=bucket.asContained(path); - if(local_path==null) continue; // this bucket is not accepting - try(InputStream ins=bucket.openSource(local_path,this)){ - if(ins==null) continue; // url did not take - String etag=bucket.signature(local_path); - if(etag!=null){ - response.setHeader("Cache-Control","max-age=0, must-revalidate"); - response.setHeader("ETag",etag); - String etag_old=request.getHeader("If-None-Match"); - if(etag.equals(etag_old)){ - // we got same etag no change - response.setStatus(Response.HTTP_NOT_MODIFIED); - return; - } - } - if(atDebug) logger.debug("\tfound:"+local_path); - String ctype=HTTP.ext2mime(local_path); - response.setStatus(Response.HTTP_OK); - response.setContentType(ctype); - ResponseEncoder enc=response.getEncoder(); - enc.writeStream(ins); - return; // we got something - } - } - }else{ - // these verbs are not supported - } + public void serve(Request request, Response response) throws IOException { + String verb=request.getVerb(); + String path=request.getPath(); + Logger logger=log(); + boolean atDebug=logger.isDebugEnabled(); + if(atDebug) logger.debug("{}:{}",verb,path); + if(HTTP.VERB_GET.equals(verb) || HTTP.VERB_HEAD.equals(verb)){ + for(Bucket bucket:buckets){ + String local_path=bucket.asContained(path); + if(local_path==null) continue; // this bucket is not accepting + if(isUnsafePath(local_path)){ + response.setStatus(Response.HTTP_BAD_REQUEST); + response.getEncoder().writeln("invalid file path:"+path); + logger.warn("rejected unsafe path:{}",path); + return; + } + String resolved_path=resolveLocalPath(local_path); + if(tryServe(bucket,resolved_path,request,response,atDebug,false)){ + return; + } + if(shouldServeFallback(local_path,request) && fallbackFile!=null){ + if(tryServe(bucket,fallbackFile,request,response,atDebug,true)){ + return; + } + } + } + }else{ + // these verbs are not supported + } response.setStatus(Response.HTTP_NOT_FOUND); response.getEncoder().writeln("missing file:"+path); logger.error("not found:{}",path); @@ -214,11 +300,106 @@ public class FileServer extends EndPoint implements AppModule,Resources.PathRewr for(Bucket b:buckets) if(b.equals(url_path)) return b; return null; } - public Iterator enumBuckets(){ - return buckets.iterator(); - } - public void publish(App app) { - Router rep=app.getRouter(); - for(Bucket b:buckets) rep.addRoute("GET",b.getPrefix()+".*",this); - } -} + public Iterator enumBuckets(){ + return buckets.iterator(); + } + public void publish(App app) { + Router rep=app.getRouter(); + for(Bucket b:buckets){ + rep.addRoute("GET",b.getPrefix()+".*",this); + rep.addRoute("HEAD",b.getPrefix()+".*",this); + } + } + protected String resolveLocalPath(String local_path){ + String normalized=normalizeLocalPath(local_path); + if((normalized==null || normalized.isEmpty()) && indexFile!=null){ + return indexFile; + } + return normalized; + } + protected String normalizeLocalPath(String local_path){ + if(local_path==null) return null; + local_path=local_path.replace('\\','/'); + while(local_path.startsWith("/")) local_path=local_path.substring(1); + if(local_path.equals(".")) return ""; + return local_path; + } + protected boolean shouldServeFallback(String local_path,Request request){ + String normalized=normalizeLocalPath(local_path); + if(normalized==null) return false; + if(!acceptsHtml(request)) return false; + if(normalized.isEmpty()) return fallbackFile!=null; + int lastSlash=normalized.lastIndexOf('/'); + int lastDot=normalized.lastIndexOf('.'); + return lastDot < lastSlash + 1; + } + protected boolean tryServe(Bucket bucket,String local_path,Request request,Response response,boolean atDebug,boolean isFallback) throws IOException{ + if(local_path==null) return false; + CachedAsset asset=bucket.getAsset(local_path,this); + if(asset==null) return false; + try(InputStream ins=asset.openSource()){ + if(ins==null) return false; + response.setHeader("X-Content-Type-Options","nosniff"); + String etag=asset.signature; + if(etag!=null){ + response.setHeader("Cache-Control",cacheControlFor(local_path,isFallback)); + response.setHeader("ETag",etag); + String etag_old=request.getHeader("If-None-Match"); + if(etag.equals(etag_old)){ + response.setStatus(Response.HTTP_NOT_MODIFIED); + return true; + } + }else{ + response.setHeader("Cache-Control",cacheControlFor(local_path,isFallback)); + } + Long lastModified=asset.lastModified>0?asset.lastModified:null; + if(lastModified!=null && lastModified>0){ + response.setHeader("Last-Modified",java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME.format( + java.time.Instant.ofEpochMilli(lastModified).atZone(java.time.ZoneOffset.UTC) + )); + } + Long contentLength=asset.contentLength>=0?asset.contentLength:null; + if(contentLength!=null && contentLength>=0){ + response.setHeader("Content-Length",String.valueOf(contentLength)); + } + if(atDebug) log().debug("\tfound:{}",local_path); + String ctype=HTTP.ext2mime(local_path); + response.setStatus(Response.HTTP_OK); + response.setContentType(ctype!=null?ctype:HTTP.MIME_BYTES); + if(!HTTP.VERB_HEAD.equals(request.getVerb())){ + if(asset.hasContent()){ + response.getEncoder().writeBytes(asset.content,0,asset.content.length); + }else{ + response.getEncoder().writeStream(ins); + } + } + return true; + } + } + protected boolean shouldCacheContent(String local_path,long contentLength){ + if(contentLength<0) return false; + return contentLength<=memoryCacheContentLimit; + } + protected boolean acceptsHtml(Request request){ + if(request==null) return true; + String accept=request.getHeader("Accept"); + if(accept==null || accept.trim().isEmpty()) return true; + accept=accept.toLowerCase(); + return accept.contains("text/html") || accept.contains("application/xhtml+xml"); + } + protected boolean isUnsafePath(String local_path){ + String normalized=normalizeLocalPath(local_path); + if(normalized==null) return false; + return normalized.equals("..") || normalized.startsWith("../") || normalized.contains("/../"); + } + protected String cacheControlFor(String local_path,boolean isFallback){ + String normalized=normalizeLocalPath(local_path); + if(normalized==null || normalized.isEmpty() || normalized.equals(indexFile)){ + return indexCacheControl; + } + if(isFallback){ + return fallbackCacheControl; + } + return assetCacheControl; + } +} diff --git a/src/main/java/com/reliancy/jabba/ResponseEncoder.java b/src/main/java/com/reliancy/jabba/ResponseEncoder.java index dfad6c4..97ff35d 100644 --- a/src/main/java/com/reliancy/jabba/ResponseEncoder.java +++ b/src/main/java/com/reliancy/jabba/ResponseEncoder.java @@ -25,8 +25,8 @@ import java.util.Iterator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.reliancy.rec.JSONEncoder; -import com.reliancy.util.CodeException; +import com.reliancy.io.JsonEncoder; +import com.reliancy.util.CodeException; import com.reliancy.jabba.ui.Rendering; import com.reliancy.jabba.ui.Template; @@ -88,15 +88,15 @@ public class ResponseEncoder implements Appendable,Closeable{ if(writer!=null) writer.flush(); if(out!=null) out.flush(); } - public ResponseEncoder writeBytes(byte[] buf,int offset,int len) throws IOException{ - - try{ - response.transitionTo(ResponseState.WRITING); - getOutputStream().write(buf,offset, len); - }finally{ - if(response.getState() == ResponseState.WRITING) { - response.transitionTo(ResponseState.WRITTEN); - } + public ResponseEncoder writeBytes(byte[] buf,int offset,int len) throws IOException{ + OutputStream os=getOutputStream(); + try{ + response.transitionTo(ResponseState.WRITING); + os.write(buf,offset, len); + }finally{ + if(response.getState() == ResponseState.WRITING) { + response.transitionTo(ResponseState.WRITTEN); + } } return this; } @@ -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; } diff --git a/src/main/java/com/reliancy/jabba/servlet/JettyApp.java b/src/main/java/com/reliancy/jabba/servlet/JettyApp.java index 5b916cf..5dd25d7 100644 --- a/src/main/java/com/reliancy/jabba/servlet/JettyApp.java +++ b/src/main/java/com/reliancy/jabba/servlet/JettyApp.java @@ -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 ? diff --git a/src/main/java/com/reliancy/rec/DecoderSink.java b/src/main/java/com/reliancy/rec/DecoderSink.java deleted file mode 100644 index 7f9a633..0000000 --- a/src/main/java/com/reliancy/rec/DecoderSink.java +++ /dev/null @@ -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); -} diff --git a/src/main/java/com/reliancy/rec/Hdr.java b/src/main/java/com/reliancy/rec/Hdr.java deleted file mode 100644 index 5034aac..0000000 --- a/src/main/java/com/reliancy/rec/Hdr.java +++ /dev/null @@ -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 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 castAs(Class clazz){ - return clazz.cast(this); - } - public List getOwnSlots(){ - return keys; - } - public boolean isOwned(Slot s){ - return keys.contains(s); - } - public Iterator iterator(int offset){ - return keys.listIterator(offset); - } - public int indexOf(String name){ - return indexOf(name,0); - } - public int indexOf(String name,int ofs){ - Iterator 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 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(); - } -} diff --git a/src/main/java/com/reliancy/rec/JSON.java b/src/main/java/com/reliancy/rec/JSON.java deleted file mode 100644 index c790181..0000000 --- a/src/main/java/com/reliancy/rec/JSON.java +++ /dev/null @@ -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(); - } - -} diff --git a/src/main/java/com/reliancy/rec/JSONDecoder.java b/src/main/java/com/reliancy/rec/JSONDecoder.java deleted file mode 100644 index a4b8533..0000000 --- a/src/main/java/com/reliancy/rec/JSONDecoder.java +++ /dev/null @@ -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 stack=new LinkedList(); - /** 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= 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; - } - -} diff --git a/src/main/java/com/reliancy/rec/JSONEncoder.java b/src/main/java/com/reliancy/rec/JSONEncoder.java deleted file mode 100644 index f239c45..0000000 --- a/src/main/java/com/reliancy/rec/JSONEncoder.java +++ /dev/null @@ -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 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; - } - -} diff --git a/src/main/java/com/reliancy/rec/Obj.java b/src/main/java/com/reliancy/rec/Obj.java deleted file mode 100644 index bdf6f1e..0000000 --- a/src/main/java/com/reliancy/rec/Obj.java +++ /dev/null @@ -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 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 k,List 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;pos0) 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;pos0) 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; - } - -} diff --git a/src/main/java/com/reliancy/rec/Rec.java b/src/main/java/com/reliancy/rec/Rec.java deleted file mode 100644 index dfa2470..0000000 --- a/src/main/java/com/reliancy/rec/Rec.java +++ /dev/null @@ -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; - } -} diff --git a/src/main/java/com/reliancy/rec/Slot.java b/src/main/java/com/reliancy/rec/Slot.java deleted file mode 100644 index 96c55a5..0000000 --- a/src/main/java/com/reliancy/rec/Slot.java +++ /dev/null @@ -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; - } -} diff --git a/src/main/java/com/reliancy/rec/TextDecoder.java b/src/main/java/com/reliancy/rec/TextDecoder.java deleted file mode 100644 index 4c11213..0000000 --- a/src/main/java/com/reliancy/rec/TextDecoder.java +++ /dev/null @@ -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); -} diff --git a/src/main/java/com/reliancy/rec/Vec.java b/src/main/java/com/reliancy/rec/Vec.java deleted file mode 100644 index 1167104..0000000 --- a/src/main/java/com/reliancy/rec/Vec.java +++ /dev/null @@ -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); -} diff --git a/src/main/java/com/reliancy/util/CodeException.java b/src/main/java/com/reliancy/util/CodeException.java deleted file mode 100644 index a63d310..0000000 --- a/src/main/java/com/reliancy/util/CodeException.java +++ /dev/null @@ -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 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 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; - } -} diff --git a/src/main/java/com/reliancy/util/Handy.java b/src/main/java/com/reliancy/util/Handy.java deleted file mode 100644 index d86ca55..0000000 --- a/src/main/java/com/reliancy/util/Handy.java +++ /dev/null @@ -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 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 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;i0?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 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;i0) 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 decrypt(String key,String m){ - m=decryptString(key,m); - Map 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> 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=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 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 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.Entrye: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 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(index0 && 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(index0){ - // 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 Iterator chainIterators(Iterator...its){ - return new JointIterator(its); - } -} diff --git a/src/main/java/com/reliancy/util/JointIterator.java b/src/main/java/com/reliancy/util/JointIterator.java deleted file mode 100644 index 31d1814..0000000 --- a/src/main/java/com/reliancy/util/JointIterator.java +++ /dev/null @@ -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 implements Iterator { - final Iterator iterators[]; - int cursor; - @SafeVarargs - public JointIterator(Iterator ...its){ - this.iterators=its; - cursor=0; - } - @Override - public boolean hasNext() { - while(cursor{ - public static interface Allocator{ - V request(K key); - } - public static interface Disposer{ - void release(K key,V val); - } - final Map data; - int capacity; - final LinkedList order=new LinkedList<>(); - Allocator allocator; - Disposer disposer; - - public LRUCache(int capacity,Map backend){ - this.capacity=capacity; - data=backend!=null?backend:new HashMap(); - } - public LRUCache(int capacity){ - this(capacity,null); - } - public LRUCache setAllocator(Allocator a){ - allocator=a; - return this; - } - public LRUCache setDisposer(Disposer 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(); - } -} diff --git a/src/main/java/com/reliancy/util/Log.java b/src/main/java/com/reliancy/util/Log.java deleted file mode 100644 index 5283442..0000000 --- a/src/main/java/com/reliancy/util/Log.java +++ /dev/null @@ -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); - } - } - -} diff --git a/src/main/java/com/reliancy/util/Path.java b/src/main/java/com/reliancy/util/Path.java deleted file mode 100644 index 40bb313..0000000 --- a/src/main/java/com/reliancy/util/Path.java +++ /dev/null @@ -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: - *
 {@code PROTOCOL://USER:PWD@MACHINE:PORT/DATABASE?key=val&... } 
- * 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=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); - } -} diff --git a/src/main/java/com/reliancy/util/Resources.java b/src/main/java/com/reliancy/util/Resources.java deleted file mode 100644 index 10e2bf9..0000000 --- a/src/main/java/com/reliancy/util/Resources.java +++ /dev/null @@ -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); - } - -} diff --git a/src/main/java/com/reliancy/util/ResultCode.java b/src/main/java/com/reliancy/util/ResultCode.java deleted file mode 100644 index 3b6fe72..0000000 --- a/src/main/java/com/reliancy/util/ResultCode.java +++ /dev/null @@ -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 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"); -} diff --git a/src/main/java/com/reliancy/util/Tokenizer.java b/src/main/java/com/reliancy/util/Tokenizer.java deleted file mode 100644 index 123eecd..0000000 --- a/src/main/java/com/reliancy/util/Tokenizer.java +++ /dev/null @@ -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,Iterator{ - 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 buf=new ArrayList(); - 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 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=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(offset0){ - 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; - } - */ - -} diff --git a/src/test/java/com/reliancy/dbo/TerminalTest.java b/src/test/java/com/reliancy/dbo/TerminalTest.java deleted file mode 100644 index 8739638..0000000 --- a/src/test/java/com/reliancy/dbo/TerminalTest.java +++ /dev/null @@ -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); - } - -} diff --git a/src/test/java/com/reliancy/jabba/JettyAppTest.java b/src/test/java/com/reliancy/jabba/JettyAppTest.java index 898caaa..d0452c4 100644 --- a/src/test/java/com/reliancy/jabba/JettyAppTest.java +++ b/src/test/java/com/reliancy/jabba/JettyAppTest.java @@ -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; + + 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 SimpleTestApp app; + 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"), "spa shell"); + 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) { + } + }); + } + } } diff --git a/src/test/java/com/reliancy/rec/ObjTest.java b/src/test/java/com/reliancy/rec/ObjTest.java deleted file mode 100644 index 6b5d3ef..0000000 --- a/src/test/java/com/reliancy/rec/ObjTest.java +++ /dev/null @@ -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); - } - -} diff --git a/src/test/java/com/reliancy/util/HandyTest.java b/src/test/java/com/reliancy/util/HandyTest.java deleted file mode 100644 index 2905fd3..0000000 --- a/src/test/java/com/reliancy/util/HandyTest.java +++ /dev/null @@ -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); - } - - } - -}