Refocus Jabba on web runtime and improve file serving

This commit is contained in:
Amer Agovic
2026-05-01 13:38:37 -05:00
parent 6efc097544
commit 847c69f112
46 changed files with 1068 additions and 5840 deletions
-230
View File
@@ -1,230 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
/** Description of a terminal operation with a slice of dbo objects as input or output.
* This object is not just for reading but also bulk updating.
* It will be used to describe a multi DBO read or write and to then also track results.
* At its core are action traits which are classes that define either loading,saving or deleting.
* The items field is a consumable object when consumed the action is done.
* So for loading we iterate once done it cannot be done again. Also when items are provided for saving
* once iterated over and saved we are done.
*/
public class Action implements Iterable<DBO>,SiphonIterator<DBO>{
public static class Trait{
public String toString(){return getClass().getSimpleName();}
}
public static class Load extends Trait{
int limit,offset;
Check filter;
}
public static class Save extends Trait{
}
public static class Delete extends Trait{
Check filter;
}
Terminal terminal;
Trait trait;
Entity entity;
Object[] params;
SiphonIterator<DBO> items;
public Action(){
trait=null;
}
public Action(Trait t){
trait=t;
}
public Action(Terminal t){
terminal=t;
trait=null;
}
public Action execute() throws IOException{
return terminal.execute(this);
}
public Terminal getTerminal() {
return terminal;
}
public Action setTerminal(Terminal terminal) {
this.terminal = terminal;
return this;
}
public Trait getTrait() {
return trait;
}
public Action setTrait(Trait t) {
this.trait = t;
return this;
}
public Entity getEntity() {
return entity;
}
public Action setEntity(Entity entity) {
this.entity = entity;
return this;
}
public void clear(){
terminal=null;
trait=null;
entity=null;
setItems((DBO)null);
}
public Action load(Entity ent){
trait=new Load();
entity=ent;
return this;
}
public Action load(Class<? extends DBO> cls){
trait=new Load();
entity=Entity.recall(cls);
return this;
}
public Action save(Entity ent){
trait=new Save();
entity=ent;
return this;
}
public Action delete(Entity ent){
trait=new Delete();
entity=ent;
return this;
}
public Action params(Object...p){
params=p;
return this;
}
public Action setItems(final DBO ...itms){
SiphonIterator<DBO> it=null;
if(itms!=null){
it=new SiphonIterator<DBO>() {
private int index = 0;
@Override
public boolean hasNext() {
return itms.length > index;
}
@Override
public DBO next() {
return itms[index++];
}
@Override
public void close() throws IOException {
}
};
}
return setItems(it);
}
public Action setItems(final Collection<DBO> itms){
SiphonIterator<DBO> it=null;
if(itms!=null){
it=new SiphonIterator<DBO>() {
private final Iterator<DBO> str = itms.iterator();
@Override
public boolean hasNext() {
return str.hasNext();
}
@Override
public DBO next() {
return str.next();
}
@Override
public void close() throws IOException {
}
};
}
return setItems(it);
}
public Action setItems(SiphonIterator<DBO> itms){
if(items==itms) return this;
if(items!=null){
try {
items.close();
} catch (Exception e) {
}
}
items=itms;
return this;
}
protected SiphonIterator<DBO> getItems(){
return items;
}
@Override
public Iterator<DBO> iterator() {
return this;
}
@Override
public boolean hasNext() {
return items!=null?items.hasNext():false;
}
@Override
public DBO next() {
return items.next();
}
@Override
public void close() throws IOException {
if(items!=null){
items.close();
items=null;
if(terminal!=null) terminal.end(this);
}
}
public Action limit(int max) {
((Load)trait).limit=max;
return this;
}
public Action filterBy(Check... c){
Check filter=null;
if(c!=null){
if(c.length>1) filter=Check.and(c);
else filter=c[0];
}
if(trait instanceof Load){
((Load)trait).filter=filter;
}else
if(trait instanceof Delete){
((Delete)trait).filter=filter;
}else{
throw new IllegalStateException("filtering not supported by trait:"+trait);
}
return this;
}
public Check getFilter(){
if(trait instanceof Load){
return ((Load)trait).filter;
}else
if(trait instanceof Delete){
return ((Delete)trait).filter;
}else{
throw new IllegalStateException("filtering not supported by trait:"+trait);
}
}
public Action if_pk(Object... id) {
Field pk=entity.getPk();
return filterBy(pk.eq(id));
}
public DBO first() {
try{
if(this.hasNext()){
return this.next();
}else{
return null;
}
}finally{
clear();
}
}
public boolean isDone(){
return items==null || items.hasNext()==false;
}
}
-168
View File
@@ -1,168 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Observable;
/** A more or less virtual collection of items.
* this object is a suitable holder of resultsets. it will be overridable so
* we can create specialized virtual holders that use backends. by itself it will
* implement in memory list.
* also this class is an observable and we can monitor update to it.
*/
public class Bag<E> extends Observable implements Collection<E>{
/** event to send to observers. */
public static final class BagChanged<E>{
public static final int ADD=0;
public static final int REMOVE=1;
public static final int ACCESS=2;
public static final int POST_LOAD=3;
public static final int PRE_SAVE=4;
final Bag<E> bag;
final int operation;
final Object[] arguments;
public BagChanged(Bag<E> p,int op,Object ... args){
bag=p;
operation=op;
arguments=args;
}
public Bag<E> getBag() {
return bag;
}
public int getOperation() {
return operation;
}
public Object[] getArguments() {
return arguments;
}
}
final ArrayList<E> items=new ArrayList<>();
public Bag(){
}
public Bag(Iterable<E> o){
this(o.iterator());
}
public Bag(Iterator<E> o){
while(o.hasNext()) add(o.next());
}
@Override
public int size() {
return items.size();
}
@Override
public boolean isEmpty() {
return size()==0;
}
@Override
public boolean contains(Object o) {
final Iterator<E> it=iterator();
while(it.hasNext()){
final E e=it.next();
if(e!=null && o!=null && e.equals(o)) return true;
else if(e==o) return true;
}
return false;
}
@Override
public boolean containsAll(Collection<?> c) {
for (Object e : c) if (!contains(e)) return false;
return true;
}
public ListIterator<E> listIterator(){
return listIterator(0);
}
public ListIterator<E> listIterator(int offset){
return items.listIterator(offset);
}
@Override
public Iterator<E> iterator() {
return items.iterator();
}
@Override
public Object[] toArray() {
return toArray(new Object[size()]);
}
@Override
public <T> T[] toArray(T[] a) {
return items.toArray(a);
}
@Override
public boolean add(E e) {
if(items.contains(e)) return true;
if(countObservers()>0){
BagChanged<E> evt=new Bag.BagChanged<>(this,BagChanged.ADD,e);
setChanged();
notifyObservers(evt);
}
return items.add(e);
}
public Bag<E> append(E e){
add(e);
return this;
}
@Override
public boolean remove(Object o) {
if(!contains(o)) return false;
if(countObservers()>0){
BagChanged<E> evt=new Bag.BagChanged<>(this,BagChanged.REMOVE,o);
setChanged();
notifyObservers(evt);
}
return items.remove(o);
}
@Override
public boolean addAll(Collection<? extends E> c) {
if(countObservers()>0){
BagChanged<E> evt=new Bag.BagChanged<>(this,BagChanged.ADD,c.toArray());
setChanged();
notifyObservers(evt);
}
if(c==null || c.size()==0) return false;
c.forEach(e->{this.append(e);});
return true;
}
@Override
public boolean removeAll(Collection<?> c) {
if(countObservers()>0){
BagChanged<E> evt=new Bag.BagChanged<>(this,BagChanged.REMOVE,c!=null?c.toArray():null);
setChanged();
notifyObservers(evt);
}
if(c!=null){
return items.removeAll(c);
}else{
items.clear();
return true;
}
}
@Override
public boolean retainAll(Collection<?> c) {
return items.retainAll(c);
}
@Override
public void clear() {
removeAll(null);
}
}
-239
View File
@@ -1,239 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.util.Iterator;
/** constraint on a field.
* conditions can be leafs or groups such as and,or,not
*/
public class Check implements Iterable<Check> {
public static abstract class Op{
public abstract boolean met(Check c,Object val);
}
/** logical AND operation. */
public static Op AND=new Op(){
public String toString(){return "AND";}
public boolean met(Check c,Object val){
return true;
}
};
/** logical OR operation. */
public static Op OR=new Op(){
public String toString(){return "OR";}
public boolean met(Check c,Object val){
return true;
}
};
/** logical NOT operation. */
public static Op NOT=new Op(){
public String toString(){return "NOT";}
public boolean met(Check c,Object val){
return true;
}
};
/** arithmetic equal test. */
public static Op EQ=new Op(){
public String toString(){return "=";}
public boolean met(Check c,Object val){
return true;
}
};
/** arithmetic negated equal test. */
public static Op NEQ=new Op(){
public String toString(){return "<>";}
public boolean met(Check c,Object val){
return true;
}
};
/** greater than check. */
public static Op GT=new Op(){
public String toString(){return ">";}
public boolean met(Check c,Object val){
return true;
}
};
/** greater than or equal check. */
public static Op GTE=new Op(){
public String toString(){return ">=";}
public boolean met(Check c,Object val){
return true;
}
};
/** less than check. */
public static Op LT=new Op(){
public String toString(){return "<";}
public boolean met(Check c,Object val){
return true;
}
};
/** less than or equal check. */
public static Op LTE=new Op(){
public String toString(){return "<=";}
public boolean met(Check c,Object val){
return true;
}
};
/** like check case insensitive. */
public static Op LIKE=new Op(){
public String toString(){return "LIKE";}
public boolean met(Check c,Object val){
return true;
}
};
/** set membership check. */
public static Op IN=new Op(){
public String toString(){return "IN";}
public boolean met(Check c,Object val){
return true;
}
};
/** negated set membership check. */
public static Op NOT_IN=new Op(){
public String toString(){return "NOT IN";}
public boolean met(Check c,Object val){
return true;
}
};
/** iterator over checks.
*
*/
public static class CheckIterator implements Iterator<Check>{
final Check root;
Check cur;
int index;
public CheckIterator(Check ch){
root=ch;
cur=root;
index=0;
}
@Override
public boolean hasNext() {
return cur.isLeaf()==false && index<cur.args.length;
}
@Override
public Check next() {
return (Check)cur.args[index++];
}
}
public static Check and(Check... c) {
return new Check(AND,c);
}
public static Check all(Check... c) {
return new Check(AND,c);
}
public static Check or(Check... c) {
return new Check(OR,c);
}
public static Check any(Check... c) {
return new Check(OR,c);
}
public static Check not(Check... c) {
return new Check(NOT,c);
}
public static Check none(Check... c) {
return new Check(NOT,c);
}
public static Check eq(Field pk, Object... args) {
Object id=args;
if(id!=null && args.length==1) id=args[0];
return new Check(EQ,pk,id);
}
public static Check neq(Field pk, Object... args) {
Object id=args;
if(id!=null && args.length==1) id=args[0];
return new Check(NEQ,pk,id);
}
public static Check gt(Field pk, Object... args) {
Object id=args;
if(id!=null && args.length==1) id=args[0];
return new Check(GT,pk,id);
}
public static Check gte(Field pk, Object... args) {
Object id=args;
if(id!=null && args.length==1) id=args[0];
return new Check(GTE,pk,id);
}
public static Check lt(Field pk, Object... args) {
Object id=args;
if(id!=null && args.length==1) id=args[0];
return new Check(LT,pk,id);
}
public static Check lte(Field pk, Object... args) {
Object id=args;
if(id!=null && args.length==1) id=args[0];
return new Check(LTE,pk,id);
}
public static Check like(Field pk, Object... args) {
Object id=args;
if(id!=null && args.length==1) id=args[0];
return new Check(LIKE,pk,id);
}
public static Check in(Field pk, Object... id) {
return new Check(IN,pk,id);
}
public static Check not_in(Field pk, Object... id) {
return new Check(NOT_IN,pk,id);
}
Op code;
boolean leaf;
Object[] args;
boolean locked;
public Check(Op code,Field f,Object val){
this.code=code;
leaf=true;
args=new Object[]{f,val};
}
public Check(Op code,Check ... sub){
this.code=code;
leaf=false;
args=sub;
}
public Check setLocked(boolean f){
locked=f;
return this;
}
public boolean isLocked(){
return locked;
}
public Op getCode(){
return code;
}
public boolean isLeaf(){
return leaf;
}
public boolean met(Object val){
return code.met(this,val);
}
@Override
public Iterator<Check> iterator() {
return new CheckIterator(this);
}
public int getChildCount(){
return leaf?0:args.length;
}
public Check getChild(int index){
return leaf?null:(Check)args[index];
}
public Field getField(){
return (Field)args[0];
}
public Object getValue(){
return (Object)args[1];
}
public Check setValue(Object val){
if(locked) throw new IllegalStateException("check value is locked");
if(!leaf) throw new IllegalStateException("check is not a leaf");
args[1]=val;
return this;
}
}
-126
View File
@@ -1,126 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.io.IOException;
import com.reliancy.rec.Hdr;
import com.reliancy.rec.JSON;
import com.reliancy.rec.Rec;
import com.reliancy.rec.Slot;
/** Instance of an entity, usually a row in a table.
*
*/
public class DBO implements Rec{
public static enum Status{
NEW,USED,DELETED,COMPUTED
}
Terminal terminal;
Entity type;
Status status;
Object[] values;
public DBO() {
Class<? extends DBO> cls=this.getClass();
if(cls!=DBO.class){
Entity ent=Entity.recall(cls);
setType(ent);
}
status=Status.NEW;
}
@Override
public String toString(){
try {
StringBuffer ret=new StringBuffer();
JSON.writes(this,ret);
return ret.toString();
} catch (IOException e) {
return e.toString();
}
}
public Terminal getTerminal() {
return terminal;
}
public DBO setTerminal(Terminal terminal) {
this.terminal = terminal;
return this;
}
public Status getStatus(){
return status;
}
public DBO setStatus(Status s) {
this.status = s;
return this;
}
public final Entity getType() {
return type;
}
public final DBO setType(Entity type) {
this.type = type;
if(type==null){
values=null;
}else{
values=new Object[type.count()];
}
return this;
}
@Override
public Hdr meta() {
return type;
}
@Override
public int count() {
return values!=null?values.length:0;
}
@Override
public Rec set(int pos, Object val) {
if(pos<0) pos=count()+pos;
values[pos]=val;
return this;
}
@Override
public Object get(int pos) {
if(pos<0) pos=count()+pos;
return values[pos];
}
@Override
public Rec add(Object val) {
throw new UnsupportedOperationException("dbo is not array");
}
@Override
public Rec remove(int s) {
throw new UnsupportedOperationException("dbo is not array");
}
@Override
public Rec set(Slot s, Object val) {
if(s==null) throw new IllegalArgumentException("invalid key provided");
int index=s.getPosition(); // try slot position
//if(index<0) index=type.findSlot(s.getName());// fall back to search if slot not set
if(index<0){
throw new IllegalArgumentException("invalid key provided:"+s.getName());
}else{
values[index]=val;
}
return this;
}
@Override
public Object get(Slot s, Object def) {
if(s==null) throw new IllegalArgumentException("invalid key provided");
int index=s.getPosition(); // try slot position
//if(index<0) index=type.findSlot(s.getName());// fall back to search if slot not set
if(index<0) throw new IllegalArgumentException("invalid key provided:"+s.getName());
Object ret=values[index];
return ret==null?def:ret;
}
@Override
public Rec remove(Slot s) {
throw new UnsupportedOperationException("dbo is not resizable");
}
}
-194
View File
@@ -1,194 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.util.HashMap;
import java.util.Iterator;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collection;
import com.reliancy.rec.Hdr;
import com.reliancy.rec.Slot;
/** Describes an object structure, usually a table.
*
*/
public class Entity extends Hdr{
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public static @interface Info {
String name();
}
static final HashMap<String,Entity> registry=new HashMap<>();
public static final void publish(Entity ent){
registry.put(ent.getName(),ent);
registry.put(ent.getId(),ent);
}
public static final void retract(Entity ent){
if(ent==null) return;
Collection<Entity> vals=registry.values();
while(vals.remove(ent)){}
}
public static final void retract(Class<? extends DBO> cls){
Entity ent=recall(cls.getSimpleName());
if(ent!=null){
retract(ent);
}
}
public static final Entity recall(String name){
return registry.get(name);
}
public static final Entity recall(Class<? extends DBO> cls){
Entity ent=recall(cls.getSimpleName());
if(ent==null){
ent=publish(cls);
}
return ent;
}
/**
* this method will analyze a DBO class and forumate an Entity object out of it.
* @param cls
* @return
*/
@SuppressWarnings("unchecked")
public static final Entity publish(Class<? extends DBO> cls){
Entity ret=registry.get(cls.getSimpleName());
if(ret!=null) return ret;
//System.out.println("Analyzing:"+cls);
Class<?> base=cls.getSuperclass();
Entity base_ent=null;
int position0=0;
if(base!=null && base!=DBO.class){
base_ent=publish((Class<? extends DBO>)base);
position0=base_ent.count();
}
java.lang.reflect.Field[] declaredFields = cls.getDeclaredFields();
ArrayList<Field> slots=new ArrayList<>();
for (java.lang.reflect.Field field : declaredFields) {
if (!java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
continue;
}
try {
String sf_name=field.getName();
Field slot=(Field) field.get(cls);
// Only set ID if not already set (allows explicit database column name mapping)
// Use Field's name (database column name) if available, otherwise use Java field name
if(slot.getId()==null || slot.getId().isEmpty()){
String dbName=slot.getName(); // This is the name passed to Field constructor (e.g., "created_on")
if(dbName!=null && !dbName.isEmpty()){
slot.setId(dbName);
}else{
slot.setId(sf_name); // Fallback to Java field name
}
}
slot.setPosition(position0+slots.size());
slots.add(slot);
//System.out.println(sf_name+":"+slot+" atpos:"+slot.getPosition());
} catch (Exception e) {
}
}
Info info=cls.getAnnotation(Info.class);
ret=new Entity(info!=null?info.name():cls.getSimpleName()).setId(cls.getSimpleName());
ret.setBase(base_ent);
ret.setType(cls);
ret.getOwnSlots().addAll(slots);
publish(ret);
return ret;
}
Entity base;
String id;
Field pk;
public Entity(String name) {
super(name);
}
@Override
public Slot makeSlot(String name){
return new Field(name);
}
@Override
public Iterator<Slot> iterator(int offset){
if(offset>0) throw new IllegalArgumentException("Offset not supported");
final Entity ent=this;
return new Iterator<Slot>(){
final FieldSlice slice=new FieldSlice(ent).including(Field.FLAG_STORABLE);
@Override
public boolean hasNext() {
return slice.hasNext();
}
@Override
public Slot next() {
return slice.next();
}
};
}
@Override
public int count(){
return super.count()+(base!=null?base.count():0);
}
/**
* gets a slot which could be here or in base.
*/
@Override
public Slot getSlot(int pos){
if(base!=null){ // we got base
int ofs=base.count();
if(pos<ofs) return base.getSlot(pos);
else return super.getSlot(pos-ofs);
}else{ // regular no base
return super.getSlot(pos);
}
}
public Field getField(int index){
return (Field)getSlot(index);
}
public int getDepth(){
return base!=null?1+base.getDepth():0;
}
public Entity getBase() {
return base;
}
public Entity setBase(Entity base) {
this.base = base;
return this;
}
public String getId() {
return id;
}
public Entity setId(String id) {
this.id = id;
return this;
}
public Entity setPk(Field pk) {
this.pk = pk;
return this;
}
public Field getPk(){
if(pk!=null) return pk;
// try to locate the pk - this now gos over base as well
for(int i=0;i<count() && pk==null;i++){
Field pp=(Field) getSlot(i);
if(pp.isPk()) pk=pp;
}
return pk;
}
public DBO newInstance() throws InstantiationException, IllegalAccessException{
return newInstance(null).setStatus(DBO.Status.NEW);
}
public DBO newInstance(Terminal t) throws InstantiationException, IllegalAccessException{
Class<?> cls=getType();
DBO ret=(DBO) cls.newInstance();
ret.setType(this).setTerminal(t).setStatus(DBO.Status.NEW);
return ret;
}
}
-108
View File
@@ -1,108 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.math.BigDecimal;
import java.sql.Date;
import java.sql.Timestamp;
import com.reliancy.rec.Slot;
/**
* Description of a column or property.
*/
public class Field extends Slot {
public static Field Int(String name){
return new Field(name,Integer.class);
}
public static Field Str(String name){
return new Field(name,String.class);
}
public static Field Bool(String name){
return new Field(name,Boolean.class);
}
public static Field Float(String name){
return new Field(name,Float.class);
}
public static Field Num(String name){
return new Field(name,BigDecimal.class);
}
public static Field Date(String name){
return new Field(name,Date.class);
}
public static Field DateTime(String name){
return new Field(name,Timestamp.class);
}
public static final int FLAG_PK =0x0100;
public static final int FLAG_AUTOINC =0x0200;
String id;
String typeParams;
public Field(String name) {
super(name);
this.raiseFlags(Field.FLAG_STORABLE);
}
public Field(String name,Class<?> typ) {
super(name,typ);
this.raiseFlags(Field.FLAG_STORABLE);
}
@Override
public boolean equals(String str){
return super.equals(str) || (id!=null && id.equalsIgnoreCase(str));
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public boolean isPk() {
return checkFlags(FLAG_PK);
}
public Field setPk(boolean pk) {
if(pk) raiseFlags(FLAG_PK); else clearFlags(FLAG_PK);
return this;
}
public boolean isAutoIncrement() {
return checkFlags(FLAG_AUTOINC);
}
public Field setAutoIncrement(boolean pk) {
if(pk) raiseFlags(FLAG_AUTOINC); else clearFlags(FLAG_AUTOINC);
return this;
}
public String getTypeParams() {
return typeParams;
}
public Field setTypeParams(String p) {
typeParams=p;
return this;
}
public Check eq(Object... val) {
return Check.eq(this,val);
}
public Check neq(Object... val) {
return Check.neq(this,val);
}
public Check gt(Object... val) {
return Check.gt(this,val);
}
public Check gte(Object... val) {
return Check.gte(this,val);
}
public Check lt(Object... val) {
return Check.lt(this,val);
}
public Check lte(Object... val) {
return Check.lte(this,val);
}
public Check like(Object... val) {
return Check.like(this,val);
}
public Check in(Object... val) {
return Check.in(this,val);
}
}
@@ -1,109 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.util.Iterator;
import java.util.List;
/** Field iterator matching flags over entity hierarchy.
*
*/public class FieldSlice implements Iterator<Field>,Iterable<Field>{
protected final Entity entity;
protected FieldSlice sup;
protected int includeMask;
protected int excludeMask;
protected int raw_index;
protected Field next_field;
protected int next_index;
protected Entity next_entity;
public FieldSlice(Entity ent){
entity=ent;
if(entity.getBase()!=null){
sup=new FieldSlice(ent.getBase());
}else{
sup=null;
}
raw_index=-1;
next_index=-1;
}
public FieldSlice including(int mask){
includeMask=mask;
if(sup!=null) sup.including(mask);
return this;
}
public FieldSlice excluding(int mask){
excludeMask=mask;
if(sup!=null) sup.excluding(mask);
return this;
}
/** we add rewind capability to allow reuse of same fieldslice.
* i.e we use it to generate recipe, then later to properly enumerate values.
*/
public FieldSlice rewind(){
raw_index=-1;
next_index=-1;
next_field=null;
next_entity=null;
if(sup!=null) sup.rewind();
return this;
}
// search for next valid field
public final Field findNext(){
List<?> local=entity.getOwnSlots();
if(raw_index>=local.size()) return null; // we have exhausted local supply
next_field=null; // clear prev result
// search at base
if(sup!=null && sup.hasNext()){
next_field=sup.next();
next_index++;
next_entity=sup.nextEntity();
return next_field;
}
next_entity=entity;
// now search locally
for(raw_index=raw_index+1;raw_index<local.size();raw_index++){
Field f=(Field) local.get(raw_index);
int attr=f.getFlags();
if((attr & excludeMask)!=0) continue; // skip if in exluding set
if((attr & includeMask)==0) continue; // skip if not in including set
next_field=f;
next_index+=1;
break;
}
return next_field;
}
@Override
public Iterator<Field> iterator() {
return this;
}
@Override
public boolean hasNext() {
Field next=findNext();
return next!=null;
}
@Override
public Field next() {
return next_field;
}
public int nextIndex(){
return next_field!=null?next_index:-1;
}
public Entity nextEntity(){
return next_entity;
}
public DBO makeRecord() throws InstantiationException, IllegalAccessException{
return entity.newInstance();
}
public void writeRecord(DBO rec,Object val){
rec.set(next_field, val);
}
public Object readRecord(DBO rec,Object def){
return rec.get(next_field, def);
}
}
-281
View File
@@ -1,281 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.reliancy.util.Handy;
public final class SQL implements Appendable{
public final static Object NULL=new Object();
public final static String WS=" ";
public final static String SELECT="SELECT";
public final static String INSERT="INSERT INTO";
public final static String UPDATE="UPDATE";
public final static String DELETE="DELETE";
public final static String FROM="FROM";
public final static String INNER_JOIN="INNER JOIN";
public final static String ON="ON";
public final static String SET="SET";
public final static String WHERE="WHERE";
final StringBuffer buffer=new StringBuffer();
final SQLTerminal terminal;
final String ql,qr;
final HashMap<Entity,String> entAlias=new HashMap<>();
public SQL(SQLTerminal terminal){
this.terminal=terminal;
ql=terminal!=null?terminal.getQuoteLeft():"\"";
qr=terminal!=null?terminal.getQuoteRight():"\"";
}
@Override
public String toString(){
return buffer.toString();
}
@Override
public final SQL append(CharSequence csq){
buffer.append(csq);
return this;
}
@Override
public final SQL append(CharSequence csq, int start, int end){
buffer.append(csq,start,end);
return this;
}
@Override
public final SQL append(char c){
buffer.append(c);
return this;
}
public final SQL select(){
append(SELECT);
return this;
}
public final SQL insert(){
append(INSERT);
return this;
}
public final SQL update(){
append(UPDATE);
return this;
}
public final SQL delete(){
append(DELETE);
return this;
}
public final SQL from(){
append(WS).append(FROM).append(WS);
return this;
}
public final SQL inner_join(){
append(WS).append(INNER_JOIN).append(WS);
return this;
}
public final SQL on(){
append(WS).append(ON).append(WS);
return this;
}
public final String wrap(String id){
if(id.startsWith(ql) && id.endsWith(qr)){
return id;
}else{
return ql+id.replace(".",qr+"."+ql)+qr;
}
}
public final SQL id(String id){
if(id.startsWith(ql) && id.endsWith(qr)){
append(id);
}else{
append(ql).append(id.replace(".",qr+"."+ql)).append(qr);
}
return this;
}
public final String getAlias(Entity e){
String eAlias=entAlias.get(e);
if(eAlias!=null) return eAlias;
eAlias="e"+entAlias.size();
entAlias.put(e,eAlias);
return eAlias;
}
public final SQL select(Entity ent,FieldSlice fit){
entAlias.clear();;
select();
while(fit.hasNext()){
int index=fit.nextIndex();
Field f=fit.next();
Entity e=fit.nextEntity();
String alias=getAlias(e);
//System.out.println("It:"+index+":/"+f+"/"+e+"/"+alias);
append(index==0?" ":",");
// Use getId() if set (database column name), otherwise use getName()
String colName=(f.getId()!=null && !f.getId().isEmpty())?f.getId():f.getName();
append(alias).append(".").id(colName);
}
from();
String eAlias=getAlias(ent);
id(ent.getName()).append(" ").append(eAlias);
for(Entity b=ent.getBase();b!=null;b=b.getBase()){
String bAlias=getAlias(b);
inner_join();
id(b.getName()).append(" ").append(bAlias);
on();
Field bPk=b.getPk();
Field ePk=ent.getPk();
// Use getId() if set (database column name), otherwise use getName()
String ePkName=(ePk.getId()!=null && !ePk.getId().isEmpty())?ePk.getId():ePk.getName();
String bPkName=(bPk.getId()!=null && !bPk.getId().isEmpty())?bPk.getId():bPk.getName();
append(eAlias).append(".").id(ePkName);
append("=");
append(bAlias).append(".").id(bPkName);
}
return this;
}
public final SQL where(){
append(WS).append(WHERE).append(WS);
return this;
}
public final SQL where(Check filter) {
append(WS).append(WHERE).append(WS).check(filter);
return this;
}
/// using entalias locate field entity and its prefix
public final String getFieldPrefix(Field f){
for(Map.Entry<Entity,String> e:entAlias.entrySet()){
Entity ent=e.getKey();
String prefix=e.getValue();
if(ent.isOwned(f)) return prefix+".";
}
return "";
}
public final SQL check(Check filter) {
if(filter.isLeaf()){
Check.Op op=filter.getCode();
Field field=filter.getField();
// Use getId() if set (database column name), otherwise use getName()
String fieldName=(field.getId()!=null && !field.getId().isEmpty())?field.getId():field.getName();
String fname=wrap(fieldName);
String opname=op.toString();
String arg="?";
Object val=filter.getValue();
if(op==Check.LIKE && terminal!=null && terminal.getProtocol().contains(":postgre")){
opname="ILIKE";
}
if(op==Check.IN){
arg="("+Handy.toString(val)+")";
}
if(Handy.isEmpty(val)){
// if not value then shortcuircuid condition
fname="1";
opname="=";
arg="1";
}
if(val==NULL){
arg="NULL";
if(op==Check.EQ) opname="IS";
if(op==Check.NEQ) opname="IS NOT";
}
append("(");
String fprefix=getFieldPrefix(field);
append(fprefix).append(fname).append(WS).append(opname).append(WS).append(arg);
append(")");
}else{
Check.Op op=filter.getCode();
String delim=op.toString();
if(op==Check.NOT){
append(delim).append("(").check(filter.getChild(0)).append(")");
}else{
append("(");
for(int i=0;i<filter.getChildCount();i++){
if(i>0) append(WS).append(delim).append(WS);
check(filter.getChild(i));
}
append(")");
}
}
return this;
}
/** fills params list with non-trivial parameters.
* we place this method here to be as close as possible to the one which generates the sql code.
* check and check_export must be in synch.
* @param filter check operation to perform over conditions.
* @param params extracted params.
*/
public final void check_export(Check filter,List<Object> params) {
if(filter.isLeaf()){
Check.Op op=filter.getCode();
Object val=filter.getValue();
if(Handy.isEmpty(val) || val==NULL || op==Check.IN) return; // skip over empty or NULL values
params.add(val);
}else{
for(Check ch:filter) check_export(ch,params);
}
}
/** fills check values from dbo record where equal and not-equal are used.
* we place this method here to be as close as possible to the one which generates the sql code.
* check and check_import must be in synch.
* @param filter set of checks
* @param rec record to check
*/
public final void check_import(Check filter,DBO rec) {
if(filter.isLeaf()){
if(filter.isLocked()) return; // no import on locked condition
Check.Op op=filter.getCode();
if(op!=Check.EQ && op!=Check.NEQ) return; // skip over all conditions except = and <>
Field f=filter.getField();
Object val=f.get(rec,null);
filter.setValue(val);
}else{
for(Check ch:filter) check_import(ch,rec);
}
}
public final SQL insert(Entity entity,List<Field> supplied){
insert();
append(SQL.WS).id(entity.getName()).append(" (");
StringBuffer ext=new StringBuffer();
String delim="";
Field pk=entity.getPk();
if(!entity.isOwned(pk)){
String pkName=(pk.getId()!=null && !pk.getId().isEmpty())?pk.getId():pk.getName();
append(delim).id(pkName);
ext.append(delim).append("?");
delim=",";
}
for(int index=0;index<supplied.size();index++){
Field f=supplied.get(index);
if(index>0) delim=",";
String fName=(f.getId()!=null && !f.getId().isEmpty())?f.getId():f.getName();
append(delim).id(fName);
ext.append(delim).append("?");
}
append(") VALUES (").append(ext).append(")");
return this;
}
public final SQL update(Entity entity,List<Field> supplied){
update();
append(SQL.WS).id(entity.getName()).append(" SET ");
for(int index=0;index<supplied.size();index++){
Field f=supplied.get(index);
String delim=index==0?"":",";
append(delim);
String fName=(f.getId()!=null && !f.getId().isEmpty())?f.getId():f.getName();
id(fName).append("=?");
}
where();
Field pk=entity.getPk();
String pkName=(pk.getId()!=null && !pk.getId().isEmpty())?pk.getId():pk.getName();
id(pkName).append("=?");
return this;
}
public final SQL delete(Entity entity){
delete().from().id(entity.getName());
return this;
}
}
@@ -1,152 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.io.Closeable;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
/** Helper object which impleents DBO deleting.
* It manages the recipe and the prepared statmenet.
* The cleaner works in two ways.
* If you supply items iterator it will delete by pk id those items.
* If you supply a Check filter and no items then it will delete based on a where statement.
*/
public class SQLCleaner implements Closeable{
protected final Entity entity;
protected final SQLTerminal terminal;
protected final SQLCleaner base; /// used for nesting
protected final SQL sql;
protected final ArrayList<Object> params;
protected Check filter;
protected Connection external;
protected PreparedStatement deleteStmt;
protected int itemsDeleted;
protected Exception error;
public SQLCleaner(Entity ent,SQLTerminal t) {
entity=ent;
terminal=t;
base=(entity.getBase()!=null)?new SQLCleaner(entity.getBase(),t):null;
sql=new SQL(terminal);
params=new ArrayList<>();
}
public SQL compileRecipe(){
if(filter==null){
// no filter we go with PK
Field pk=entity.getPk();
filter=pk.eq("?");
}
sql.delete(entity).where(filter);
return sql;
}
public boolean isLinkExternal(){
return external!=null;
}
public SQLCleaner setExternalLink(Connection link){
external=link;
return this;
}
protected Connection getExternalLink(){
return external;
}
protected Connection getInternalLink(){
try{
if(deleteStmt!=null) return deleteStmt.getConnection();
}catch(SQLException ex){
}
return null;
}
public SQLCleaner open() throws SQLException{
return open(null);
}
public SQLCleaner open(Check where) throws SQLException{
this.filter=where;
Connection link=isLinkExternal()?getExternalLink():terminal.getConnection();
if(base!=null) base.setExternalLink(link).open(filter); // definitely external link for base
SQL delSQL=compileRecipe();
//System.out.println("DEL:"+delSQL+"/"+filter);
deleteStmt=link.prepareStatement(delSQL.toString());
return this;
}
@Override
public void close() throws IOException{
if(base!=null) base.close(); // since link is external it will not close link just the rest
Connection link=getInternalLink();
if(deleteStmt!=null){
try{
deleteStmt.close();
}catch(SQLException ex){
if(error==null) error=ex;
}
}
try{
if(link!=null && !isLinkExternal()) link.close();
external=null;
}catch(SQLException ex){
if(error==null) error=ex;
}
if(error!=null){
if(error instanceof IOException) throw (IOException)error;
else throw new IOException(error);
}
}
public void flush(Iterator<DBO> items) throws SQLException {
Connection link=getInternalLink();
boolean autocommited=link.getAutoCommit();
try{
link.setAutoCommit(false);
if(items==null){ // deleting by filter
throw new SQLException("delete by filter not implemented yet");
// we would need to leave the primary filter
// we would use filter in an ID in (SUBQUERY)
// this is because filter could reference all entities and we have inheritance so multiple
// we would generate a select statement with filter and selecting only ID
}else{ // deleting by incoming records
while(items.hasNext()){
DBO rec=items.next();
deleteRecord(rec);
}
}
if(!link.getAutoCommit()){
link.commit();
}
}catch(SQLException ex){
if(!link.getAutoCommit()){
link.rollback();
}
throw ex;
}finally{
link.setAutoCommit(autocommited);
}
}
/**
* This calls one delete. It can and is called from outside in case of nesting when link is external.
* @param rec database object to delete
* @throws SQLException sql related error
*/
public boolean deleteRecord(DBO rec) throws SQLException{
if(rec==null) return false;
if(base!=null) base.deleteRecord(rec); // save the superclass first
sql.check_import(filter,rec); // get values from dbo
params.clear();
sql.check_export(filter,params); // move them into params
for(int pindex=0;pindex<params.size();pindex++){
Object val=params.get(pindex);
deleteStmt.setObject(pindex+1,val);
}
int dcode=deleteStmt.executeUpdate();
itemsDeleted+=dcode;
return dcode>0;
}
}
@@ -1,134 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import com.reliancy.dbo.Action.Load;
/** SQLIterator will delay closing a connection and will iterate over result set.
* TODO: no support for orderby yet
*/
public class SQLReader implements SiphonIterator<DBO>{
protected final Entity entity;
protected final SQLTerminal terminal;
protected final SQL sql;
protected FieldSlice slice;
protected ResultSet result;
protected Exception error;
public SQLReader(Entity ent,SQLTerminal t) {
this.entity=ent;
terminal=t;
// slice controls sql fields but also lets us correctly import values later
slice=new FieldSlice(entity).including(Field.FLAG_STORABLE);
sql=new SQL(terminal);
}
public SQLReader open() throws SQLException{
return open(null);
}
public SQLReader open(Action action) throws SQLException{
error=null;
if(action==null){
sql.select(entity,slice); // simple case
}else{
compileRecipe(action); // complete case
}
//System.out.println("SQL:"+sql);
Connection link=terminal.getConnection();
PreparedStatement prep=link.prepareStatement(sql.toString());
if(action!=null){
Load tr=(Load) action.getTrait();
if(tr.filter!=null){
ArrayList<Object> params=new ArrayList<>();
sql.check_export(tr.filter, params);
for(int pindex=0;pindex<params.size();pindex++){
Object val=params.get(pindex);
prep.setObject(pindex+1,val);
}
}
}
result=prep.executeQuery();
if(link.getAutoCommit()==false) link.commit();
//action.setItems(this); -- maybe we want multiple readers on same actions - leave this to terminal
return this;
}
public SQL compileRecipe(Action action){
sql.select(action.getEntity(),slice);
Load tr=(Load) action.getTrait();
if(tr.filter!=null){
sql.where(tr.filter);
}
return sql;
}
@Override
public boolean hasNext() {
try {
return error==null?result.next():false;
} catch (SQLException e) {
error=e;
return false;
}
}
@Override
public DBO next() {
try {
DBO ret=(DBO) slice.makeRecord();
FieldSlice fit=slice.rewind();
while(fit.hasNext()){
int findex=fit.nextIndex();
//Field field=fit.next();
Object val=result.getObject(findex+1);
fit.writeRecord(ret, val);
}
ret.setStatus(DBO.Status.USED);
return ret;
} catch (Exception e) {
error=e;
return null;
}
}
@Override
public void close() throws IOException {
if(result!=null){
Statement stmt=null;
Connection link=null;
try{
stmt=result.getStatement();
link=stmt!=null?stmt.getConnection():null;
if(!result.isClosed()) result.close();
}catch(SQLException ex){
if(error==null) error=ex;
}
try{
if(stmt!=null) stmt.close();
}catch(SQLException ex){
if(error==null) error=ex;
}
try{
if(link!=null) link.close();
}catch(SQLException ex){
if(error==null) error=ex;
}
}
if(error!=null){
if(error instanceof IOException) throw (IOException)error;
else throw new IOException(error);
}
}
}
@@ -1,231 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.io.IOException;
import java.lang.reflect.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.JDBCType;
import java.sql.SQLException;
import java.sql.Types;
import java.util.HashMap;
import java.util.Map;
import com.reliancy.util.Path;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
/** SQL particular implementation of a terminal.
* It will use a connection pool under it to take care of connection re-use.
*
*/
public class SQLTerminal implements Terminal{
HikariConfig config = new HikariConfig();
HikariDataSource ds;
Path url;
String quoteLeft="\""; // quotes could be subject to sql flavour
String quoteRight="\"";
public SQLTerminal(String url){
this.url=new Path(url);
String proto=this.url.getProtocol();
if(!proto.startsWith("jdbc:")) proto="jdbc:"+proto;
String u=proto+"://"+this.url.getHost()+":"+this.url.getPort()+"/"+this.url.getDatabase();
config.setJdbcUrl(u);
config.setUsername(this.url.getUserid());
config.setPassword(this.url.getPassword());
//config.setAutoCommit(false); -- do this in batch cases only
config.addDataSourceProperty( "cachePrepStmts" , "true" );
config.addDataSourceProperty( "prepStmtCacheSize" , "250" );
config.addDataSourceProperty( "prepStmtCacheSqlLimit" , "2048" );
ds = new HikariDataSource( config );
}
public Connection getConnection() throws SQLException{
return ds.getConnection();
}
@Override
public Action execute(Action q) throws IOException{
// System.out.println("Executing..."+q.getTrait());
Action.Trait tr=q.getTrait();
if(tr instanceof Action.Load){
Entity ent=q.getEntity();
SQLReader reader=new SQLReader(ent,this);
try {
reader.open(q);
q.setItems(reader);
} catch (SQLException e) {
reader.close();
throw new IOException(e);
}
//System.out.println("Executing...Done");
return q;
}else if(tr instanceof Action.Save){
Entity ent=q.getEntity();
try(SQLWriter writer=new SQLWriter(ent,this)) {
writer.open();
writer.flush(q.getItems());
//System.out.println("Executing...Done");
return q;
}catch(SQLException e){
throw new IOException(e);
}
}else if(tr instanceof Action.Delete){
Entity ent=q.getEntity();
try(SQLCleaner cleaner=new SQLCleaner(ent,this)) {
cleaner.open();
cleaner.flush(q.getItems());
//System.out.println("Executing...Done");
return q;
}catch(SQLException e){
throw new IOException(e);
}
}else{
throw new UnsupportedOperationException("Trait not supported:"+tr);
}
}
public String getProtocol() {
return url.getProtocol();
}
public String getQuoteLeft(){
return this.quoteLeft;
}
public String getQuoteRight(){
return this.quoteRight;
}
final HashMap<Integer,Class<?>> sql2java=new HashMap<>();
final HashMap<Class<?>,Integer> java2sql=new HashMap<>();
public Map<Class<?>,Integer> getJava2SQL(){
if(!java2sql.isEmpty()) return java2sql;
String protocol=url.getProtocol();
java2sql.put(java.math.BigDecimal.class,Types.DECIMAL);
java2sql.put(java.math.BigInteger.class,Types.DECIMAL);
java2sql.put(Boolean.class,protocol.contains(":oracle")?Types.INTEGER:Types.BOOLEAN);
java2sql.put(Byte.class,Types.TINYINT);
java2sql.put(Short.class,Types.SMALLINT);
java2sql.put(Integer.class,Types.INTEGER);
java2sql.put(Long.class,Types.BIGINT);
java2sql.put(Float.class,Types.FLOAT);
java2sql.put(Double.class,Types.DOUBLE);
java2sql.put(byte[].class,Types.VARBINARY);
java2sql.put(Blob.class,Types.BLOB);
java2sql.put(char[].class,Types.VARCHAR);
java2sql.put(String.class,Types.VARCHAR);
java2sql.put(StringBuffer.class,Types.VARCHAR);
java2sql.put(Clob.class,Types.CLOB);
java2sql.put(java.sql.Date.class,Types.DATE);
java2sql.put(java.sql.Time.class,Types.TIME);
java2sql.put(java.sql.Timestamp.class,Types.TIMESTAMP);
java2sql.put(Array.class,Types.ARRAY);
return java2sql;
}
public Map<Integer,Class<?>> getSQL2Java(){
if(!sql2java.isEmpty()) return sql2java;
//String protocol=url.getProtocol();
sql2java.put(Types.NUMERIC,java.math.BigDecimal.class);
sql2java.put(Types.DECIMAL,java.math.BigDecimal.class);
sql2java.put(Types.BIT,Boolean.class);
sql2java.put(Types.BOOLEAN,Boolean.class);
sql2java.put(Types.TINYINT,Byte.class);
sql2java.put(Types.SMALLINT,Short.class);
sql2java.put(Types.INTEGER,Integer.class);
sql2java.put(Types.BIGINT,Long.class);
sql2java.put(Types.REAL,Float.class);
sql2java.put(Types.FLOAT,Float.class);
sql2java.put(Types.DOUBLE,Double.class);
sql2java.put(Types.BINARY,byte[].class);
sql2java.put(Types.VARBINARY,byte[].class);
sql2java.put(Types.LONGVARBINARY,byte[].class);
sql2java.put(Types.CHAR,String.class);
sql2java.put(Types.NCHAR,String.class);
sql2java.put(Types.VARCHAR,String.class);
sql2java.put(Types.NVARCHAR,String.class);
sql2java.put(Types.LONGVARCHAR,String.class);
sql2java.put(Types.LONGNVARCHAR,String.class);
sql2java.put(Types.DATE,java.sql.Date.class);
sql2java.put(Types.TIME,java.sql.Time.class);
sql2java.put(Types.TIMESTAMP,java.sql.Timestamp.class);
sql2java.put(Types.BLOB,byte[].class);
sql2java.put(Types.CLOB,char[].class);
sql2java.put(Types.ARRAY,java.sql.Array.class);
sql2java.put(Types.JAVA_OBJECT,Object.class);
return sql2java;
}
/**
* Returns back java class for given id and or name.
* The name is not used in default implementation.
* @param typeid sql type to map
* @return Class matching sql typeid.
*/
public Class<?> getJavaType(int typeid) {
Class<?> ret=getSQL2Java().get(typeid);
return ret;
}
/**
* This method will correct cases when sqltype is varchar (12) but type name is date or similar.
* @param sqltype
* @param type_name
* @return tries to promote sqltype given type name to something more specific.
*/
public int getTypeId(int sqltype,String type_name){
if(type_name==null) return sqltype;
type_name=type_name.toLowerCase();
if(sqltype==Types.VARCHAR || sqltype==Types.CHAR){
if(type_name.equals("date")) sqltype=Types.DATE;
if(type_name.equals("time")) sqltype=Types.TIME;
if(type_name.equals("datetime")) sqltype=Types.TIMESTAMP;
}
return sqltype;
}
/**
* @param cls
* @param createParams
* @return SQL type given java class and create params
*/
public int getTypeId(Class<?> cls,String createParams){
int ret=getJava2SQL().get(cls);
return ret;
}
public String getTypeName(Class<?> cls,String createParams){
int id=this.getTypeId(cls, createParams);
String ret = JDBCType.valueOf(id).getName();
if(ret==null) return null;
String protocol=url.getProtocol();
if(protocol.contains(":sqlserver")){
if("boolean".equalsIgnoreCase(ret)) ret="BIT";
if("timestamp".equalsIgnoreCase(ret)) ret="DATETIME";
if("double".equalsIgnoreCase(ret)) ret="float";
if("float".equalsIgnoreCase(ret)) ret="real";
}
if(protocol.contains(":postgre")){
if("varbinary".equalsIgnoreCase(ret)) ret="bytea";
if("double".equalsIgnoreCase(ret)) ret="double precision";
}
if("varchar".equalsIgnoreCase(ret) && (createParams!=null && !createParams.isEmpty())){
long size=Long.parseLong(createParams);
if(protocol.contains(":sqlserver")) ret=size>8000?ret.concat("(").concat("MAX").concat(")"):ret.concat("(").concat(String.valueOf(size)).concat(")");
else if(protocol.contains(":oracle")) ret=size>2000?"CLOB":ret.concat("(").concat(String.valueOf(size)).concat(")");
else if(protocol.contains(":mysql")) ret=size>Character.MAX_VALUE?"TEXT":ret.concat("(").concat(String.valueOf(size)).concat(")");
else if(protocol.contains(":h2")) ret=size>Integer.MAX_VALUE?"CLOB":ret.concat("(").concat(String.valueOf(size)).concat(")");
else if(protocol.contains(":postgre")) ret=size>Character.MAX_VALUE?"TEXT":ret.concat("(").concat(String.valueOf(size)).concat(")");
else ret=(size>Character.MAX_VALUE)?"CLOB":ret.concat("(").concat(String.valueOf(size)).concat(")");
}
String args=null;
if(ret.indexOf('(')==-1 && createParams!=null && !createParams.isEmpty()){
if("decimal".equalsIgnoreCase(ret)) args=createParams;
if("numeric".equalsIgnoreCase(ret)) args=createParams;
}
if(args!=null){
ret=ret.concat("(").concat(args).concat(")");
}
return ret;
}
}
@@ -1,222 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.io.Closeable;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import com.reliancy.util.Handy;
/** Helper object which impleents DBO saving. It manages the recipe and the prepared statmenet.
* Also keeps track of which fields are sent down to DB.
* The writer does the work in flush method during which it exhausts the items. While in flush it
* will disable autocommit and enable it at the end. We could call flush multiple times to send multiple
* batches down to DB.
* Initially we would ctor with Action object but actually we need to generate writes for different entities
* especially in inheritance cases.
*/
public class SQLWriter implements Closeable{
protected final Entity entity;
protected final SQLTerminal terminal;
protected final SQLWriter base; /// used for nesting
protected final ArrayList<Field> supplied=new ArrayList<Field>();
protected final ArrayList<Field> generated=new ArrayList<Field>();
protected String insertSQL;
protected String updateSQL;
protected Connection external;
protected PreparedStatement insertStmt;
protected PreparedStatement updateStmt;
protected int itemsInserted;
protected int itemsUpdated;
protected Exception error;
public SQLWriter(Entity ent,SQLTerminal t) {
entity=ent;
terminal=t;
base=(entity.getBase()!=null)?new SQLWriter(entity.getBase(),t):null;
// we select proper fields for this entity
FieldSlice slice=new FieldSlice(entity).including(Field.FLAG_STORABLE); // includes all even autoincrement
while(slice.hasNext()){
Field f=slice.next();
Entity e=slice.nextEntity();
if(e!=entity) continue; // skip if not part of this entity
if(f.isAutoIncrement()){
generated.add(f);
}else{
supplied.add(f);
}
}
}
public String compileInsertRecipe(){
if(insertSQL!=null) return insertSQL;
SQL buf=new SQL(terminal);
buf.insert(entity,supplied);
insertSQL=buf.toString();
return insertSQL;
}
public String compileUpdateRecipe(){
if(updateSQL!=null) return updateSQL;
SQL buf=new SQL(terminal);
buf.update(entity,supplied);
updateSQL=buf.toString();
return updateSQL;
}
public boolean isLinkExternal(){
return external!=null;
}
public SQLWriter setExternalLink(Connection link){
external=link;
return this;
}
protected Connection getExternalLink(){
return external;
}
protected Connection getInternalLink(){
try{
if(insertStmt!=null) return insertStmt.getConnection();
if(updateStmt!=null) return updateStmt.getConnection();
}catch(SQLException ex){
}
return null;
}
public SQLWriter open() throws SQLException{
Connection link=isLinkExternal()?getExternalLink():terminal.getConnection();
if(base!=null) base.setExternalLink(link).open(); // definitely external link for base
String inSql=compileInsertRecipe();
String upSql=compileUpdateRecipe();
//System.out.println("INS:"+inSql);
//System.out.println("UPD:"+upSql);
String[] genkeys=new String[generated.size()];
for(int i=0;i<generated.size();i++){
Field f=generated.get(i);
genkeys[i]=f.getName();
}
insertStmt=link.prepareStatement(inSql,genkeys);
updateStmt=link.prepareStatement(upSql);
//result=prep.executeQuery();
//if(link.getAutoCommit()==false) link.commit();
return this;
}
@Override
public void close() throws IOException{
if(base!=null) base.close(); // since link is external it will not close link just the rest
Connection link=getInternalLink();
if(insertStmt!=null){
try{
insertStmt.close();
}catch(SQLException ex){
if(error==null) error=ex;
}
}
if(updateStmt!=null){
try{
updateStmt.close();
}catch(SQLException ex){
if(error==null) error=ex;
}
}
try{
if(link!=null && external!=link) link.close();
external=null;
}catch(SQLException ex){
if(error==null) error=ex;
}
if(error!=null){
if(error instanceof IOException) throw (IOException)error;
else throw new IOException(error);
}
}
public void flush(Iterator<DBO> items) throws SQLException {
Connection link=isLinkExternal()?getExternalLink():getInternalLink();
boolean autocommited=link.getAutoCommit();
try{
link.setAutoCommit(false);
while(items.hasNext()){
DBO rec=items.next();
writeRecord(rec);
}
if(!link.getAutoCommit()){
link.commit();
}
}catch(SQLException ex){
if(!link.getAutoCommit()){
link.rollback();
}
throw ex;
}finally{
link.setAutoCommit(autocommited);
}
}
/**
* This calls one update/insert. It can and is called from outside in case of nesting when link is external.
* @param rec
* @throws SQLException
*/
public boolean writeRecord(DBO rec) throws SQLException{
if(base!=null) base.writeRecord(rec); // save the superclass first
// select mode
int pindex=0;
Field pk=entity.getPk();
boolean pk_owned=entity.isOwned(pk);
PreparedStatement stmt=null;
if(rec.getStatus()==DBO.Status.NEW){
stmt=insertStmt;
// need to inject pk here is not owned
if(!pk_owned) stmt.setObject(++pindex,pk.get(rec,null),terminal.getTypeId(pk.getType(),pk.getTypeParams()));
}
if(rec.getStatus()==DBO.Status.USED){
stmt=updateStmt;
// update has a pk condition for sure
Object pkval=pk.get(rec,null);
if(Handy.isEmpty(pkval)) throw new SQLException("Used object with empty PK");
//System.out.println("UPDT:"+stmt+"/"+pkval);
stmt.setObject(
supplied.size()+1,
pkval,
terminal.getTypeId(pk.getType(),pk.getTypeParams())
);
}
if(stmt==null) return false;
// copy values
for(int index=0;index<supplied.size();index++){
Field f=supplied.get(index);
pindex+=1;
int tid=terminal.getTypeId(f.getType(),f.getTypeParams());
Object val=f.get(rec,null);
//System.out.println("Param:"+pindex+":"+f.getName()+":"+val);
stmt.setObject(pindex,val,tid);
}
int ucode=stmt.executeUpdate();
//System.out.println("UCode:"+ucode);
if(rec.getStatus()==DBO.Status.NEW){
this.itemsInserted+=ucode;
if(ucode>0 && !generated.isEmpty()){
try (ResultSet keys = stmt.getGeneratedKeys()) {
if(keys.next()){
for(int i=0;i<generated.size();i++){
Field f=generated.get(i);
Object autoval=keys.getObject(i+1);
f.set(rec,autoval);
}
}
}
}
}else
if(rec.getStatus()==DBO.Status.USED){
this.itemsUpdated+=ucode;
}
return ucode>0;
}
}
@@ -1,16 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.io.Closeable;
import java.util.Iterator;
/** Iterator interface suitable for dbo resultsets.
*
*/
public interface SiphonIterator<T> extends Iterator<T>, Closeable {
}
@@ -1,63 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.io.IOException;
/** Endpoint for dbo objects.
* this interface will implemet CRUD plus control over a databse or folder.
* control will be implemented via meta terminal which will return specialized terminals for each entity and running actions on it
* will modify the entity structure.
*
* the core of the temrminal will be the Action object. The others will just be wrappers for item actions.
* the action will be a read or write query with session management.
*/
public interface Terminal {
public Action execute(Action q) throws IOException;
public default Action begin(){
return begin(null);
}
public default Action begin(String sig){
return new Action(this);
}
public default void end(Action act){
act.clear();
}
public default Terminal meta(Entity ent){
return null;
}
public default <T extends DBO> T load(Class<T> cls,Object...id) throws IOException {
Entity ent=Entity.recall(cls);
String sig="/"+ent.getName()+"/load";
try(Action act=begin(sig).load(ent).limit(1).if_pk(id).execute()){
return cls.cast(act.first());
}
}
public default DBO load(Entity ent,Object...id) throws IOException {
String sig="/"+ent.getName()+"/load";
try(Action act=begin(sig).load(ent).limit(1).if_pk(id).execute()){
return act.first();
}
}
public default boolean save(DBO rec) throws IOException{
Entity ent=rec.getType();
String sig="/"+ent.getName()+"/save";
try(Action act=begin(sig).save(ent).setItems(rec).execute()){
return act.isDone();
}
}
public default boolean delete(DBO rec) throws IOException {
if(rec==null) return false;
Entity ent=rec.getType();
String sig="/"+ent.getName()+"/delete";
try(Action act=begin(sig).delete(ent).setItems(rec).execute()){
return act.isDone();
}
}
}
+57
View File
@@ -0,0 +1,57 @@
package com.reliancy.io;
/**
* Small protocol-oriented helpers used by Jabba I/O codecs.
*
* This intentionally avoids depending on the broader bstore/jabba utility stack.
*/
public final class Handy {
private Handy() {
}
public static boolean isEmpty(CharSequence seq) {
if (seq == null) {
return true;
}
for (int i = 0; i < seq.length(); i++) {
if (!Character.isWhitespace(seq.charAt(i))) {
return false;
}
}
return true;
}
public static boolean isNumeric(String value) {
if (value == null || value.isEmpty()) {
return false;
}
int start = value.charAt(0) == '-' || value.charAt(0) == '+' ? 1 : 0;
if (start >= value.length()) {
return false;
}
boolean sawDigit = false;
boolean sawDot = false;
boolean sawExponent = false;
for (int i = start; i < value.length(); i++) {
char ch = value.charAt(i);
if (Character.isDigit(ch)) {
sawDigit = true;
continue;
}
if (ch == '.' && !sawDot && !sawExponent) {
sawDot = true;
continue;
}
if ((ch == 'e' || ch == 'E') && !sawExponent && sawDigit && i + 1 < value.length()) {
sawExponent = true;
sawDigit = false;
if (value.charAt(i + 1) == '+' || value.charAt(i + 1) == '-') {
i++;
}
continue;
}
return false;
}
return sawDigit;
}
}
@@ -0,0 +1,266 @@
package com.reliancy.io;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Lightweight JSON decoder that produces common Java object trees.
*/
public final class JsonDecoder {
private final CharSequence input;
private int index;
private JsonDecoder(CharSequence input) {
this.input = input == null ? "" : input;
}
public static Object decode(CharSequence input) {
JsonDecoder parser = new JsonDecoder(input);
Object value = parser.readValue();
parser.skipWhitespace();
if (parser.hasMore()) {
throw parser.error("Unexpected trailing content");
}
return value;
}
public static Map<String, Object> decodeObject(CharSequence input) {
Object value = decode(input);
if (value instanceof Map<?, ?> map) {
@SuppressWarnings("unchecked")
Map<String, Object> typed = (Map<String, Object>) map;
return typed;
}
throw new IllegalArgumentException("JSON payload is not an object");
}
public static List<Object> decodeArray(CharSequence input) {
Object value = decode(input);
if (value instanceof List<?> list) {
@SuppressWarnings("unchecked")
List<Object> typed = (List<Object>) list;
return typed;
}
throw new IllegalArgumentException("JSON payload is not an array");
}
private Object readValue() {
skipWhitespace();
if (!hasMore()) {
throw error("Unexpected end of input");
}
char ch = current();
switch (ch) {
case '{':
return readObject();
case '[':
return readArray();
case '"':
return readString();
case 't':
expectLiteral("true");
return Boolean.TRUE;
case 'f':
expectLiteral("false");
return Boolean.FALSE;
case 'n':
expectLiteral("null");
return null;
default:
if (ch == '-' || Character.isDigit(ch)) {
return readNumber();
}
throw error("Unexpected token");
}
}
private Map<String, Object> readObject() {
expect('{');
LinkedHashMap<String, Object> result = new LinkedHashMap<>();
skipWhitespace();
if (peek('}')) {
index++;
return result;
}
while (true) {
skipWhitespace();
String key = readString();
skipWhitespace();
expect(':');
Object value = readValue();
result.put(key, value);
skipWhitespace();
if (peek('}')) {
index++;
return result;
}
expect(',');
}
}
private List<Object> readArray() {
expect('[');
ArrayList<Object> result = new ArrayList<>();
skipWhitespace();
if (peek(']')) {
index++;
return result;
}
while (true) {
result.add(readValue());
skipWhitespace();
if (peek(']')) {
index++;
return result;
}
expect(',');
}
}
private String readString() {
expect('"');
StringBuilder buf = new StringBuilder();
while (hasMore()) {
char ch = input.charAt(index++);
if (ch == '"') {
return buf.toString();
}
if (ch != '\\') {
buf.append(ch);
continue;
}
if (!hasMore()) {
throw error("Unterminated escape sequence");
}
char esc = input.charAt(index++);
switch (esc) {
case '"':
case '\\':
case '/':
buf.append(esc);
break;
case 'b':
buf.append('\b');
break;
case 'f':
buf.append('\f');
break;
case 'n':
buf.append('\n');
break;
case 'r':
buf.append('\r');
break;
case 't':
buf.append('\t');
break;
case 'u':
buf.append(readUnicodeEscape());
break;
default:
throw error("Unsupported escape sequence");
}
}
throw error("Unterminated string");
}
private Number readNumber() {
int start = index;
if (peek('-')) {
index++;
}
readDigits();
boolean fractional = false;
if (peek('.')) {
fractional = true;
index++;
readDigits();
}
if (peek('e') || peek('E')) {
fractional = true;
index++;
if (peek('+') || peek('-')) {
index++;
}
readDigits();
}
String token = input.subSequence(start, index).toString();
if (!Handy.isNumeric(token)) {
throw error("Invalid number");
}
if (fractional) {
return Double.valueOf(token);
}
try {
return Integer.valueOf(token);
} catch (NumberFormatException ignore) {
return Long.valueOf(token);
}
}
private char readUnicodeEscape() {
if (index + 4 > input.length()) {
throw error("Invalid unicode escape");
}
int value = 0;
for (int i = 0; i < 4; i++) {
char ch = input.charAt(index++);
int digit = Character.digit(ch, 16);
if (digit < 0) {
throw error("Invalid unicode escape");
}
value = (value << 4) + digit;
}
return (char) value;
}
private void readDigits() {
int start = index;
while (hasMore() && Character.isDigit(current())) {
index++;
}
if (start == index) {
throw error("Expected digit");
}
}
private void expectLiteral(String literal) {
for (int i = 0; i < literal.length(); i++) {
if (!hasMore() || input.charAt(index++) != literal.charAt(i)) {
throw error("Expected literal " + literal);
}
}
}
private void expect(char expected) {
skipWhitespace();
if (!peek(expected)) {
throw error("Expected '" + expected + "'");
}
index++;
}
private void skipWhitespace() {
while (hasMore() && Character.isWhitespace(current())) {
index++;
}
}
private boolean hasMore() {
return index < input.length();
}
private boolean peek(char ch) {
return hasMore() && current() == ch;
}
private char current() {
return input.charAt(index);
}
private IllegalArgumentException error(String message) {
return new IllegalArgumentException(message + " at offset " + index);
}
}
@@ -0,0 +1,180 @@
package com.reliancy.io;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.Iterator;
import java.util.Map;
/**
* Lightweight JSON encoder for common Java object trees used in web payloads.
*/
public final class JsonEncoder {
private JsonEncoder() {
}
public static int encode(Object value, Appendable out) throws IOException {
if (value == null) {
if (out != null) {
out.append("null");
}
return 4;
}
if (value instanceof String || value instanceof CharSequence || value instanceof Character) {
return writeQuoted(String.valueOf(value), out);
}
if (value instanceof Number || value instanceof Boolean) {
String text = String.valueOf(value);
if (out != null) {
out.append(text);
}
return text.length();
}
if (value instanceof Map<?, ?> map) {
return encodeMap(map, out);
}
if (value instanceof Iterable<?> iterable) {
return encodeIterable(iterable.iterator(), out);
}
if (value instanceof Iterator<?> iterator) {
return encodeIterable(iterator, out);
}
Class<?> type = value.getClass();
if (type.isArray()) {
return encodeArray(value, out);
}
return writeQuoted(String.valueOf(value), out);
}
public static int encodeMap(Map<?, ?> map, Appendable out) throws IOException {
int len = 2;
if (out != null) {
out.append('{');
}
int index = 0;
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (index++ > 0) {
len += 1;
if (out != null) {
out.append(',');
}
}
len += writeQuoted(String.valueOf(entry.getKey()), out);
len += 1;
if (out != null) {
out.append(':');
}
len += encode(entry.getValue(), out);
}
if (out != null) {
out.append('}');
}
return len;
}
public static int encodeArray(Object array, Appendable out) throws IOException {
int len = 2;
if (out != null) {
out.append('[');
}
int size = Array.getLength(array);
for (int i = 0; i < size; i++) {
if (i > 0) {
len += 1;
if (out != null) {
out.append(',');
}
}
len += encode(Array.get(array, i), out);
}
if (out != null) {
out.append(']');
}
return len;
}
public static int encodeIterable(Iterator<?> iterator, Appendable out) throws IOException {
int len = 2;
if (out != null) {
out.append('[');
}
int index = 0;
while (iterator.hasNext()) {
if (index++ > 0) {
len += 1;
if (out != null) {
out.append(',');
}
}
len += encode(iterator.next(), out);
}
if (out != null) {
out.append(']');
}
return len;
}
public static boolean needsEscaping(CharSequence text) {
if (text == null) {
return false;
}
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
if (ch == '"' || ch == '\\' || ch == '/' || ch < 0x20) {
return true;
}
}
return false;
}
public static CharSequence escape(CharSequence text) {
if (text == null || text.length() == 0 || !needsEscaping(text)) {
return text == null ? "" : text;
}
StringBuilder buf = new StringBuilder(text.length() + 8);
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
switch (ch) {
case '"':
buf.append("\\\"");
break;
case '\\':
buf.append("\\\\");
break;
case '/':
buf.append("\\/");
break;
case '\b':
buf.append("\\b");
break;
case '\f':
buf.append("\\f");
break;
case '\n':
buf.append("\\n");
break;
case '\r':
buf.append("\\r");
break;
case '\t':
buf.append("\\t");
break;
default:
if (ch < 0x20) {
buf.append(String.format("\\u%04x", (int) ch));
} else {
buf.append(ch);
}
break;
}
}
return buf;
}
private static int writeQuoted(String text, Appendable out) throws IOException {
CharSequence escaped = escape(text);
if (out != null) {
out.append('"').append(escaped).append('"');
}
return escaped.length() + 2;
}
}
@@ -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);
}
+303 -122
View File
@@ -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<String,Long> hit_history=new LRUCache<>(2*Runtime.getRuntime().availableProcessors());
public FileBucket(String prefix){
this.prefix=prefix;
extAllowed=new String[]{};
domain=new Object[]{};
public class FileServer extends EndPoint implements AppModule,Resources.PathRewrite{
public static class CachedAsset{
final URL source;
final long lastModified;
final long contentLength;
final byte[] content;
final String signature;
public CachedAsset(URL source,long lastModified,long contentLength,byte[] content){
this.source=source;
this.lastModified=lastModified;
this.contentLength=contentLength;
this.content=content;
if(lastModified>0 || contentLength>=0){
signature=Handy.hashMD5(lastModified+":"+contentLength);
}else if(content!=null){
signature=Handy.hashMD5(new String(content,java.nio.charset.StandardCharsets.ISO_8859_1));
}else{
signature=null;
}
}
public InputStream openSource() throws IOException{
if(content!=null) return new ByteArrayInputStream(content);
if(source==null) return null;
return source.openStream();
}
public boolean hasContent(){
return content!=null;
}
}
/** Bucket interface to abstract i/o and provide easier extensibility.
* asContainer matches path and then returns local-to-packet path.
* signature returns a hash over lastModified or content that reflects modification.
*/
public static interface Bucket{
String getPrefix();
String asContained(String path);
boolean equals(String pref);
InputStream openSource(String local_path,FileServer user) throws IOException;
OutputStream openSink(String local_path,FileServer user) throws IOException;
String signature(String local_path);
Long lastModified(String local_path);
Long contentLength(String local_path);
CachedAsset getAsset(String local_path,FileServer user) throws IOException;
}
public static class FileBucket implements Bucket{
final String prefix;
String[] extAllowed;
Object[] domain;
LRUCache<String,CachedAsset> hit_history=new LRUCache<>(64);
public FileBucket(String prefix){
this.prefix=prefix;
extAllowed=new String[]{};
domain=new Object[]{};
}
@Override
public final String getPrefix(){return prefix;}
@Override
public String asContained(String path) {
if(!path.startsWith(prefix)) return null; // not contained
String local_path=path.replace(prefix,"");
if(extAllowed.length==0) return local_path;
for(String ext:extAllowed){
if(path.endsWith(ext)){
return local_path;
}
@Override
public String asContained(String path) {
if(!path.startsWith(prefix)) return null; // not contained
String local_path=path.substring(prefix.length());
if(extAllowed.length==0) return local_path;
for(String ext:extAllowed){
if(path.endsWith(ext)){
return local_path;
}
}
return null;
}
@@ -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<Bucket> buckets=new ArrayList<>();
String diskPrefix; // will be prefixed to source if file
String classPrefix; // will be prefixed to source if class
String urlPrefix; // will be prefixed to source if URL
public FileServer(String url_path,String offset,Object ... source){
super(null);
diskPrefix=classPrefix=offset;
addBucket(new FileBucket(url_path).setDomain(source));
public Object[] getDomain(){
return (domain!=null && domain.length>0)?domain:Resources.search_path;
}
@Override
public CachedAsset getAsset(String local_path,FileServer user) throws IOException{
Object[] sp=getDomain();
URL f=Resources.findFirst(user,local_path, sp);
if(f==null) return null;
URLConnection conn=f.openConnection();
long lastModified=conn.getLastModified();
long contentLength=conn.getContentLengthLong();
CachedAsset cached=hit_history.get(local_path);
if(cached!=null
&& cached.lastModified==lastModified
&& cached.contentLength==contentLength){
return cached;
}
byte[] content=null;
if(user.shouldCacheContent(local_path,contentLength)){
try(InputStream is=conn.getInputStream()){
content=is.readAllBytes();
contentLength=content.length;
}
}
CachedAsset asset=new CachedAsset(f,lastModified,contentLength,content);
hit_history.remove(local_path);
hit_history.put(local_path,asset);
return asset;
}
public InputStream openSource(String local_path,FileServer user) throws IOException{
CachedAsset asset=getAsset(local_path,user);
return asset!=null?asset.openSource():null;
}
public OutputStream openSink(String local_path,FileServer user) throws IOException{
return null;
}
public String signature(String local_path){
CachedAsset asset=hit_history.get(local_path);
return asset!=null?asset.signature:null;
}
@Override
public Long lastModified(String local_path){
CachedAsset asset=hit_history.get(local_path);
return asset!=null && asset.lastModified>0?asset.lastModified:null;
}
@Override
public Long contentLength(String local_path){
CachedAsset asset=hit_history.get(local_path);
return asset!=null && asset.contentLength>=0?asset.contentLength:null;
}
}
final ArrayList<Bucket> buckets=new ArrayList<>();
String diskPrefix; // will be prefixed to source if file
String classPrefix; // will be prefixed to source if class
String urlPrefix; // will be prefixed to source if URL
String indexFile;
String fallbackFile;
String assetCacheControl="public, max-age=3600";
String indexCacheControl="no-cache";
String fallbackCacheControl="no-cache";
long memoryCacheContentLimit=256*1024;
public FileServer(String url_path,String offset,Object ... source){
super(null);
diskPrefix=classPrefix=offset;
addBucket(new FileBucket(url_path).setDomain(source));
}
public FileServer(){
super(null);
@@ -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<Bucket> enumBuckets(){
return buckets.iterator();
}
public void publish(App app) {
Router rep=app.getRouter();
for(Bucket b:buckets) rep.addRoute("GET",b.getPrefix()+".*",this);
}
}
public Iterator<Bucket> enumBuckets(){
return buckets.iterator();
}
public void publish(App app) {
Router rep=app.getRouter();
for(Bucket b:buckets){
rep.addRoute("GET",b.getPrefix()+".*",this);
rep.addRoute("HEAD",b.getPrefix()+".*",this);
}
}
protected String resolveLocalPath(String local_path){
String normalized=normalizeLocalPath(local_path);
if((normalized==null || normalized.isEmpty()) && indexFile!=null){
return indexFile;
}
return normalized;
}
protected String normalizeLocalPath(String local_path){
if(local_path==null) return null;
local_path=local_path.replace('\\','/');
while(local_path.startsWith("/")) local_path=local_path.substring(1);
if(local_path.equals(".")) return "";
return local_path;
}
protected boolean shouldServeFallback(String local_path,Request request){
String normalized=normalizeLocalPath(local_path);
if(normalized==null) return false;
if(!acceptsHtml(request)) return false;
if(normalized.isEmpty()) return fallbackFile!=null;
int lastSlash=normalized.lastIndexOf('/');
int lastDot=normalized.lastIndexOf('.');
return lastDot < lastSlash + 1;
}
protected boolean tryServe(Bucket bucket,String local_path,Request request,Response response,boolean atDebug,boolean isFallback) throws IOException{
if(local_path==null) return false;
CachedAsset asset=bucket.getAsset(local_path,this);
if(asset==null) return false;
try(InputStream ins=asset.openSource()){
if(ins==null) return false;
response.setHeader("X-Content-Type-Options","nosniff");
String etag=asset.signature;
if(etag!=null){
response.setHeader("Cache-Control",cacheControlFor(local_path,isFallback));
response.setHeader("ETag",etag);
String etag_old=request.getHeader("If-None-Match");
if(etag.equals(etag_old)){
response.setStatus(Response.HTTP_NOT_MODIFIED);
return true;
}
}else{
response.setHeader("Cache-Control",cacheControlFor(local_path,isFallback));
}
Long lastModified=asset.lastModified>0?asset.lastModified:null;
if(lastModified!=null && lastModified>0){
response.setHeader("Last-Modified",java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME.format(
java.time.Instant.ofEpochMilli(lastModified).atZone(java.time.ZoneOffset.UTC)
));
}
Long contentLength=asset.contentLength>=0?asset.contentLength:null;
if(contentLength!=null && contentLength>=0){
response.setHeader("Content-Length",String.valueOf(contentLength));
}
if(atDebug) log().debug("\tfound:{}",local_path);
String ctype=HTTP.ext2mime(local_path);
response.setStatus(Response.HTTP_OK);
response.setContentType(ctype!=null?ctype:HTTP.MIME_BYTES);
if(!HTTP.VERB_HEAD.equals(request.getVerb())){
if(asset.hasContent()){
response.getEncoder().writeBytes(asset.content,0,asset.content.length);
}else{
response.getEncoder().writeStream(ins);
}
}
return true;
}
}
protected boolean shouldCacheContent(String local_path,long contentLength){
if(contentLength<0) return false;
return contentLength<=memoryCacheContentLimit;
}
protected boolean acceptsHtml(Request request){
if(request==null) return true;
String accept=request.getHeader("Accept");
if(accept==null || accept.trim().isEmpty()) return true;
accept=accept.toLowerCase();
return accept.contains("text/html") || accept.contains("application/xhtml+xml");
}
protected boolean isUnsafePath(String local_path){
String normalized=normalizeLocalPath(local_path);
if(normalized==null) return false;
return normalized.equals("..") || normalized.startsWith("../") || normalized.contains("/../");
}
protected String cacheControlFor(String local_path,boolean isFallback){
String normalized=normalizeLocalPath(local_path);
if(normalized==null || normalized.isEmpty() || normalized.equals(indexFile)){
return indexCacheControl;
}
if(isFallback){
return fallbackCacheControl;
}
return assetCacheControl;
}
}
@@ -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;
}
@@ -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 ?
@@ -1,22 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
/** Similar to a SAX interface used by parsers for XML and JSON to assemble DOM structures.
* Simply gets notified of events during parsing.
* @author amer
*/
public interface DecoderSink {
void beginDocument(Rec init);
Rec endDocument();
void beginElement(String name);
void endElement(String name);
void setKey(String name);
void setValue(CharSequence seq);
}
-158
View File
@@ -1,158 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/** Base class of meta objects.
* We use it to describe certain meta information. We derive from it Slot.
* We define keys list of slots on the header level to describe sub-slots.
*
* This class describes structure of Fields or Entities via the keys array of slots.
* Additionally we provide a number of methods to locate, set or get or remove or add slots.
* However slots could reside in other places such as base classes and so getOwnSlots will return a
* bare list of slots in this object while all other methods will take into account other sources.
* We do this to stay consistent at Rec level when Hdr inheritance comes into play.
*/
public class Hdr {
public static final int FLAG_ARRAY =0x0001;
public static final int FLAG_CHANGED =0x0002;
public static final int FLAG_STORABLE =0x0004;
public static final int FLAG_LOCKED =0x0008;
int flags;
String name;
String label;
Class<?> type;
final ArrayList<Slot> keys;
public Hdr(String name) {
this.name=name;
keys=new ArrayList<>();
}
public Hdr(String name,Class<?> type) {
this.name=name;
this.type=type;
keys=new ArrayList<>();
}
@Override
public String toString(){
StringBuilder ret=new StringBuilder();
ret.append(name).append(":");
ret.append("{")
.append("flags:").append(flags)
.append(",dim:").append(count())
.append("}");
return ret.toString();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLabel() {
return label!=null?label:name;
}
public void setLabel(String name) {
this.label = name;
}
public Class<?> getType() {
return type;
}
public void setType(Class<?> type) {
this.type = type;
}
public int getFlags(){
return flags;
}
public Hdr raiseFlags(int f){
flags|=f;
return this;
}
public Hdr clearFlags(int f){
flags&=~f;
return this;
}
public boolean checkFlags(int f){
return (flags & f)!=0;
}
public <T extends Hdr> T castAs(Class<T> clazz){
return clazz.cast(this);
}
public List<Slot> getOwnSlots(){
return keys;
}
public boolean isOwned(Slot s){
return keys.contains(s);
}
public Iterator<Slot> iterator(int offset){
return keys.listIterator(offset);
}
public int indexOf(String name){
return indexOf(name,0);
}
public int indexOf(String name,int ofs){
Iterator<Slot> it=iterator(ofs);
int index=-1;
while(it.hasNext()){
index+=1;
Slot e=it.next();
//if(e.getName().equalsIgnoreCase(name)) return index;
if(e.equals(name)) return index;
}
return -1;
}
public int indexOf(Slot s,int ofs){
Iterator<Slot> it=iterator(ofs);
int index=-1;
while(it.hasNext()){
index+=1;
Slot e=it.next();
if(e==s) return index;
}
return -1;
}
public Slot makeSlot(String name){
return new Slot(name);
}
/**
* this version will get or create a slot by given name.
* @param name slot name
* @param make wether to create if not present
* @return Slot or null.
*/
public Slot getSlot(String name,boolean make){
int index=indexOf(name);
if(index<0){
return make?makeSlot(name):null;
}else{
return getSlot(index);
}
}
public Slot getSlot(int pos){
return keys.get(pos);
}
public Hdr removeSlot(int pos){
keys.remove(pos);
return this;
}
public Hdr addSlot(Slot s){
keys.add(s);
return this;
}
public Hdr setSlot(int index,Slot s){
keys.set(index,s);
return this;
}
public int count(){
return keys.size();
}
}
-36
View File
@@ -1,36 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
import java.io.IOException;
/**
* Static methods related to JSON format.
*/
public class JSON {
private JSON(){
}
public static final Rec reads(CharSequence seq){
JSONDecoder dec=new JSONDecoder();
dec.beginDocument();
dec.parse(0, seq);
return dec.endDocument();
}
public static final void writes(Rec rec,Appendable sink) throws IOException{
JSONEncoder.encode(rec, sink);
}
public static final String toString(Rec rec){
StringBuffer buf=new StringBuffer();
try {
writes(rec,buf);
} catch (IOException e) {
}
return buf.toString();
}
}
@@ -1,333 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
import java.util.LinkedList;
import com.reliancy.util.Tokenizer;
import com.reliancy.util.Handy;
/** Special class which will tokenize string according to rules for JSON and feed the info to a listener.
* TODO: reuse headers in an array if same structure
* @author amer
*/
public class JSONDecoder implements TextDecoder,DecoderSink {
DecoderSink handler;
String[] inBody;
String[] sets;
String lastToken=null;
StringBuilder out = new StringBuilder();
public JSONDecoder(DecoderSink h){
handler=h;
String delimChars="{}[],;:=";
String escapeChars="'\"";
String whiteChars=" \t\r\f\n";//" \t\r\f\n";
inBody = new String[]{delimChars,escapeChars,whiteChars};
sets=inBody;
}
public JSONDecoder(){
this(null);
handler=this;
}
@Override
public int parse(int offset,CharSequence in){
int noffset=0;
while((noffset = Tokenizer.nextToken(offset, in, out, sets))!=offset){
offset=noffset;
if(out.length()==0) continue;
String token=out.toString();
out.setLength(0);
if("{".equals(token)){
if(lastToken!=null){
if(lastToken.startsWith("/*") || lastToken.startsWith("//")){
handler.setValue(lastToken); // support comments in our stream
}else{
handler.setKey(lastToken); // we consider string before { a key or name unless comment
}
lastToken=null;
}
handler.beginElement("object");
}else if("}".equals(token)){
if(lastToken!=null){
handler.setValue(lastToken);
lastToken=null;
}
handler.endElement("object");
}else if("[".equals(token)){
if(lastToken!=null){
handler.setValue(lastToken);
lastToken=null;
}
handler.beginElement("array");
}else if("]".equals(token)){
if(lastToken!=null){
handler.setValue(lastToken);
lastToken=null;
}
handler.endElement("array");
}else if(",".equals(token) || ";".equals(token)){
if(lastToken!=null){
handler.setValue(lastToken);
lastToken=null;
}
}else if(":".equals(token) || "=".equals(token)){
if(lastToken!=null){
handler.setKey(lastToken);
lastToken=null;
}
}else{
lastToken=token;
}
}
if(lastToken!=null){
handler.setValue(lastToken);
lastToken=null;
}
return offset;
}
Slot KEY=new Slot("__key",String.class);
/** We use a stack structure to manage recusion. */
LinkedList<Rec> stack=new LinkedList<Rec>();
/** will not add white space only nodes. */
boolean whitespaceIgnored=true;
boolean entitycharsIgnored=false;
public boolean isWhitespaceIgnored() {
return whitespaceIgnored;
}
public void setWhitespaceIgnored(boolean whitespaceIgnored) {
this.whitespaceIgnored = whitespaceIgnored;
}
public boolean isEntitycharsIgnored() {
return entitycharsIgnored;
}
public void setEntitycharsIgnored(boolean entitycharsIgnored) {
this.entitycharsIgnored = entitycharsIgnored;
}
public Rec getRoot() {
return stack.getLast();
}
public Rec getSubject(){
if(stack.isEmpty()) return null;
return stack.getFirst();
}
public void pushSubject(Rec n){
stack.push(n);
}
public Rec popSubject(){
Rec child=stack.pop();
Rec parent=getSubject();
if(parent==null) return child;
if(parent.isArray()){
parent.add(child);
}else{
String key=(String) parent.get(KEY,null);
Slot keyslot=parent.getSlot(key);
parent.remove(KEY).set(keyslot,child);
// if array and has key it should bomb
//parent.setArray(false);
}
return child;
}
public void beginDocument() {
beginDocument(null);
}
@Override
public void beginDocument(Rec init) {
sets=inBody;
out.setLength(0);
lastToken=null;
stack.clear();
Rec arr=new Obj(true);
stack.push(arr);
//System.out.println("BeginDoc");
}
@Override
public Rec endDocument() {
// need to set the actual parent
while(stack.getFirst()!=stack.getLast()){
popSubject();
}
// now adjust the root if it is array with only one child - one we added in start document as first element
Rec root=getSubject();
if(root.isArray() && root.count()==1 && root.get(0) instanceof Rec){
// ok we collapse our array from above - since we only have one object
Object bb=root.get(0);
Rec b=(Rec)bb ;
popSubject();
pushSubject(b);
}
//System.out.println("EndDoc");
return getRoot();
}
@Override
public void beginElement(String name) {
Rec element=new Obj("array".equals(name));
//element.setAttr(0);
pushSubject(element);
//System.out.println("BeginElement:"+name);
}
@Override
public void endElement(String name) {
// check if the correct end element is sent
Rec sub=this.getSubject();
if(!sub.isArray()) sub.remove(KEY);
// finally pop the root
popSubject();
//System.out.println("EndElement:"+name);
}
@Override
public void setKey(String name) {
Rec sub=this.getSubject();
String key=(String) sub.get(KEY,null);
if(key!=null){
// something is wrong - our tokizer might have ignored escape char or input has forgotten a delimiter
// we try to split name because it would contain key and value merged
int split=0;
if(name.startsWith("\"")) split=name.indexOf('\"', 1);
if(name.startsWith("'")) split=name.indexOf('\'', 1);
String val=name.substring(0,split+1);
setValue(val);
name=name.substring(split+1);
}
int start=0;int stop=name.length();
while(start<stop && (name.charAt(start)=='"' || name.charAt(start)=='\'')) start++;
while(start<stop && (name.charAt(stop-1)=='"' || name.charAt(stop-1)=='\'')) stop--;
sub.set(KEY, name.subSequence(start, stop));
//System.out.println("BeginAttribute:"+name);
}
@Override
public void setValue(CharSequence seq) {
if(seq==null) return;
Rec sub=this.getSubject();
String key=(String) sub.get(KEY,null);
if(key==null){
if(isWhitespaceIgnored() && Handy.isEmpty(seq)){
// skip empty strings
return;
}
// now key we are adding to body
Object val=interpretString(seq);
sub.add(val);
}else{
// we are setting attribute
Object val=interpretString(seq);
Slot keyslot=sub.getSlot(key);
sub.remove(KEY).set(keyslot,val);
// it should bomb if array and comes with key
//sub.setArray(false); // if it needs to be array why does it have a key
}
//System.out.println("Data:"+seq);
}
public Object interpretString(CharSequence seq){
int start=0;int stop=seq.length();
while(start<stop && seq.charAt(start)=='"' && seq.charAt(stop-1)=='"'){
start++;
stop--;
}
if(start==0 && stop==seq.length()){
// we do not trim single quotes unless double are missing
while(start<stop && seq.charAt(start)=='\'' && seq.charAt(stop-1)=='\''){
start++;
stop--;
}
}
seq=seq.subSequence(start, stop);
Object val=seq;
if(start==0){
String sVal=String.valueOf(seq);
// we did not have quotes - so try to interpet a few things
if("null".equalsIgnoreCase(sVal)){
val=null;
}else
if("true".equalsIgnoreCase(sVal)){
val=Boolean.TRUE;
}else
if("false".equalsIgnoreCase(sVal)){
val=Boolean.FALSE;
}else
if(Handy.isNumeric(sVal)){
if (sVal.indexOf(".") >= 0) {
val = Double.parseDouble(sVal);
} else {
val = Integer.parseInt(sVal);
}
}else if(this.isEntitycharsIgnored()==false && seq!=null && seq.length()>0){
// maybe it is a string after all
val=unescape(seq);
}
}else if(this.isEntitycharsIgnored()==false && seq!=null && seq.length()>0){
// we had quotes so lets decode escaed chars
val=unescape(seq);
}
return val;
}
public static CharSequence unescape(CharSequence str) {
StringBuilder buf = null;
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
if (ch == '\\' && i < (str.length() - 1)) {
i = i + 1;
char ch2 = str.charAt(i);
switch (ch2) {
case '"':
if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):"");
buf.append("\"");
break;
case '\\':
if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):"");
buf.append("\\");
break;
case '/':
if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):"");
buf.append("/");
break;
case 'b':
if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):"");
buf.append("\b");
break;
case 'f':
if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):"");
buf.append("\f");
break;
case 'n':
if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):"");
buf.append("\n");
break;
case 'r':
if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):"");
buf.append("\r");
break;
case 't':
if(buf==null) buf=new StringBuilder(i>0?str.subSequence(0, i-1):"");
buf.append("\t");
break;
default:
if(buf!=null) buf.append(ch);
}
} else {
if(buf!=null) buf.append(ch);
}
}
return buf!=null?buf.toString():str;
}
}
@@ -1,294 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
import java.io.IOException;
import java.util.List;
import java.util.Map;
public class JSONEncoder{
public JSONEncoder(){
}
/**
* We encode into an appendable various primitives and Rec.
* If appendable null then we just compute expected size.
* keys are not escaped they better not contain any special chars.
* values are quoted and escaped unless we detect a string that looks like a json object those are passed thru.
* in the past we tried to deduce if quoting was needed, but this is not the place to do so because we do not know how many times
* value was escaped so the only thing we can assume is that it needs to be escaped. So feeding a value that is quoted and
* escaped will return back on parse the same and will need to dequoted and descaped once more but that shoudl work fine with
* whoever quoted it in the upstream in the first place.
* @param val property value
* @param o encoding output
* @return length in characters of encoded result
* @throws IOException
*/
public static int encode(Object val,Appendable o) throws IOException {
int len = 0;
/*
// first key
if (key != null) {
if (o != null) {
o.append('"').append(key).append("\":");
}
len += 3 + key.length();
}
*/
// now value
if (val instanceof Object[]) {
Object[] valval = (Object[]) val;
if (o != null) {
o.append('[');
}
int index = 0;
for (Object obj : valval) {
if (index++ > 0) {
len += 1;
if (o != null) {
o.append(",");
}
}
len += encode(obj, o);
}
if (o != null) {
o.append(']');
}
len += 2;
} else if (val instanceof List) {
List<?> valval = (List<?>) val;
if (o != null) {
o.append('[');
}
int index = 0;
for (Object obj : valval) {
if (index++ > 0) {
len += 1;
if (o != null) {
o.append(",");
}
}
len += encode(obj, o);
}
if (o != null) {
o.append(']');
}
len += 2;
} else if (val instanceof Map) {
len+=encodeMap((Map<?,?>)val,o);
} else if (val instanceof Rec) {
len += encodeRec((Rec) val, o);
} else if (val instanceof Number || val instanceof Boolean) {
String str = val.toString();
if (o != null) {
o.append(str);
}
len += str.length();
}else if(val instanceof int[]){
int[] valval = (int[]) val;
if (o != null) {
o.append('[');
}
int index = 0;
for (int obj : valval) {
if (index++ > 0) {
len += 1;
if (o != null) {
o.append(",");
}
}
if(o!=null) o.append(String.valueOf(obj));
len += 1;
}
if (o != null) {
o.append(']');
}
len += 2;
}else if(val instanceof float[]){
float[] valval = (float[]) val;
if (o != null) {
o.append('[');
}
int index = 0;
for (float obj : valval) {
if (index++ > 0) {
len += 1;
if (o != null) {
o.append(",");
}
}
if(o!=null) o.append(String.valueOf(obj));
len += 1;
}
if (o != null) {
o.append(']');
}
len += 2;
}else if (val instanceof Object) {
String str = val.toString();
boolean jsontxt = false;
jsontxt |= str.length() > 0 && str.startsWith("{") && str.endsWith("}");
jsontxt |= str.length() > 0 && str.startsWith("[") && str.endsWith("]");
//boolean quoted=str.length() > 1 && str.startsWith("\"") && str.endsWith("\"");
// embedded json is not quoted and not escaped
// all other text is quoted otherwise we will prevent quoted quotes (those would be swallowed)
// we will not try to be smart if someone added an item that is quoted already it will be escaped and queotes retained
// we must be consistent so that repeated parse and encode works and not too smart here
// we need to put quotes around unless
if (!jsontxt) {
str = escape(str).toString();
if (o != null) {
o.append('"');
}
len += 1;
}
if (o != null) {
o.append(str);
}
len += str.length();
if (!jsontxt) {
if (o != null) {
o.append('"');
}
len += 1;
}
} else if (val == null) {
String str = "null";
if (o != null) {
o.append(str);
}
len += str.length();
}
return len;
}
public static int encodeMap(Map<?,?> valval,Appendable o) throws IOException{
int len=0;
if (o != null) {
o.append('{');
}
int index = 0;
for (Object obj : valval.keySet()) {
if (index++ > 0) {
len += 1;
if (o != null) {
o.append(",");
}
}
String key=obj.toString();
if (o != null) {
o.append('"').append(key).append("\":");
}
len += 3 + key.length();
len += encode(valval.get(obj), o);
}
if (o != null) {
o.append('}');
}
len += 2;
return len;
}
public static int encodeRec(Rec val,Appendable o) throws IOException{
int len=0;
if (o != null) {
o.append(val.isArray()?"[":"{");
}
for (int i=0;i<val.count();i++) {
Slot k=val.getSlot(i);
Object v=val.get(i);
if (i > 0) {
len += 1;
if (o != null) {
o.append(",");
}
}
if(k!=null){
String key=k.getName();
if (o != null) {
o.append('"').append(key).append("\":");
}
len += 3 + key.length();
}
len += encode(v, o);
}
if (o != null) {
o.append(val.isArray()?"]":"}");
}
len += 2;
return len;
}
/**
* @param str
* @return true if the string includes any of the special chars.
*/
public static boolean needsEscaping(String str) {
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
switch (ch) {
case '"':
case '\\':
case '/':
case '\b':
case '\f':
case '\n':
case '\r':
case '\t':
return true;
}
}
return false;
}
/**
* this helper method handle quotes and control chars.
* @param str input string
* @return output after encoding special chars
*/
public static CharSequence escape(CharSequence str) {
StringBuilder buf = null;
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
switch (ch) {
case '"':
if(buf==null) buf=new StringBuilder(str.subSequence(0,i));
buf.append("\\\"");
break;
case '\\':
if(buf==null) buf=new StringBuilder(str.subSequence(0,i));
buf.append("\\\\");
break;
case '/':
if(buf==null) buf=new StringBuilder(str.subSequence(0,i));
buf.append("\\/");
break;
case '\b':
if(buf==null) buf=new StringBuilder(str.subSequence(0,i));
buf.append("\\b");
break;
case '\f':
if(buf==null) buf=new StringBuilder(str.subSequence(0,i));
buf.append("\\f");
break;
case '\n':
if(buf==null) buf=new StringBuilder(str.subSequence(0,i));
buf.append("\\n");
break;
case '\r':
if(buf==null) buf=new StringBuilder(str.subSequence(0,i));
buf.append("\\r");
break;
case '\t':
if(buf==null) buf=new StringBuilder(str.subSequence(0,i));
buf.append("\\t");
break;
default:
if(buf!=null) buf.append(ch);
}
}
return buf!=null?buf:str;
}
}
-160
View File
@@ -1,160 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
import java.util.ArrayList;
import java.util.List;
/**
* Default implementation of a Rec.
* We separate keys and values because Obj could just be an array.
* If object is declated an array keys are nonexistant and rec related methods will return null or crash.
* Our setters return this object to main the calls chainable.
* Also positional calls accept negative values which reference from end backward.
*/
public class Obj implements Rec{
final List<Object> values;
final Hdr meta;
public Obj() {
values=new ArrayList<>();
meta=new Slot(null);
}
public Obj(boolean is_array) {
values=new ArrayList<>();
meta=new Slot(null);
if(is_array) meta.raiseFlags(Hdr.FLAG_ARRAY);
}
public Obj(List<Slot> k,List<Object> v) {
values=v;
meta=new Slot(null);
meta.keys.addAll(k);
}
/**
* This ctor is reserved for derivations with fixed slot definitions.
* This constructor will inspect static Slot members and construct keys that way
* if meta named.
* @param def
*/
protected Obj(Hdr def){
values=new ArrayList<>();
meta=def;
}
@Override
public String toString(){
StringBuilder buf=new StringBuilder();
toString(buf);
return buf.toString();
}
public int toString(StringBuilder buf){
boolean is_arr=isArray();
int length0=buf.length();// length before anything done
//StringBuffer indent=new StringBuffer(); // detect indent
//for(int i=length0;i>0 && Character.isWhitespace(buf.charAt(i));i--){
// indent.append(buf.codePointAt(i));
//}
buf.append(is_arr?"[":"{");
if(is_arr){
for(int pos=0;pos<count();pos++){
if(pos>0) buf.append(",");
Object val=this.get(pos);
if(val instanceof Obj) ((Obj)val).toString(buf);
else if(val!=null) buf.append(val.toString());
else buf.append("null");
}
}else{
for(int pos=0;pos<count();pos++){
if(pos>0) buf.append(",");
Slot s=getSlot(pos);
buf.append(s.getName()+":");
Object val=this.get(pos);
if(val!=null) s.toString(val,buf); else buf.append("null");
}
}
buf.append(is_arr?"]":"}");
return buf.length()-length0;
}
@Override
public Hdr meta(){
return meta;
}
@Override
public boolean isArray(){
return meta==null || meta.checkFlags(Hdr.FLAG_ARRAY);
}
@Override
public int count() {
return values.size();
}
@Override
public Rec set(int pos, Object val) {
if(pos<0) pos=count()+pos;
values.set(pos,val);
return this;
}
@Override
public Object get(int pos) {
if(pos<0) pos=count()+pos;
return values.get(pos);
}
@Override
public Rec add(Object val) {
values.add(val);
if(!isArray()) meta.addSlot(new Slot("arg"+count(),Object.class));
return this;
}
@Override
public Rec remove(int s) {
values.remove(s);
if(!isArray()) meta.removeSlot(s);
return this;
}
@Override
public Rec set(Slot s, Object val) {
if(s==null) throw new IllegalArgumentException("invalid key provided");
if(isArray()) throw new IllegalStateException("array not mappable with:"+s.getName());
int index=s.getPosition(); // try slot position
if(index<0) index=meta.indexOf(s.getName());// fall back to search if slot not set
if(index<0){
values.add(val);
meta.addSlot(s);
}else{
values.set(index,val);
meta.setSlot(index,s);
}
return this;
}
/**
* Returns value by slot key.
* If the underlying rec is a vec/array this method might work if slot is positioned else it will
* return def value.
*/
@Override
public Object get(Slot s, Object def) {
if(s==null) throw new IllegalArgumentException("invalid key provided");
//if(keys==null) throw new IllegalStateException("array not mappable with:"+s.getName());
int index=s.getPosition(); // try slot position
if(index<0 && !isArray()) index=meta.indexOf(s.getName());// fall back to search if slot not set
return index<0?def:values.get(index);
}
@Override
public Rec remove(Slot s) {
int index=s.getPosition(); // try slot position
if(index<0 && !isArray()) index=meta.indexOf(s.getName());// fall back to search if slot not set
if(index>=0) remove(index);
return this;
}
}
-27
View File
@@ -1,27 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
/**
* A record representation like in JSON.
* This is either an array or a map of fields.
* Each field definition we call a slot.
*/
public interface Rec extends Vec{
public Rec set(Slot s,Object val);
public Object get(Slot s,Object def);
public Rec remove(Slot s);
public default Slot getSlot(String name){
Hdr m=meta();
return m!=null?m.getSlot(name,true):null;
}
public default Slot getSlot(int pos){
Hdr m=meta();
return m!=null?m.getSlot(pos):null;
}
}
-73
View File
@@ -1,73 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
/**
* Slot is a definition of a value start with the name.
* We use it to define columns/fields of records.
* It is also used as header of actual records.
*/
public class Slot extends Hdr {
public static interface Initializer{
Object getInitalValue(Slot s,Rec rec);
}
public static final Initializer DEFAULT_INITIALIZER=new Initializer(){
public Object getInitalValue(Slot s,Rec rec) {return s.getInitValue();}
};
int position;
Object defaultValue;
Initializer initValue;
public Slot(String name){
this(name,Object.class);
}
public Slot(String name,Class<?> type){
super(name,type);
this.position=-1;
this.initValue=DEFAULT_INITIALIZER;
}
public boolean equals(String str){
return name.equalsIgnoreCase(str);
}
public int getPosition() {
return position;
}
public Slot setPosition(int position) {
this.position = position;
return this;
}
public Object getInitValue() {
return defaultValue;
}
public Slot setInitValue(Object defaultValue) {
this.defaultValue = defaultValue;
return this;
}
public Initializer getInitVia() {
return initValue;
}
public Slot setInitVia(Initializer initValue) {
this.initValue = initValue;
return this;
}
public int toString(Object val, StringBuilder buf) {
int length0=buf.length();
if(val instanceof Obj) ((Obj)val).toString(buf);
else if(val!=null) buf.append(val.toString());
else buf.append("null");
return buf.length()-length0;
}
public Object get(Rec r,Object def){
return r.get(this, def);
}
public Slot set(Rec r,Object val){
r.set(this, val);
return this;
}
}
@@ -1,19 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
/** An interface used in parser implementation.
*
* @author amer
*/
public interface TextDecoder {
void beginDocument(Rec init);
Rec endDocument();
public int parse(int offset,CharSequence in);
}
-25
View File
@@ -1,25 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
/**
* dimensioned container of values.
* Our setters return this object to make the calls chainable.
* Also positional calls accept negative values which reference from end backward.
*
*/
public interface Vec {
public default boolean isArray(){
return meta().checkFlags(Hdr.FLAG_ARRAY);
}
public Hdr meta();
public int count();
public Rec set(int pos,Object val);
public Object get(int pos);
public Rec add(Object val);
public Rec remove(int s);
}
@@ -1,138 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.util;
import java.util.HashMap;
/** One exception to rule them all.
* This exception works with ResultCode and represents and instance with context information.
* If a ResultCode is deemed parametric then we use provided parameters to update it when generating a message.
*
* @author amer
*/
public class CodeException extends RuntimeException {
protected final int code;
protected final HashMap<String,Object> context=new HashMap<>();
public CodeException(int code) {
this.code = code;
}
public CodeException(Throwable cause, int code) {
super(cause);
this.code = code;
}
@Override
public String toString(){
return getMessage();
}
public int getCode() {
return code;
}
public ResultCode getResultCode(){
return ResultCode.get(code);
}
@Override
public String getMessage() {
ResultCode rcode=getResultCode();
if(rcode!=null){
boolean wrapped=(rcode.getCode()==ResultCode.FAILURE);
String msg=rcode.getMessage();
if(msg.contains("$")){
for(String key:context.keySet()){
Object obj=context.get(key);
if(obj==null) continue;
String val=String.valueOf(obj);
msg=msg.replaceAll("\\$\\{"+key+"\\}",val);
}
}else if(this.getCause()!=null && wrapped){
msg=CodeException.getUserMessage(this.getCause());
}
return msg;
}else{
return "("+String.format("%08X", code)+")";
}
}
@SuppressWarnings("unchecked")
public <T> T get(String name) {
return (T)context.get(name);
}
public CodeException put(String name, String value) {
context.put(name, value);
return this;
}
public static CodeException wrap(Throwable exception) {
if (exception instanceof CodeException) {
CodeException se = (CodeException)exception;
return se;
} else {
return new CodeException(exception,ResultCode.FAILURE);
}
}
public static String getUserMessage(Throwable ex,Object context) {
return getUserMessage(ex);
}
public static String getUserMessage(Throwable ex) {
StringBuilder buf=new StringBuilder();
fillUserMessage(ex,buf,null);
return buf.toString();
}
public static Throwable fillUserMessage(Throwable ex,StringBuilder msg,StringBuilder title) {
Throwable c = ex;
//System.out.println(">>>"+c+"/"+c.getCause());
while(c.getCause()!=null){
Throwable cc= c.getCause();
if(c.getMessage()==null){
c=cc;continue;
}
String cMsg=c.getMessage();
String ccMsg=cc.getMessage();
//System.out.println("!!!"+cMsg+"/"+c.getClass().getName()+"/"+cc.getClass().getName());
boolean wrapped=(c instanceof CodeException) && ((CodeException)c).getCode()==ResultCode.FAILURE;
boolean plain_at=cMsg.equals(c.getClass().getName());
boolean plain_sub=cMsg.equals(cc.getClass().getName());
boolean same_msg=cMsg.equalsIgnoreCase(ccMsg);
//System.out.println("\t"+plain_sub+"#"+cc+"$"+cc.getCause()+"*"+cc.getMessage());
if(plain_at || plain_sub || cMsg.startsWith(cc.getClass().getName()+":") || same_msg || wrapped){
c=cc;
}else{
break;
}
}
//System.out.println("CC:"+c);
// take care of title
String _title=c.getClass().getSimpleName();
if(c instanceof CodeException){
CodeException cc=(CodeException) c;
if(cc.getCause()!=null){
_title=cc.getClass().getSimpleName();
}else{
// we do not have a cause
int code=cc.getCode();
ResultCode rcode=ResultCode.get(code);
if(rcode!=null) _title=rcode.getSource();
}
}
if(title!=null) title.append(_title);
// now take care of detail
String _msg=c.getLocalizedMessage();
if(_msg==null || _msg.trim().isEmpty()){
_msg=c.getClass().getSimpleName();
StackTraceElement[] se=c.getStackTrace();
if(se!=null && se.length>0) _msg+="\n\t at "+se[0].toString();
}
String prefString="Exception:";
String prefString2="Error:";
int prefix=_msg.lastIndexOf(prefString);
if(prefix<0) prefix=_msg.lastIndexOf(prefString2);
if(prefix>0 && _msg.substring(0, prefix).contains(".")) _msg=_msg.substring(prefix+prefString.length());
if(msg!=null) msg.append(_msg);
return c;
}
}
-593
View File
@@ -1,593 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
/**
* Common utility methods.
*/
public final class Handy {
public static final String WHITE=" \t\r\f\n";
/** place left-right around verb.
* @param verb body of text
* @param left to add left of verb
* @param left to add right of verb
* @return adjusted text
* */
public static String wrap(String verb, String left, String right) {
if(verb==null) verb="";
if(verb.startsWith(left) && verb.endsWith(right)) return verb;
return left+verb.trim()+right;
}
/** remove left-right around verb.
* @param verb body of text
* @param left to remove left of verb
* @param left to remove right of verb
* @return adjusted text
**/
public static String unwrap(String verb, String left, String right) {
if(verb==null) return verb;
String ret=verb.trim();
if(ret.startsWith(left) && ret.endsWith(right)){
ret=ret.substring(left.length());
ret=ret.substring(0,ret.length()-right.length());
}
return ret;
}
/** remove any chars elements from left of verb.
* @param verb body of text
* @return adjusted text
*/
public static String trimLeft(String verb,String chars){
while(verb.length()>0 && chars.indexOf(verb.charAt(0))!=-1){
verb=verb.substring(1);
}
return verb;
}
/** remove any chars elements from right of verb.
* @param verb body of text
* @return adjusted text
*/
public static String trimRight(String verb,String chars){
while(verb.length()>0 && chars.indexOf(verb.charAt(verb.length()-1))!=-1){
verb=verb.substring(0,verb.length()-1);
}
return verb;
}
/** remove any chars elements from right and right of verb.
* @param verb body of text
* @return adjusted text
*/
public static String trimBoth(String verb,String chars){
verb=trimLeft(verb, chars);
verb=trimRight(verb, chars);
return verb;
}
/** remove any chars elements from right and right of verb symetrically. trims whitespace first. */
public static String trimEvenly(String verb,String chars){
verb=trimBoth(verb," \t\n\r\f");
while(verb.length()>1){
char left=verb.charAt(0);
char right=verb.charAt(verb.length()-1);
if(left!=right) break; // left-right not even
if(chars.indexOf(left)<0) break; // even but not in chars list
verb=verb.substring(1,verb.length()-1);
}
return verb;
}
public static <T> T nz(T val, T def){
return val!=null?val:def;
}
/** Convert incoming value to an expected class.
* @param clazz expected class
* @param val observed value
* @return val converted to type clazz.
*/
public static Object normalize(Class<?> clazz, Object val ) {
if(val==null) return null; // we are null
if(clazz.isAssignableFrom(val.getClass())) return val; // we are assignable
if(val instanceof String){
String value=(String) val;
if(value.isEmpty() || value.equals("''") || value.equals("\"\"")) return null;
if( Boolean.class==( clazz ) || boolean.class==( clazz ) ) return Boolean.parseBoolean( value );
if( Byte.class==( clazz ) || byte.class==( clazz ) ) return Byte.parseByte( value );
if( Short.class==( clazz ) || short.class==( clazz ) ) return Short.parseShort( value );
if( Integer.class==( clazz ) || int.class==( clazz ) ) return Integer.parseInt( value );
if( Long.class==( clazz ) || long.class==( clazz )) return Long.parseLong( value );
if( Float.class==( clazz ) || float.class==( clazz ) ) return Float.parseFloat( value );
if( Double.class==( clazz ) || double.class==( clazz )) return Double.parseDouble( value );
}
if(clazz==String.class || clazz==CharSequence.class){
return String.valueOf(val);
}
return val;
}
/**
* This method is a bit more complex because it locks onto two delimiters one for grouping and other
* for decimal point and chooses those from a list of [space],'`. which are used all over the world in different places.
* Returns true if the string only contains digits and numeric characters.
* This should match 1000000 also 1,000,000 and also 1,000,000.00 but it is still not possible to differentiate between 1,000 =1000 in us
* from 1000,00 whch is used in europe. So it is difficult to normalize the string so it could process any number.
* @param str string to test
* @return trie if string looks numeric or is null/empty
*/
public static final boolean isNumeric(String str){
int strLen;
if (str == null || (strLen = str.length()) == 0) {
return true;
}
String delims=" ,.'`";
int delimNumber=0; // 0 we load, 1 we let one more, 2 we exit on second occurance
char delimUsed=0; // char used as delim
boolean delimLast=false;
int digitCount=0;
for (int i = 0; i < strLen; i++) {
char ch=str.charAt(i);
boolean accept=Character.isDigit(ch);
if(accept) digitCount++;
accept=accept || (ch=='-' && i==0);
accept=accept || (ch=='+' && i==0);
if(delims.indexOf(ch)>=0){
accept=!delimLast; // prevent delims following each other
delimLast=true;
if(delimNumber==0){
delimNumber=1;
delimUsed=ch;
}else if(delimNumber==1){
if(delimUsed!=ch) delimNumber=2;
delimUsed=ch;
}else{
// we have seen two different delim and whatever is coming here is breaking numeric format like second delim second time or some otehr
accept=false;
}
}else{
delimLast=false;
}
if(!accept) return false;
}
return digitCount>0;
}
/**
* @return true if the string is null, empty or contains only white space.
*/
public static boolean isBlank(CharSequence str) {
int strLen;
if (str == null || (strLen = str.length()) == 0) {
return true;
}
for (int i = 0; i < strLen; i++) {
if ((Character.isWhitespace(str.charAt(i)) == false)) {
return false;
}
}
return true;
}
/**
* Provides a unified notion of what constitutes an empty value.
* For any object if it is null.
* For string also if it is blank
* For arrays and lists and collection and maps also if no entries or keys exist.
* @param value anything
* @return true if any of the above matches
*/
public static boolean isEmpty(Object value){
if(value==null) return true;
if(value instanceof CharSequence){
return isBlank((CharSequence)value);
}
Class<?> cls=value.getClass();
if(cls.isArray()) {
if(value instanceof Object[]){
Object[] arr=(Object[]) value;
if(arr.length==0) return true;
for(int i=0;i<arr.length;i++) if(isEmpty(arr[i])==false) return false;
return true;
}if(value instanceof byte[]){
return ((byte[])value).length==0;
}else if(value instanceof short[]){
return ((short[])value).length==0;
}else if(value instanceof int[]){
return ((int[])value).length==0;
}else if(value instanceof long[]){
return ((long[])value).length==0;
}else if(value instanceof float[]){
return ((float[])value).length==0;
}else if(value instanceof double[]){
return ((double[])value).length==0;
}else{
return false;
}
}
if(value instanceof Collection){
Collection<?> c=(Collection<?>)value;
if(c.isEmpty()) return true;
for(Object o:c){
if(isEmpty(o)==false) return false;
}
return true;
}
return false;
}
/** Attempts to take a compact string and beautify it.
* Will uppercase the first letter. Will also expand CamelCase.
* Also will replace _ with empty space.
* @param str
* @return nicely formatted string ready for display
*/
public static final String prettyPrint(String str){
if(str==null) return "";
boolean fix=false;
char prevCh=0;
if(str.startsWith("org.") || str.startsWith("net.") || str.startsWith("com.") || str.startsWith("java.")){
str=str.substring(1+str.lastIndexOf('.')); // we strip class name paths
}
for(int i=0;i<str.length();i++){
char currCh=str.charAt(i);
if(i==0 && Character.isLowerCase(currCh)) fix=true;
if(Character.isLowerCase(prevCh) && Character.isUpperCase(currCh)) fix=true;
if(Character.isUpperCase(prevCh) && Character.isUpperCase(currCh) && i<(str.length()-1) && Character.isLowerCase(str.charAt(i+1))) fix=true;
if(!Character.isLetter(currCh)) fix=true;
prevCh=currCh;
}
if(!fix) return str;
StringBuilder bufs=new StringBuilder();
boolean toUC=false;
for(int i=0;i<str.length();i++){
char currCh=str.charAt(i);
if(currCh=='_') currCh=' ';
prevCh=bufs.length()>0?bufs.charAt(bufs.length()-1):currCh;
if(Character.isWhitespace(currCh)){
if(!Character.isWhitespace(prevCh)){
bufs.append(' ');
}
continue; // ignore repeated whitespace otherwise emit space
}
if(bufs.length()==0){
toUC=true;
}else if((!Character.isUpperCase(prevCh) && ("-+/%*".indexOf(prevCh)==-1 || Character.isLetter(prevCh))) && Character.isUpperCase(currCh)){
// non uc (a not one of operands) behind, uc ahead
bufs.append(" ");
}else if(Character.isLetter(prevCh) && Character.isDigit(currCh)){
// letter behind, digit ahead
bufs.append(" ");
}else if(Character.isUpperCase(prevCh) && Character.isUpperCase(currCh) && i<(str.length()-1) && Character.isLowerCase(str.charAt(i+1))){
// behind me uppercase infrom uppercase then lowercase
bufs.append(" ");
}
bufs.append(toUC?Character.toUpperCase(currCh):currCh);
toUC=false;
}
while(bufs.length()>0 && Character.isWhitespace(bufs.charAt(bufs.length()-1))){
// trims whitespace from end
bufs.setLength(bufs.length()-1);
}
return bufs.toString();
}
/** Attempts to take a user string and compact it to camel case.
* @param value more or less presentable string
* @return nicely compact string
*/
public static String toCamelCase(String value) {
if(value==null || value.trim().isEmpty()) return "";
StringBuilder sb = new StringBuilder();
//final char delimChar = ' ';
boolean flip = false;
for (int charInd = 0; charInd < value.length(); charInd++) {
char ch = value.charAt(charInd);
if (Character.isWhitespace(ch)) {
flip = true;
}else if(flip){
flip = false;
if(ch==Character.toLowerCase(ch)) sb.append("_");
sb.append(ch);
}else{
sb.append(ch);
}
}
return sb.toString();
}
public static byte[] deflate(byte[] content) throws IOException{
Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION,true);
deflater.setInput(content);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(content.length);
deflater.finish();
byte[] buffer = new byte[1024];
while (!deflater.finished()) {
int count = deflater.deflate(buffer); // returns the generated code... index
outputStream.write(buffer, 0, count);
}
outputStream.close();
byte[] output = outputStream.toByteArray();
return output;
}
public static byte[] inflate(byte[] contentBytes) throws IOException, DataFormatException{
Inflater inflater = new Inflater(true);
inflater.setInput(contentBytes);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(contentBytes.length);
byte[] buffer = new byte[1024];
while (!inflater.finished()) {
int count = inflater.inflate(buffer);
outputStream.write(buffer, 0, count);
}
outputStream.close();
byte[] output = outputStream.toByteArray();
return output;
}
public static void shuffle(Object[] e){
Random rn = new Random();
for(int i=0;i<e.length;i++){
int other=rn.nextInt(e.length);
Object tmp=e[i];
e[i]=e[other];
e[other]=tmp;
}
}
public static String encodeBase64(byte[] data){
return Base64.getEncoder().encodeToString(data);
}
public static byte[] decodeBase64(String data){
return Base64.getDecoder().decode(data);
}
/** Simple XOR encryption of a map of key-value pairs.
* We randomize the order of key value pairs to make the string more unpredictable.
* Returned string is base64 and web safe
* @param key encryption key
* @param m map of param-value pairs to encrypt values.
* @return a string of encoded map param-value pairs which were then encrypted
*/
public static final String encrypt(String key,Map<String,String> m){
String ret=null;
Object[] es=m.keySet().toArray();
shuffle(es); // we shuffle entries to confuse the string a bit
StringBuilder buf=new StringBuilder();
for(int i=0;i<es.length;i++){
Object e=es[i];
if(i>0) buf.append("\n");
buf.append(e).append(":").append(m.get(e));
}
ret=encryptString(key,buf.toString());
return ret;
}
/**
* This method will encrypt a string and return BASE 64 string that is web safe.
* TO make the string web safe we replace + with - and / with _
* Must revert this change on the reverse.
* @param key
* @param ret
* @return
*/
public static final String encryptString(String key,String ret){
try{
byte[] bkey=key.getBytes("UTF-8");
byte[] bstr=ret.getBytes("UTF-8");
for(int i=0;i<bstr.length;i++){
bstr[i]=(byte)(bstr[i] ^ bkey[i%bkey.length]);
}
// now need to encode this
ret=encodeBase64(bstr);
ret=ret.replace('+','-');
ret=ret.replace('/','_');
ret=ret.replace('=','.');
ret=ret.replace("\n","");
}catch(Exception e){
ret="";
}
return ret;
}
/**Reverses the effects of encrypt.
* Also changes
* @param key
* @param m
* @return values decrypted and parsed into key-value pair along newline.
*/
public static final Map<String,String> decrypt(String key,String m){
m=decryptString(key,m);
Map<String,String> ret=new HashMap<>();
//System.out.println("Output:"+m);
Tokenizer tokz=new Tokenizer(m);
tokz.setDelimChars("\n");
tokz.setWhiteChars(null);
for(String t=tokz.nextToken();t!=null;t=tokz.nextToken()){
if("\n".equals(t)) continue;
String[] kv=t.split(":",2);
ret.put(kv[0],kv.length>1?kv[1]:null);
}
return ret;
}
public static final String decryptString(String key,String m){
try{
//m=URLDecoder.decode(m, "UTF-8");
m=m.replace('-','+');
m=m.replace('_','/');
m=m.replace('.','=');
byte[] bkey=key.getBytes("UTF-8");
byte[] bstr=decodeBase64(m);
for(int i=0;i<bstr.length;i++){
bstr[i]=(byte)(bstr[i] ^ bkey[i%bkey.length]);
}
m=new String(bstr,"UTF-8");
}catch(Exception e){
}
return m;
}
/**
* Generates a hash string with the algorithm name prefixed.
* @param message text to hash
* @param algorithm algorithm to use
* @return hash digest
*/
public static String hashString(String message, String algorithm) throws NoSuchAlgorithmException, UnsupportedEncodingException{
if(message==null) return message;
MessageDigest digest = MessageDigest.getInstance(algorithm);
byte[] hashedBytes = digest.digest(message.getBytes("UTF-8"));
return algorithm.toLowerCase()+":"+encodeBase64(hashedBytes);
}
/** hash text using sha256. */
public static String hashSHA256(String message){
try{
return hashString(message,"SHA-256");
}catch(Exception ex){
return "sha-256:"+Integer.toHexString(message.hashCode());
}
}
public static String hashMD5(String input){
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] messageDigest = md.digest(input.getBytes());
BigInteger no = new BigInteger(1, messageDigest);
String hashtext = no.toString(16);
while (hashtext.length() < 32) {
hashtext = "0" + hashtext;
}
return hashtext;
}catch (NoSuchAlgorithmException e) { // For specifying wrong message digest algorithms
throw new RuntimeException(e);
}
}
public static String toHexString(byte[] hash){
char[] HEX_CHARS = "0123456789ABCDEF".toCharArray();
StringBuilder sb = new StringBuilder(hash.length * 2);
for (byte b : hash) {
sb.append(HEX_CHARS[(b & 0xF0) >> 4]);
sb.append(HEX_CHARS[b & 0x0F]);
}
return sb.toString();
}
/**
* Finds first occurrence of sub inside body with and without case.
* We implement this search via a FSM and ignore the case.
* @param body text to search
* @param sub subsequence to find
* @param offset offset from 0
* @return offset of next occurance starting at offiset
*/
public static final int indexOf(CharSequence body,CharSequence sub,int offset){
if(body==null) return -1;
int state=0;
int blen=body.length();
int slen=sub.length();
boolean ignorecase=true;
for(int index=offset;index<blen;index++){
char bC=body.charAt(index);
char sC=sub.charAt(state);
if(ignorecase){
bC=Character.toLowerCase(bC);
sC=Character.toLowerCase(sC);
}
if(bC==sC) state+=1; else state=0;
if(state>=slen) return index-slen+1; // we found a match
}
return -1;
}
/**
* Will trim the string from left and right and remove any of the symbols.
* @param trim text to strip
* @param sym set of characters to trim
*/
public static String trim(String trim,String sym) {
if(trim==null || trim.length()==0) return trim;
int start=0;
int end=trim.length();
while(start<trim.length() && sym.indexOf(trim.charAt(start))!=-1) start++;
while(0<end && sym.indexOf(trim.charAt(end-1))!=-1) end--;
if(start==0 && end==trim.length()) return trim;
return start<end?trim.substring(start, end):"";
}
/** will copy contents of a list into a fixed length array */
public static String[] asArray(List<String> all) {
if(all==null) return null;
String[] ret=new String[all.size()];
all.toArray(ret);
return ret;
}
public static String toString(Object...args){
StringBuilder buf=new StringBuilder();
if(args.length>1){
buf.append("[");
for(int i=0;i<args.length;i++) buf.append(i==0?"":",").append(toString(args[i]));
buf.append("]");
}else if(args.length==1){
Object arg=args[0];
if(arg instanceof Iterable){
java.util.Iterator<?> it=((Iterable<?>)arg).iterator();
buf.append("[");
while(it.hasNext()) buf.append(buf.length()>1?",":"").append(it.next());
buf.append("]");
}else
if(arg instanceof java.util.Map){
java.util.Map<?,?> marg=(Map<?,?>) arg;
buf.append("{");
for(java.util.Map.Entry<?,?>e:marg.entrySet()){
buf.append(e.getKey().toString()).append(":").append(toString(e.getValue()));
}
buf.append("}");
}else{
buf.append(String.valueOf(arg));
}
}
return buf.toString();
}
/** splitting without using regex.
* leading or trailing delims will produce empty tokens.
* if you need to split on a set of single chars please use tokenizer.
* @param delim delim string
* @param str body of text to chop
* @param delim_count maximal number of splits or -1 for all
* @return array of tokens
*/
public static String[] split(String delim,String str,int delim_count) {
ArrayList<String> ret=new ArrayList<>();
int delimLen=delim!=null?delim.length():0;
int len=str.length();
int index=0;
int delimCnt=0; // track splits
int delimAt=0; // last delim position
while(index<len){
if(delim_count>0 && delimCnt>=delim_count) break; // reached limit of delims
delimAt=delimLen>0?str.indexOf(delim, index):index+1;
if(delimAt<0) break; // no more delims
// we got a hit
delimCnt+=1;
ret.add(str.substring(index, delimAt)); // add token
index=delimAt+delimLen;
}
if(index<len || delimAt>0){
// add remainder (no more delim or delim at end)
ret.add(str.substring(index));
}
return ret.toArray(new String[ret.size()]);
}
/** split a string as often as possible. */
public static String[] split(String delim,String str) {
return split(delim,str,-1);
}
@SafeVarargs
public static <T> Iterator<T> chainIterators(Iterator<T>...its){
return new JointIterator<T>(its);
}
}
@@ -1,35 +0,0 @@
package com.reliancy.util;
import java.util.Iterator;
import java.util.NoSuchElementException;
/** Chains multiple iterators to act as one.
*
*/
public class JointIterator<T> implements Iterator<T> {
final Iterator<T> iterators[];
int cursor;
@SafeVarargs
public JointIterator(Iterator<T> ...its){
this.iterators=its;
cursor=0;
}
@Override
public boolean hasNext() {
while(cursor<iterators.length){
if(iterators[cursor].hasNext()) return true;
cursor+=1; // cursor exhausted got to next iterator
}
return false;
}
@Override
public T next() {
if(cursor<iterators.length){
return iterators[cursor].next();
}else{
throw new NoSuchElementException();
}
}
}
@@ -1,79 +0,0 @@
package com.reliancy.util;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
/** Least recently used cache is a useful map of sorts.
* It has a fixed capacity and it forgets least used entries if new are added.
* If an allocator is installed it is consulted on cache miss.
* If a disposer is installed is is consulted on cache overflow.
* We can provide the same object that implements both and make use of a pool.
*/
public class LRUCache<K,V>{
public static interface Allocator<K,V>{
V request(K key);
}
public static interface Disposer<K,V>{
void release(K key,V val);
}
final Map<K,V> data;
int capacity;
final LinkedList<K> order=new LinkedList<>();
Allocator<K,V> allocator;
Disposer<K,V> disposer;
public LRUCache(int capacity,Map<K,V> backend){
this.capacity=capacity;
data=backend!=null?backend:new HashMap<K,V>();
}
public LRUCache(int capacity){
this(capacity,null);
}
public LRUCache<K,V> setAllocator(Allocator<K,V> a){
allocator=a;
return this;
}
public LRUCache<K,V> setDisposer(Disposer<K,V> a){
disposer=a;
return this;
}
public int size() {
return data.size();
}
public boolean containsKey(Object key) {
return data.containsKey(key);
}
public boolean containsValue(Object value) {
return data.containsValue(value);
}
public V get(K key) {
V ret=data.get(key);
if(ret!=null){
//cache is hit
order.remove(key);
order.addFirst(key);
}else{
//cache is missed
ret=allocator!=null?allocator.request(key):null;
}
return ret;
}
public V put(K key, V value) {
if(order.size()>=capacity){
// capacity is reached
K last=order.removeLast();
data.remove(last);
if(disposer!=null) disposer.release(key, value);
}
order.addFirst(key);
return data.put(key,value);
}
public V remove(Object key) {
order.remove(key);
return data.remove(key);
}
public void clear() {
order.clear();
data.clear();
}
}
-69
View File
@@ -1,69 +0,0 @@
package com.reliancy.util;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
/** Logging support based on JUL.
* We implement a deferred logmanager that survives shutdownhook until we release.
*/
public class Log {
static {
// must be called before any Logger method is used.
System.setProperty("java.util.logging.manager", DeferredMgr.class.getName());
System.setProperty("java.util.logging.SimpleFormatter.format","%1$tF %1$tT %4$-7s [%3$s] %5$s%6$s%n");
}
public static class DeferredMgr extends LogManager {
@Override public void reset() { /* don't reset yet. */ }
private void resetFinally() { super.reset(); }
}
public static Logger setup(){
Logger root_logger=Logger.getLogger("");
return root_logger;
}
public static void cleanup(){
LogManager mgr=LogManager.getLogManager();
if(mgr instanceof DeferredMgr){
((DeferredMgr)mgr).resetFinally();
}
}
public static void setLevel(Logger logger,String level_name){
if(level_name==null || level_name.isEmpty()) level_name="ERROR";
level_name=level_name.toUpperCase();
switch(level_name){
case "v":{
level_name="WARN";
break;
}
case "vv":{
level_name="INFO";
break;
}
case "vvv":{
level_name="DEBUG";
break;
}
}
switch(level_name){
case "WARN":{
level_name="WARNING";
break;
}
case "DEBUG":{
level_name="FINER";
break;
}
case "ERROR":{
level_name="SEVERE";
break;
}
}
Level lvl=Level.parse(level_name);
logger.setLevel(lvl);
for (Handler h : logger.getHandlers()) {
h.setLevel(lvl);
}
}
}
-440
View File
@@ -1,440 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.util;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
/** Path to a resource almost identical to a URL.
* It might but should not hold any handles. It holds the address and
* possibly takes care of looking up addresses.
* The richest syntax is:
* <pre> {@code PROTOCOL://USER:PWD@MACHINE:PORT/DATABASE?key=val&... } </pre>
* Properties are held in their own string and need to be decoded.
*
* We use forward slash for path delimitation of database portion. For the rest we preserve other slashes to allow windows domain\\user or server\\instance.
* Special chars are [/@?,:;]
* if any are found at the end of protocol we skip ://
* if any are found at the beginning of properties we skip ?
* if any are found at the beginning of database we skip /
*
* In windows we have volume or drive letters which are postfixed by colon : then slashes. We treat the volume as part of database.
* We store the database without first slash to allow specification of relative paths.
* To render it as absolute set protocol or host to empty string instead of null. Then a slash will be prefixed and you will get absolute path.
* @author amer
*/
public class Path {
static final String SYMBOLS="/@?,:;";
String connectstring;
String protocol; ///< protocol guides the interpretation of the other elements in conn, string
String userid; ///< authentication
String password; ///< authorization
String host; ///< machine or computer
String port; ///< access to computer
String database; ///< name of database or filename
String properties; ///< properties are what follows ? in a url
public Path(String connect,boolean do_parse) {
if(do_parse){
parse(connect);
}else{
connectstring=connect;
}
}
public Path(String connect) {
this(connect,true);
}
public Path(Path in) {
connectstring=in.connectstring;
protocol=in.protocol;
userid=in.userid;
password=in.password;
host=in.host;
port=in.port;
database=in.database;
properties=in.properties;
}
@Override
public String toString() {
return assemble();
}
/**
* Converts the path to a file.
* If absolute is true
* @param absolute if true forms absolute path else will return relative path
* @return
*/
public File toFile(boolean absolute){
String proto=getProtocol();
String host=getHost();
try{
setProtocol(absolute?"":null);
setHost(absolute?"":null);
String path=toString();
return new File(path);
}finally{
setProtocol(proto);
setHost(host);
}
}
public URL toURL() throws MalformedURLException{
String path=toString();
if(Handy.isBlank(getHost()) && path.contains("://") && !path.contains(":///")) path=path.replace("://",":///");
return new URL(path);
}
public Path clear(){
connectstring=null;
protocol=null;
userid=null;
password=null;
host=null;
port=null;
database=null;
properties=null;
return this;
}
public String assemble() {
if(connectstring!=null) return connectstring;
// assemble the connect string
StringBuilder buf=new StringBuilder();
//boolean absolute=false;
if(!Handy.isBlank(protocol)){
buf.append(protocol);
if(SYMBOLS.indexOf(protocol.charAt(protocol.length()-1))<0) buf.append("://");
}
if(!Handy.isBlank(host)){
if(userid!=null && password!=null){
buf.append(userid).append(":").append(password).append("@");
}
buf.append(host);
if(port!=null) buf.append(":").append(port);
}
if(!Handy.isBlank(database)){
if(buf.length()>0 && SYMBOLS.indexOf(database.charAt(0))<0){
// we got something in front so we need to use slash
buf.append("/");
}else if(protocol!=null || host!=null){
boolean winvol=database.length()>2 && database.charAt(1)==':' && (database.charAt(2)=='/' || database.charAt(2)=='\\');
// we got nothing in front but if host or protocol empty but not null we treat as absolute
if(!winvol) buf.append("/");
}
buf.append(database);
}
if(properties!=null){
if(SYMBOLS.indexOf(properties.charAt(0))<0) buf.append("?");
buf.append(properties);
}
connectstring=buf.toString();
return connectstring;
}
public Path parse(String connect) {
clear();
if (connect == null) {
return this;
}
this.connectstring=connect;
// first get protocol - everything up to : which is not followed by a symbol (includes :// but also c:/
int oldst=0;
int st=0;
for(int i=0;i<(connectstring.length()-1);i++){
char curr=connectstring.charAt(i);
if(curr==':'){
oldst=st;
st=i;
}else if(SYMBOLS.indexOf(curr)!=-1){
if(curr=='@') st=oldst; // this will back out one : if protocl search ended with @ indicating a server
break;
}
}
if(st==1){ st=0;} // this will supress single letter protocols i.e. c:/ ued in windows as part of database/file
if(2==(st-oldst)) st=oldst;
if(st>0){
this.protocol=connectstring.substring(0,st);
while(SYMBOLS.indexOf(connectstring.charAt(st))!=-1) st++; // advance over symbols
}
// next assume the rest is a file/database
database = connectstring.substring(st);
// now check for user id and password
st = database.indexOf('@');
boolean checkhost=st>=0;
if(!Handy.isBlank(protocol)){
checkhost=!protocol.contains(":file") && !protocol.contains(":mem") && !protocol.equals("file") && !protocol.equals("mem");;
}
if (st != -1) {
userid = database.substring(0, st);
if(userid.contains("%4")) try{userid=URLDecoder.decode(userid,"UTF-8");}catch(Exception e){}
database = database.substring(st + 1);
// now try to split user id into password if possible
st = userid.indexOf(':');
if (st != -1) {
password = userid.substring(st + 1);
userid = userid.substring(0, st);
}
}
// ok next try to split up machine if possible (only for absolute urls)
st = database.indexOf(':');
if(st<0) st=database.indexOf('/');
if (st != -1 && checkhost) {
boolean portfollows=database.charAt(st)==':';
host = database.substring(0, st);
// now try to recover port
if(portfollows){
int st2 = database.indexOf(':',st+1);
if(st2<0) st2 = database.indexOf('/',st+1);
if (st2 != -1 && st2>(st+1)) { // we have a port
port = database.substring(st + 1,st2);
st=st2;
}else{
// no port we have : then / - which is used in windows to indicate volume and we treat as part of database
st=-1;
host="";
}
}
database = database.substring(st + 1);
}
database=fixSlashes(database);
// finally split the properties from database
st = database.indexOf('?');
if(st==-1) st=database.indexOf(';');
if (st != -1) { // we have properties
properties = database.substring(st);
database = database.substring(0, st);
}
return this;
}
/**
* Absolute ResourcePath will have a protocol
*/
public boolean isAbsolute() {
return (protocol != null || host!=null);
}
/// will clear host and protocol using empty string thereby making database absolute path
public Path setAbsolute(){
setHost("");
setProtocol("");
return this;
}
public String getDatabase() {
return database;
}
public Path setDatabase(String database) {
this.database = database;
connectstring=null;
return this;
}
public String getHost() {
return host;
}
public Path setHost(String host) {
this.host = host;
connectstring=null;
return this;
}
public String getPassword() {
return password;
}
public Path setPassword(String password) {
this.password = password;
connectstring=null;
return this;
}
public String getPort() {
return port;
}
public Path setPort(String port) {
this.port = port;
connectstring=null;
return this;
}
public String getProtocol() {
return protocol;
}
public Path setProtocol(String protocol) {
this.protocol = protocol;
connectstring=null;
return this;
}
public String getUserid() {
return userid;
}
public Path setUserid(String userid) {
this.userid = userid;
connectstring=null;
return this;
}
public String getProperties() {
return properties;
}
public Path setProperties(String userid) {
this.properties = userid;
connectstring=null;
return this;
}
public String getBase() {
return Path.getBase(database);
}
public String getExtension() {
return Path.getExtension(database);
}
public String getPathItem() {
return Path.getPathItem(database);
}
/** Ensures that we use forward slashes and that single dot is not present mid or and the end.
*
* @param path a unix or windows or uri path
* @return a path with forward slashes
*/
public static String fixSlashes(String path) {
if(path==null || path.length()==0) return path;
path=path.replace("\\", "/");
path=path.replace("/./","/");
while(true){
if(path.endsWith("/")) path=path.substring(0,path.length()-1);
else if(path.endsWith("/.")) path=path.substring(0,path.length()-2);
else break;
}
return path;
}
/** returns database path given path and file.
* We assume the path uses forward backslash for delimitation.
*/
public static String getBase(String path) {
int st1 = path.lastIndexOf('/');
int st2 = path.lastIndexOf('\\');
int st=st2>st1?st2:st1;
if (st == -1) {
return null;
}
return path.substring(0, st);
}
public static String getExtension(String path) {
int st=Math.max(path.lastIndexOf('/'),path.lastIndexOf('\\'));
int st2 = path.lastIndexOf('.');
if (st2 == -1 || (st>0 && st2<st)) {
return null;
}
return path.substring(st2 + 1);
}
public static String getPathItem(String path) {
path=path.replace('\\','/');
int st11 = path.lastIndexOf('/');
int st1=1+st11;
int st2 = path.lastIndexOf('.');
if (st2 <0) {
st2 = path.length();
}
return path.substring(st1, st2);
}
/**
* Assuming that url starts with base will return string beyond base in url.
* @param base
* @param url
*/
public static String getRemainder(String base,String url){
if(base.length()>=url.length()) return null;
return url.substring(base.length());
}
/**
* unites two paths.
* @param base
* @param url
*/
public static String getUnion(String base,String url){
if(base==null || base.isEmpty()){
return url;
}
if(url==null || url.isEmpty()){
return base;
}
//if(url.startsWith(base)) return url;
if(Handy.indexOf(url,base,0)==0) return url;
StringBuilder ret=new StringBuilder();
ret.append(base);
if(!base.endsWith("/") && !url.startsWith("/")) ret.append("/");
if(base.endsWith("/") && url.startsWith("/")) ret.setLength(ret.length()-1);
ret.append(url);
return ret.toString();
}
/**
* method will split paths used in linux and windows.
* in particular for windows it checks if a single letter precedes a colon in which case it considers it a volume
* and does not split there.
* @param _paths paths joined with colon or semi-colon
* @return array of paths
*/
public static String[] splitPaths(String _paths){
String[] paths=_paths.replaceAll("(;|:|^)([a-zA-Z]):","$1$2##").split("[:;]");
for (int i = 0; i < paths.length; i++) {
String path=paths[i];
path = path.replace("##",":");
path=path.replace("/./","/");
path=path.replace("//","/");
path=path.replace("\\.\\","\\");
path=path.replace("\\\\","\\");
paths[i]=path;
}
return paths;
}
/**
* Returns a list of key,value pairs in the order they occur in the string str.
* @param str
*/
public static String[] splitProperties(String str) {
if(str.startsWith("?")) str=str.substring(1);
if(str.startsWith(";")) str=str.substring(1);
return str.split("&");
}
public static String[] splitKeyValue(String str) {
String[] t=Handy.split("=",str,1);
if(t==null || t.length==0) return null;
t[0]=Handy.trim(t[0],"'\"");
try {
t[1]=URLDecoder.decode(t[1],"UTF-8");
t[1]=Handy.trim(t[1],"'\"");
return t;
} catch (Exception e) {
if(t.length<2){
return new String[]{t[0],null};
}else{
return t;
}
}
}
public static String[] split(String str) {
return Handy.split("/",str);
}
}
@@ -1,217 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.util;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.HttpURLConnection;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/** Static utility with helper methods to read or write resources.
* The place where we host a global search path often used by others
* such as Template or FileServe (unless overriden.)
*/
public class Resources {
public static interface PathRewrite{
public String rewritePath(String path,Object context);
}
public static Object[] search_path;
/** appends one+ paths to search at position pos.
* neg pos substracts from end
*/
public static Object[] appendSearch(int pos,Object ...src){
//search_history.clear();
if(search_path==null) search_path=new Object[0];
if(pos<0) pos=search_path.length+pos+1;
if(pos<0 || pos>search_path.length) throw new IndexOutOfBoundsException("at:"+pos);
Object[] new_path=new Object[search_path.length+src.length];
// first left side of old search path
if(pos>0){
System.arraycopy(search_path, 0, new_path, 0, pos);
}
// next new sources
if(src.length>0){
System.arraycopy(src, 0, new_path, pos, src.length);
}
// lastly right side of old search path
System.arraycopy(search_path,pos, new_path,pos+src.length, search_path.length-pos);
search_path=new_path;
return search_path;
}
/** returns first good URL for path over sp or search_path.
* Along the way it optionally rewrites path to adjust to search path context.
* When possible it records lastModified timestamp in search_history for later lookup.
* @param remap rewrite rule if any
* @param path path to locate
* @param sp search path reverts to search_path if not specified
* @return URL that can be read.
*/
public static URL findFirst(PathRewrite remap,String path,Object ... sp){
String path0=path;
if(sp==null || sp.length==0) sp=search_path;
for(Object base:sp){
if(remap!=null) path=remap.rewritePath(path0,base);
if(base instanceof Class){
URL ret=((Class<?>)base).getResource(path);
return ret;
}else if(base instanceof String){
File ff=new File(base.toString(),path);
if(ff.exists()){
try {
URL ret=ff.toURI().toURL();
//search_history.put(path0,ff.lastModified());
return ret;
} catch (MalformedURLException e) {
continue;
}
}
}else if(base instanceof File){
File ff=new File((File)base,path);
if(ff.exists()){
try {
URL ret=ff.toURI().toURL();
//search_history.put(path,ff.lastModified());
return ret;
} catch (MalformedURLException e) {
continue;
}
}
}else if(base instanceof URL){
try {
URL ret=new URL((URL)base,path);
String proto=ret.getProtocol();
if(proto.equals("http") || proto.equals("https")){
HttpURLConnection huc = null;
try{
huc=(HttpURLConnection) ret.openConnection();
huc.setRequestMethod("HEAD");
int responseCode = huc.getResponseCode();
if(responseCode==HttpURLConnection.HTTP_OK){
//search_history.put(path,huc.getLastModified());
return ret;
}
}finally{
if(huc!=null) huc.disconnect();
}
}
if(proto.startsWith("jar")){
JarURLConnection juc = null;
juc=(JarURLConnection) ret.openConnection();
if(juc.getJarEntry()!=null){
//search_history.put(path,juc.getLastModified());
return ret;
}
}
if(proto.equals("file")){
File f=new File(ret.getPath());
if(f.exists()){
//search_history.put(path,f.lastModified());
return ret;
}
}
} catch (MalformedURLException e) {
continue;
} catch (IOException e2) {
continue;
}
}
}
return null;
}
/** if recorded in previous searches returns time modified. */
// public static Long lastModified(String path){
// Long ret=search_history.get(path);
// return ret;
// }
public static String toString(URL url) throws IOException{
return toString(url,StandardCharsets.UTF_8);
}
public static String toString(URL url,Charset chs) throws IOException{
try(InputStream is=url.openStream()){
return readChars(is,chs).toString();
}
}
public static byte[] toBytes(URL url) throws IOException{
try(InputStream is=url.openStream()){
return readBytes(is);
}
}
public static long copy(InputStream input, OutputStream output, byte[] buffer) throws IOException {
long count = 0;
int n = 0;
while (-1 != (n = input.read(buffer))) {
output.write(buffer, 0, n);
count += n;
}
return count;
}
public static long copy(Reader input, Writer output, char[] buffer) throws IOException {
long count = 0;
int n = 0;
while (-1 != (n = input.read(buffer))) {
output.write(buffer, 0, n);
count += n;
}
return count;
}
/** Reads a stream in one pass and returns bytes.
* Uses internally Handy.copy and a 4K buffer.
*/
public static final byte[] readBytes(InputStream str) throws IOException{
ByteArrayOutputStream bout=new ByteArrayOutputStream();
Resources.copy(str, bout, new byte[4096]);
return bout.toByteArray();
}
public static final CharSequence readChars(InputStream str) throws IOException{
return readChars(str,StandardCharsets.UTF_8);
}
public static final CharSequence readChars(InputStream str,Charset chset) throws IOException{
BufferedReader rdr=new BufferedReader(new InputStreamReader(str,chset));
StringBuilder ret=new StringBuilder();
for(String line=rdr.readLine();line!=null;line=rdr.readLine()){
ret.append(line).append("\n");
}
return ret;
}
public static CharSequence readChars(Class<?> cls,String name){
InputStream io=cls.getResourceAsStream(name);
try{
return readChars(io);
}catch(Exception e){
return null;
}finally{
if(io!=null) try{io.close();}catch(Exception e){}
}
}
public static void writeChars(CharSequence seq,OutputStream out,Charset chset) throws IOException{
OutputStreamWriter dout=new OutputStreamWriter(out,chset);
dout.append(seq);
dout.flush();
}
public static void writeChars(CharSequence seq,OutputStream out) throws IOException{
writeChars(seq,out,StandardCharsets.UTF_8);
}
public static void writeBytes(int offset,int len,byte[] seq,OutputStream out) throws IOException{
out.write(seq,offset, len);
}
}
@@ -1,131 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.util;
import java.util.HashMap;
/** Utility class to handle error codes and error messages.
* Error codes are integers that describe outcome of an operation.
*
* In cases when return codes are used and not exceptions thrown it is a mess to keep track of what they mean.
* With this class we define a uniform way of managing return codes and allow for additional information.
* This additional information can be text that could be localized and give better user information about what happened.
*
* First we distinguish between success and failure. Any code that is negative is failure.
* Default success code is 0 and provides no additional info. Any positive code is a warning or info and possibly carries extra meaning and
* or description.
*
* success,failure,pending
* source
* parametric
*/
public class ResultCode {
public static final byte TYPE_SUCCESS=0x0;
public static final byte TYPE_PENDING=0x1;
public static final byte TYPE_FAILURE=0xF;
final int code;
final String message;
final String source;
public ResultCode(byte type,short value,String src,String message) {
this.message=message;
this.source=src;
this.code=ResultCode.getCode(type,value,source!=null?source.hashCode():0);
}
public int getCode() {
return code;
}
public byte getType() {
return getType(code);
}
public int getValue() {
return getValue(code);
}
public String getSource() {
return source;
}
public String getMessage() {
return message;
}
@Override
public String toString() {
int code=getCode();
String context=getSource();
String message=getMessage();
if(context!=null){
return context+"("+String.format("%08X", code)+"):"+message;
}else{
return "("+String.format("%08X", code)+"):"+message;
}
}
protected static final HashMap<Integer,ResultCode> codes=new HashMap<>();
public static final int getCode(byte type,int value,int source){
int st=(type <<28) & 0xF0000000;
int sc=(source <<8) & 0x0FFFFF00;
int vl=(value) & 0x000000FF;
return (int) (st | sc | vl);
}
public static final byte getType(int code){
return (byte)((code>>28) & 0x0F);
}
public static final boolean testType(int code,byte st){
return getType(code)==st;
}
public static final boolean isSuccess(int code){
return testType(code,TYPE_SUCCESS);
}
public static final boolean isFailure(int code,int st){
return testType(code,TYPE_FAILURE);
}
public static final boolean isPending(int code,int st){
return testType(code,TYPE_PENDING);
}
public static final int getValue(int code){
return (int)(code & 0x000000FF);
}
public static final int getSource(int code){
return (int)((code & 0x0FFFFF00)>>8);
}
public static final synchronized ResultCode get(int code){
if(codes==null) return null;
return (ResultCode)codes.get(code);
}
public static final synchronized ResultCode put(ResultCode c){
ResultCode old=(ResultCode) codes.get(c.getCode());
codes.put(c.getCode(),c);
return old;
}
public static final int define(byte type,int value,Class<?> source,String message){
return define(type,value,source!=null?source.getSimpleName():null,message);
}
public static final int define(byte type,int value,String source,String message){
int code=getCode(type,value,source!=null?source.hashCode():0);
ResultCode c=get(code);
if(c!=null){
System.err.println("Result code redefinition(consider different value or source):"+c);
return code;
}
c=new ResultCode(type, (short) value,source,message);
put(c);
return code;
}
public static final int defineSuccess(int value,Class<?> source,String message){
return define(TYPE_SUCCESS,value,source,message);
}
public static final int defineFailure(int value,Class<?> source,String message){
return define(TYPE_FAILURE,value,source,message);
}
public static final int definePending(int value,Class<?> source,String message){
return define(TYPE_PENDING,value,source,message);
}
public static final int SUCCESS=ResultCode.defineSuccess(0,null,"Success");
public static final int FAILURE=ResultCode.defineFailure(0,null,"Failure");
public static final int PENDING=ResultCode.definePending(0,null,"Pending");
}
@@ -1,267 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.util;
import java.util.ArrayList;
import java.util.Iterator;
/** A utility to help us tokenize text along delimChars.
* This class is a little better than the java version because it allows for escaped delimChars.
* Delimiters are escaped with a slash, also single and double quotes supress delimiting when encountered.
* @author amer
*/
public class Tokenizer implements Iterable<String>,Iterator<String>{
public static final String WHITECHARS=" \t\r\f\n";
public static final String DELIMCHARS=" ,:;=<>{}[]()";
int offset;
CharSequence input;
String delimChars=DELIMCHARS;
String escapeChars="'\"";
String whiteChars=WHITECHARS;
public Tokenizer(CharSequence input){
this.input=input;
}
public Tokenizer(CharSequence input,int offset){
this.input=input;
this.offset=offset;
}
public CharSequence getInput() {
return input;
}
public int getOffset() {
return offset;
}
public Tokenizer setOffset(int offset) {
this.offset = offset;
return this;
}
public Tokenizer setInput(CharSequence input) {
this.input = input;
return this;
}
public boolean hasMoreTokens(){
if(offset>=input.length()) return false;
for(int i=offset;i<input.length();i++){
char ch=input.charAt(i);
if(Tokenizer.isElementOf(ch,whiteChars)==-1) return true;
}
return false;
}
public String nextToken(){
final StringBuilder out=new StringBuilder();
if(nextToken(out)){
return out.toString();
}else{
return null;
}
}
public boolean nextToken(StringBuilder out){
String[] sets={delimChars,escapeChars,whiteChars};
int noffset=nextToken(offset,input,out,sets);
if(noffset==offset){
return false;
}else{
offset=noffset;
return true;
}
}
public String getDelimChars() {
return delimChars;
}
public Tokenizer setDelimChars(String delimChars) {
this.delimChars = delimChars;
return this;
}
public String getEscapeChars() {
return escapeChars;
}
public Tokenizer setEscapeChars(String escapeChars) {
this.escapeChars = escapeChars;
return this;
}
public String getWhiteChars() {
return whiteChars;
}
public Tokenizer setWhiteChars(String whiteChars) {
this.whiteChars = whiteChars;
return this;
}
/**
* Utility method which collects all tokens and returns an array of them.
* Use it for small length strings when parsing user input.
* @param withdelims if tru returns delimiters as well
*/
public String[] getTokens(boolean withdelims){
final ArrayList<String> buf=new ArrayList<String>();
final StringBuilder out=new StringBuilder();
boolean lastSkipped=false;
while(this.nextToken(out)){
String tok=out.toString();
out.setLength(0);
if(!withdelims && tok.length()==1 && isElementOf(tok.charAt(0),delimChars)!=-1){
if(lastSkipped) buf.add("");
lastSkipped=true;
continue;
}
buf.add(tok);
lastSkipped=false;
}
return buf.toArray(new String[buf.size()]);
}
@Override
public Iterator<String> iterator() {
return this;
}
@Override
public boolean hasNext() {
return this.hasMoreTokens();
}
@Override
public String next() {
return this.nextToken();
}
public static int isElementOf(char ch,String d){
if(d==null) return -1;
return d.indexOf(ch);
}
/**Returns the next token and updated offset.
* This is an inline tokenizer for text parsing and the workhorse of the class.
* It stops when it encounters a delimiter. It treats delimChars as tokens too.
* It advances the offset whenever it was able to move be it delimiter or not.
* We should not have to adjust it for repeated calls except for special cases.
* @param offset
* @param sets various char sets 0-delimiters,1-escape chars,3-white chars
* @param input input chars
* @param out value of the token
* @return offset after processing
*/
public static int nextToken(int offset,CharSequence input, StringBuilder out,String[] sets){
String delimChars=(sets!=null && sets.length>=1)?sets[0]:",:;=<>{}[]()";
String escapeChars=(sets!=null && sets.length>=2)?sets[1]:null;
String whiteChars=(sets!=null && sets.length>=3)?sets[2]:null;
int escChar=-1; // if not -1 then we are escaping
char lastChar=0;
char curChar=0;
int lastOffset=offset;
int isWhiteChar=-1;
int isDelimChar=-1;
boolean weakEscape=false;
int controlCount=0; // counts number of \\ to prevent shortcuit on even number
while(offset<input.length()){ // only scan until the end
lastChar=curChar;
curChar=input.charAt(offset++); // from here on offset is ahead
isWhiteChar=isElementOf(curChar,whiteChars);
isDelimChar=isElementOf(curChar,delimChars);
// determine if we should ignore testing for exit
int isEscapeChar=isElementOf(curChar,escapeChars);
controlCount=(lastChar=='\\')?controlCount+1:0; // control count counts number of \\ to
if((controlCount%2)==1){
isDelimChar=isEscapeChar=-1; // shortcircuit delimiting or escaping if prev char was \\ but only unevent number of times
}
if(escChar==-1){ // should we enter escaping
if(isEscapeChar!=-1){
// will enter escChar but only once
escChar=isEscapeChar;
}
}else{ // should we exit escaping
if(weakEscape==false) isDelimChar=-1; // shortcircuit delim signal if in escape mode and not weak
// exit back to normal if escape found second time
if(isEscapeChar==escChar){
// special rule:if oldchar==curchar and next is not delim or whitespace we ignore escape char
boolean isletter=offset<input.length() && !(isElementOf(input.charAt(offset),delimChars)!=-1 || isElementOf(input.charAt(offset),whiteChars)!=-1);
if(lastChar==curChar && isletter){
// we are special enter weak escaping (where delimiter is not ignored)
// this will correct spurios double quotes but will recover forgotter delimiters between two parts
// we stay in escape mode but listen for delims
weakEscape=true;
}else{
escChar=-1; // we are exiting escaping
}
}
}
// emit chars and test for exit
if(escChar>=0 || isWhiteChar==-1 || isDelimChar!=-1){
// emit if escaping or if delimiter or not white char
out.append(curChar);
}
if(isDelimChar!=-1) break; // exit delimiter found
}
if(isDelimChar!=-1 && lastOffset<(offset-1)){
// fix end of out to not have a delimiter if it has any other string
offset-=1;out.setLength(out.length()-1);
}
return offset;
}
/**Returns the next token and updated offset.
* An improved inline tokenizer using various rules to control delimiting, escaping and text swallowing.
* We supply an array of events or if none is provided a default delimiter event is constructed.
* After that the events are used to control tokenization. We enter a loop and feed the input to
* the events if one or more are armed or triggered (state >=0) we defer emiting chars to output until we determine what to do.
* For events that do escape we just defer until end of escape is detected, for delimit we return back and
* for supress we just swallow the input without emitting it.
*/
/*
public static int nextToken(TokenizerRule state,int offset,CharSequence input, StringBuilder out){
int emitCount=0;
int oldOffset=offset;
while(offset<input.length()){
int st=state.consume(offset, input);
st=(st==TokenizerRule.DO_DEFER && offset==(input.length()))?TokenizerRule.DO_EMIT:st;
switch(st){
case TokenizerRule.DO_EMIT:
// we can emit what we got so far
if(oldOffset<=offset){
offset++;
out.append(input,oldOffset,offset);
emitCount+=(offset-oldOffset);
oldOffset=offset;
}
break;
case TokenizerRule.DO_DEFER:
// we need to defer emitting
offset++;
break;
case TokenizerRule.DO_SKIP:
// we just skip over this input
offset++;
oldOffset=offset;
break;
case TokenizerRule.DO_EXITBEFORE:
if(emitCount>0){
offset-=(state.getSize()-1);
}
case TokenizerRule.DO_EXITAFTER:
if(emitCount==0 && oldOffset<=offset){
// if there is anything left
offset++;
out.append(input,oldOffset,offset);
emitCount+=(offset-oldOffset);
oldOffset=offset;
}
state.clear();
default:
return st>=0?st:offset;
}
}
return offset;
}
*/
}
@@ -1,137 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.dbo;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Date;
import com.reliancy.rec.JSON;
import org.junit.BeforeClass;
import org.junit.Test;
public class TerminalTest {
@Entity.Info(
name="dbo.Maps"
)
public static class Maps extends DBO{
public static Field map_id=Field.Int("Map_id").setPk(true);
public static Field map_name=Field.Str("Map_name");
public static Field created=Field.DateTime("Created");
public static Field active=Field.Bool("Active");
static{
//Entity.publish(Maps.class);
}
}
@Entity.Info(
name="public.securable"
)
public static class Securable extends DBO{
public static Field id=Field.Int("id").setPk(true).setAutoIncrement(true);
public static Field kind=Field.Str("kind");
public static Field name=Field.Str("name");
public static Field display_name=Field.Str("display_name");
public static Field created=Field.DateTime("created_on");
public static Field is_essential=Field.Bool("is_essential");
static{
//Entity.publish(Maps.class);
}
}
@Entity.Info(
name="public.product"
)
public static class Product extends Securable{
public static Field valid_since=Field.DateTime("valid_since");
public static Field valid_until=Field.DateTime("valid_until");
public static Field short_info=Field.Str("short_info");
}
static SQLTerminal t;
@BeforeClass
public static void beforeAllTestMethods() {
System.out.println("Invoked once before all test methods");
String url=System.getenv("DB_URL");
System.out.println("DB URL:"+url);
t=new SQLTerminal(url);
}
/**
* jdbc connectivity
* @throws IOException
* @throws SQLException
*/
@Test
public void connection() throws IOException, SQLException{
try(Connection c=t.getConnection()){
System.out.println("Connection:"+c);
try (Statement stmt = c.createStatement()) {
// use stmt here
String sql = "SELECT * from \"dbo\".\"Maps\"";
try (ResultSet resultSet = stmt.executeQuery(sql)) {
// use resultSet here
while (resultSet.next()) {
System.out.println("ROw:"+resultSet.getInt("Map_id")+":"+resultSet.getString("Map_name"));
}
}
}
}
}
@Test
public void simpleCRUD() throws IOException, SQLException{
System.out.println("SimpleCRUD");
try(Action act=t.begin().load(Maps.class).execute()){
for(DBO o:act){
System.out.println("DBO:"+o);
}
}
Entity.retract(Maps.class);
}
@Test
public void complexCRUD() throws IOException, SQLException{
System.out.println("ComplexCRUD");
// Reading
try(Action act=t.begin().load(Product.class).execute()){
for(DBO o:act){
System.out.println("DBO:"+o);
}
}
//Saving
Product p=new Product();
p.setStatus(DBO.Status.USED);
Product.id.set(p,35);
Product.kind.set(p,Product.class.getSimpleName());
Product.name.set(p,"myproduct");
Product.created.set(p,new Date());
Product.short_info.set(p,"a sweet melody:"+new java.sql.Timestamp(System.currentTimeMillis()));
Product.display_name.set(p,"first entry");
System.out.println("Update P0:"+JSON.toString(p));
t.save(p);
System.out.println("Update P1:"+JSON.toString(p));
// Creating
Product pp=new Product();
Product.kind.set(pp,Product.class.getSimpleName());
Product.name.set(pp,"myproduct");
Product.created.set(pp,new Date());
Product.short_info.set(pp,"a sweet melody:");
Product.display_name.set(pp,"created entry:"+new java.sql.Timestamp(System.currentTimeMillis()));
t.save(pp);
System.out.println("Create PP0:"+JSON.toString(pp));
pp=t.load(Product.class, Product.id.get(pp,null));
System.out.println("Returning:"+pp);
// Deleting
t.delete(pp);
//Entity.retract(Maps.class);
}
}
@@ -8,9 +8,12 @@ You may not use this file except in compliance with the License.
package com.reliancy.jabba;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.After;
import org.junit.Before;
@@ -69,8 +72,37 @@ public class JettyAppTest {
return "no arg response";
}
}
public static class SpaTestApp extends JettyApp {
private final String staticRoot;
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"), "<html><body>spa shell</body></html>");
Files.writeString(spaRoot.resolve("app.js"), "console.log('spa');");
app = new SpaTestApp(spaRoot.toString());
ArgsConfig config = new ArgsConfig();
Config.SERVER_PORT.set(config, testPort);
config.load();
app.begin(config);
int attempts = 0;
while(!app.isStarted() && attempts < 20){
Thread.sleep(100);
attempts++;
}
Thread.sleep(200);
assertTrue("Root path should serve index file", readBody(httpRequest("GET", "/", "text/html")).contains("spa shell"));
assertTrue("Nested frontend route should fallback to index file", readBody(httpRequest("GET", "/dashboard/settings", "text/html")).contains("spa shell"));
assertEquals("API route should still reach routed endpoint", "api pong", httpGet("/api/ping"));
assertTrue("Direct asset request should serve asset file", httpGet("/app.js").contains("console.log('spa');"));
HttpURLConnection headRoot = httpRequest("HEAD", "/", "text/html");
assertEquals("HEAD should succeed on root file", HttpURLConnection.HTTP_OK, headRoot.getResponseCode());
assertEquals("HEAD should report HTML content type", "text/html", headRoot.getContentType());
assertNotNull("HEAD should expose ETag", headRoot.getHeaderField("ETag"));
assertNotNull("HEAD should expose Last-Modified", headRoot.getHeaderField("Last-Modified"));
assertEquals("HEAD should set nosniff", "nosniff", headRoot.getHeaderField("X-Content-Type-Options"));
assertEquals("Index should default to no-cache", "no-cache", headRoot.getHeaderField("Cache-Control"));
assertTrue("HEAD should report content length", headRoot.getContentLengthLong() > 0);
HttpURLConnection assetGet = httpRequest("GET", "/app.js", "*/*");
assertEquals("Asset should be served successfully", HttpURLConnection.HTTP_OK, assetGet.getResponseCode());
assertEquals("Asset should use asset cache policy", "public, max-age=3600", assetGet.getHeaderField("Cache-Control"));
assertNotNull("Asset should expose Last-Modified", assetGet.getHeaderField("Last-Modified"));
HttpURLConnection apiMiss = httpRequest("GET", "/api/missing", "application/json");
assertEquals("API-style missing route should stay 404 instead of SPA fallback", HttpURLConnection.HTTP_NOT_FOUND, apiMiss.getResponseCode());
HttpURLConnection traversal = httpRequest("GET", "/%2E%2E/secret.txt", "text/html");
assertEquals("Traversal attempts should be rejected", HttpURLConnection.HTTP_BAD_REQUEST, traversal.getResponseCode());
}
@Test
public void testFileServerCachesSmallAssetsInMemory() throws Exception {
Path cacheRoot = Files.createTempDirectory("jabba-cache");
try {
Files.writeString(cacheRoot.resolve("small.js"), "console.log('small');");
StringBuilder large = new StringBuilder();
for(int i=0;i<400;i++){
large.append("0123456789abcdef");
}
Files.writeString(cacheRoot.resolve("large.js"), large.toString());
FileServer server = new FileServer("/", "", cacheRoot.toString())
.setMemoryCacheContentLimit(256);
FileServer.Bucket bucket = server.getBucket("/");
FileServer.CachedAsset small = bucket.getAsset("small.js", server);
assertNotNull("Small asset should resolve", small);
assertNotNull("Small asset should keep bytes in memory", small.content);
assertTrue("Small asset length should be tracked", small.contentLength > 0);
FileServer.CachedAsset largeAsset = bucket.getAsset("large.js", server);
assertNotNull("Large asset should resolve", largeAsset);
assertNull("Large asset should not keep bytes in memory", largeAsset.content);
assertTrue("Large asset length should be tracked", largeAsset.contentLength > 256);
} finally {
Files.walk(cacheRoot)
.sorted((a,b) -> b.compareTo(a))
.forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
}
});
}
}
}
@@ -1,40 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.rec;
import java.io.IOException;
import org.junit.Test;
public class ObjTest {
/**
* Plain CRUD
* @throws IOException
*/
@Test
public void crudVec() throws IOException
{
Obj o=new Obj();
Obj a=new Obj(true);
System.out.println("O1:"+o);
System.out.println("A1:"+a);
a.add(1).add("three");
o.add(1).add("three").set(new Slot("arr"),new String[]{"a","b","c"});
System.out.println("O2meta:"+o.isArray()+"/"+o.meta());
System.out.println("O2:"+o);
System.out.println("A2:"+a);
o.set(o.getSlot("car"),"bar");
System.out.println("O3:"+o);
StringBuilder json=new StringBuilder();
JSON.writes(o,json);
System.out.println("ENC:"+json);
Rec dec=JSON.reads(json);
System.out.println("DEC:"+dec);
}
}
@@ -1,40 +0,0 @@
/*
Copyright (c) 2011-2022 Reliancy LLC
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
You may not use this file except in compliance with the License.
*/
package com.reliancy.util;
import java.io.IOException;
import org.junit.Test;
public class HandyTest {
/**
* Plain CRUD
* @throws IOException
*/
@Test
public void splitting() throws IOException
{
System.out.println("Splitting test...");
String tst1="One,Two,Three";
System.out.println(tst1+" over ,");
for(String s:Handy.split(",",tst1)){
System.out.println("\tt:"+s);
}
//System.out.println(tst1+" over ");
//for(String s:Handy.split("",tst1)){
// System.out.println("\tt:"+s);
//}
String tst2="AND A AND B ANDAND D AND";
System.out.println(tst2+" over AND");
for(String s:Handy.split("AND",tst2)){
System.out.println("\tt:"+s);
}
}
}