diff --git a/.classpath b/.classpath index 89f941d..dafc7bb 100644 --- a/.classpath +++ b/.classpath @@ -1,11 +1,5 @@ - - - - - - @@ -19,6 +13,12 @@ + + + + + + diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs index 61bafe0..7b8f972 100644 --- a/.settings/org.eclipse.buildship.core.prefs +++ b/.settings/org.eclipse.buildship.core.prefs @@ -1,13 +1,13 @@ -arguments=--init-script C\:\\Users\\agov0001\\AppData\\Roaming\\Code\\User\\globalStorage\\redhat.java\\1.22.1\\config_win\\org.eclipse.osgi\\57\\0\\.cp\\gradle\\init\\init.gradle --init-script C\:\\Users\\agov0001\\AppData\\Roaming\\Code\\User\\globalStorage\\redhat.java\\1.22.1\\config_win\\org.eclipse.osgi\\57\\0\\.cp\\gradle\\protobuf\\init.gradle +arguments= auto.sync=false build.scans.enabled=false -connection.gradle.distribution=GRADLE_DISTRIBUTION(LOCAL_INSTALLATION(C\:\\ProgramData\\chocolatey\\lib\\gradle\\tools\\gradle-7.5.1)) +connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) connection.project.dir= eclipse.preferences.version=1 gradle.user.home= -java.home=C\:/Program Files/Microsoft/jdk-11.0.12.7-hotspot +java.home= jvm.arguments= offline.mode=false -override.workspace.settings=true -show.console.view=true -show.executions.view=true +override.workspace.settings=false +show.console.view=false +show.executions.view=false diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index ab26a74..e51d796 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -10,7 +10,7 @@ org.eclipse.jdt.core.circularClasspath=warning org.eclipse.jdt.core.classpath.exclusionPatterns=enabled org.eclipse.jdt.core.classpath.mainOnlyProjectHasTestOnlyDependency=error org.eclipse.jdt.core.classpath.multipleOutputLocations=enabled -org.eclipse.jdt.core.classpath.outputOverlappingAnotherSource=error +org.eclipse.jdt.core.classpath.outputOverlappingAnotherSource=ignore org.eclipse.jdt.core.codeComplete.argumentPrefixes= org.eclipse.jdt.core.codeComplete.argumentSuffixes= org.eclipse.jdt.core.codeComplete.camelCaseMatch=enabled diff --git a/build.gradle b/build.gradle index d4d89ee..7144246 100644 --- a/build.gradle +++ b/build.gradle @@ -4,26 +4,50 @@ Unix - ~/.m2 Windows - C:\Users\\.m2 For example - /Users/alex/.m2/repository///. */ - apply plugin: 'java' apply plugin: 'maven-publish' apply plugin: 'application' apply plugin: 'eclipse' apply from: 'extra.gradle' -group='com.reliancy' -mainClassName = group+'.'+name+'.JettyApp' -version = '0.2-SNAPSHOT' -sourceCompatibility = 1.8 -targetCompatibility = 1.8 project.buildDir = 'target' +group='com.reliancy' +version = '0.3-SNAPSHOT' +application{ + mainClass=(group+'.'+name+'.JettyApp') +} +java{ + sourceCompatibility = 1.8 + targetCompatibility = 1.8 +} // print some info for orientation -println("group:"+group); -println("name:"+name); -println("version:"+version); -println("entry:"+mainClassName); +//println("group:"+group); +//println("name:"+name); +//println("version:"+version); +//println("entry:"+mainClassName); +sourceSets { + main { + resources { + srcDirs "src/main/resources", "src/main/web" + } + } +} +processResources { + from (sourceSets.main.java.srcDirs) { + include '**/*.htm' + include '**/*.html' + include '**/*.properties' + include '**/*.ini' + include '**/*.json' + include '**/*.js' + include '**/*.css' + include '**/*.txt' + include '**/*.xml' + include '**/*.png' + } +} repositories { //mavenLocal() //mavenCentral() @@ -75,7 +99,8 @@ javadoc { } dependencies { implementation "org.eclipse.jetty:jetty-server:11.0.1" - implementation "org.slf4j:slf4j-simple:2.0.0-alpha0" + implementation "org.slf4j:slf4j-jdk14:2.0.10" + //implementation "org.slf4j:slf4j-simple:2.0.10" //implementation 'com.hubspot.jinjava:jinjava:2.5.10' implementation 'com.github.jknack:handlebars:4.3.0' implementation 'com.h2database:h2:2.1.214' @@ -92,9 +117,10 @@ test { // standard out or error is shown // in Gradle output. outputs.upToDateWhen {false} - showStandardStreams = true + //showStandardStreams = true exceptionFormat = 'full' // Or we use events method: + events "passed", "skipped", "failed", "standardOut", "standardError" // events 'standard_out', 'standard_error' // Or set property events: @@ -110,7 +136,7 @@ jar { archiveBaseName = project.name archiveVersion = project.version manifest { - attributes "Main-Class": mainClassName + attributes "Main-Class": application.mainClass attributes "Class-Path": configurations.runtimeClasspath.collect { it.getName() }.join(' ') } } @@ -121,11 +147,11 @@ task copyToLib(type: Copy) { build.finalizedBy(copyToLib) eclipse{ classpath { - defaultOutputDir = file("target/bin") ///default + defaultOutputDir = file("${relativePath(buildDir)}/bin") ///default file.whenMerged { cp -> cp.entries.forEach { cpe -> if (cpe.kind == 'src' && cpe.hasProperty('output')) { - cpe.output = cpe.output.replace('bin/', "target/classes/java/") + cpe.output = cpe.output.replace('bin/', "${relativePath(buildDir)}/classes/java/") } } } diff --git a/extra.gradle b/extra.gradle index 460b25f..8a34b0c 100644 --- a/extra.gradle +++ b/extra.gradle @@ -12,11 +12,11 @@ task dotenv{ } task packageJavadoc(type: Jar, dependsOn: 'javadoc') { from javadoc - classifier = 'javadoc' + archiveClassifier = 'javadoc' } task packageSources(type: Jar, dependsOn: 'classes') { from sourceSets.main.allSource - classifier = 'sources' + archiveClassifier = 'sources' } task fat_jar(type: Jar) { archiveBaseName = 'fat-'+project.name @@ -92,7 +92,7 @@ class Server implements Runnable{ info("stopping server"); if(driver!=null){ driver.interrupt(); - driver.join(); + //driver.join(); } for(Thread th:Thread.getAllStackTraces().keySet()){ if(th.getName().equalsIgnoreCase("executor")){ @@ -116,8 +116,8 @@ task runServer{ doLast { Server.main().start({ project.javaexec { - classpath = project.sourceSets.main.runtimeClasspath - main = mainClassName + classpath = sourceSets.main.runtimeClasspath + main = application.mainClass.get() } }); } diff --git a/src/main/java/com/reliancy/dbo/Action.java b/src/main/java/com/reliancy/dbo/Action.java index 5b4e7ab..b63243b 100644 --- a/src/main/java/com/reliancy/dbo/Action.java +++ b/src/main/java/com/reliancy/dbo/Action.java @@ -17,7 +17,7 @@ import java.util.Iterator; * 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 they we done. + * once iterated over and saved we are done. */ public class Action implements Iterable,SiphonIterator{ public static class Trait{ diff --git a/src/main/java/com/reliancy/dbo/SQLTerminal.java b/src/main/java/com/reliancy/dbo/SQLTerminal.java index e978151..f297da8 100644 --- a/src/main/java/com/reliancy/dbo/SQLTerminal.java +++ b/src/main/java/com/reliancy/dbo/SQLTerminal.java @@ -22,6 +22,10 @@ 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; @@ -48,7 +52,7 @@ public class SQLTerminal implements Terminal{ } @Override public Action execute(Action q) throws IOException{ - System.out.println("Executing..."+q.getTrait()); + // System.out.println("Executing..."+q.getTrait()); Action.Trait tr=q.getTrait(); if(tr instanceof Action.Load){ Entity ent=q.getEntity(); @@ -60,14 +64,14 @@ public class SQLTerminal implements Terminal{ reader.close(); throw new IOException(e); } - System.out.println("Executing...Done"); + //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"); + //System.out.println("Executing...Done"); return q; }catch(SQLException e){ throw new IOException(e); @@ -77,7 +81,7 @@ public class SQLTerminal implements Terminal{ try(SQLCleaner cleaner=new SQLCleaner(ent,this)) { cleaner.open(); cleaner.flush(q.getItems()); - System.out.println("Executing...Done"); + //System.out.println("Executing...Done"); return q; }catch(SQLException e){ throw new IOException(e); diff --git a/src/main/java/com/reliancy/dbo/Terminal.java b/src/main/java/com/reliancy/dbo/Terminal.java index 7602777..81f163a 100644 --- a/src/main/java/com/reliancy/dbo/Terminal.java +++ b/src/main/java/com/reliancy/dbo/Terminal.java @@ -14,7 +14,7 @@ import java.io.IOException; * 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 since item actions. + * 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 { diff --git a/src/main/java/com/reliancy/jabba/App.java b/src/main/java/com/reliancy/jabba/App.java index 4063689..4a43f5a 100644 --- a/src/main/java/com/reliancy/jabba/App.java +++ b/src/main/java/com/reliancy/jabba/App.java @@ -8,47 +8,81 @@ You may not use this file except in compliance with the License. package com.reliancy.jabba; +import java.io.File; import java.io.IOException; +import java.util.logging.Logger; +import com.reliancy.dbo.Terminal; import com.reliancy.jabba.sec.SecurityPolicy; import com.reliancy.util.CodeException; import com.reliancy.util.ResultCode; /** Base Application class from where specific launchers derive. * Derived classes will usually bring in jetty or tomcat or some other launch ability. + * At the level of App we manage pure app or infrastructure concepts. + * Examples of such concepts are: + * - processor chain + * - main / router processor + * - security policy + * - storage which pools resources among instances + * - app wide logger and config + * It does not include: + * - per user config + * - per user work directory + * + * Storage is an abstract, possibly external data store. It could be file based or not. + * On the other hand work path is usually local disk path specific to a machine, still + * it can be overriden to return www resource paths for certain items. That is why we treat + * it as a string and not a File or URL. */ public abstract class App extends Processor{ public static int ERR_NOCONFIG=ResultCode.defineFailure(0x01,App.class,"config missing. provide at least empty one."); public static int ERR_NOTCLOSED=ResultCode.defineFailure(0x02,App.class,"unbalanced call. resource called twice:${resource}"); protected Processor first=null; protected Processor last=null; - protected RoutedEndPoint router=null; + protected Router router=null; protected SecurityPolicy policy=null; - + protected Terminal storage=null; + public App(String id) { super(id); } + /** does nothing. */ public void before(Request request,Response response) throws IOException{ } + /** does nothing. */ public void after(Request request,Response response) throws IOException{ } + /** app serves by processing first-last chain then router. + * always conditional on status being null otherwise it skips. + */ public void serve(Request req,Response resp) throws IOException{ - if(first!=null) first.process(req, resp); - if(router!=null) router.process(req,resp); + if(first!=null && resp.getStatus()==null) first.process(req, resp); + if(router!=null && resp.getStatus()==null) router.process(req,resp); } - public T addProcessor(T m){ + /** add one or a chain of processors. */ + public T addMiddleWare(T m){ + if(m==null) return null; if(first==null){ last=first=m; + m.setParent(m); }else{ last.next=m; } - while(last.next!=null) last=last.next; + while(last.next!=null){ + last=last.next; + last.setParent(m); + } return m; } - public void removeProcessor(Processor m){ + public void removeMiddleWare(Processor m){ + if(m==null) return; if(first==m){ - if(first==last) last=null; - first=first.next; + if(first==last){ + first=last=null; + }else{ + first=first.next; + } while(last!=null && last.next!=null) last=last.next; }else{ for(Processor prev=first;prev!=null;prev=prev.next){ @@ -60,6 +94,7 @@ public abstract class App extends Processor{ } } m.next=null; + m.setParent(null); } public Processor getProcessor(String id){ for(Processor c=first;c!=null;c=c.next){ @@ -68,11 +103,26 @@ public abstract class App extends Processor{ return null; } - public RoutedEndPoint getRouter() { + public Router getRouter() { return router; } - public void setRouter(RoutedEndPoint router) { + public void setRouter(Router router) { + if(this.router==router) return; + if(this.router!=null) this.router.setParent(null); this.router = router; + router.setParent(this); + } + public String getWorkPath(String rel_path){ + Config cnf=getConfig(); + String work_dir=Config.APP_WORKDIR.get(cnf); + if(!work_dir.endsWith("/")) work_dir+="/"; + if(rel_path==null) rel_path=""; + if(rel_path.startsWith("/")) rel_path=rel_path.substring(1); + if(rel_path==".") rel_path=""; + rel_path=rel_path.replace("\\","/"); + rel_path=rel_path.replace("/./","/"); + String ret=work_dir+rel_path; + return ret; } public void run(Config conf) throws Exception { try{ @@ -88,34 +138,41 @@ public abstract class App extends Processor{ if(conf==null) throw new CodeException(ERR_NOCONFIG); config=conf; for(Processor p=first;p!=null;p=p.getNext()){ - p.begin(config); + p.begin(); } if(router!=null) router.begin(config); } @Override public void end() throws Exception{ - if(router!=null) router.end(); - for(Processor p=first;p!=null;p=p.getNext()){ - p.end(); + try{ + if(router!=null) router.end(); + for(Processor p=first;p!=null;p=p.getNext()){ + p.end(); + } + log().info("stopped:"+getId()); + super.end(); // detaches from config + }finally{ + // we notify all of end (especially cleaner thread) + synchronized(this){ + this.notifyAll(); + } } - super.end(); - log().info("stopping app:"+getId()); } public AppSessionFilter addAppSession(){ - return addProcessor(new AppSessionFilter(this)); + return addMiddleWare(new AppSessionFilter(this)); } public AppSessionFilter addAppSession(AppSession.Factory f){ - return addProcessor(new AppSessionFilter(this,f)); + return addMiddleWare(new AppSessionFilter(this,f)); } public SecurityPolicy setSecurityPolicy(SecurityPolicy secpol){ if(secpol==policy) return secpol; if(policy!=null){ MethodDecorator.retract(policy); - removeProcessor(policy); + removeMiddleWare(policy); } policy=secpol; if(policy!=null){ - addProcessor(policy); + addMiddleWare(policy); MethodDecorator.publish(policy); // register security policy as decorator factory } return secpol; @@ -123,4 +180,10 @@ public abstract class App extends Processor{ public SecurityPolicy getSecurityPolicy(){ return policy; } + public void setStorage(Terminal db){ + storage=db; + } + public Terminal getStorage(){ + return storage; + } } diff --git a/src/main/java/com/reliancy/jabba/AppModule.java b/src/main/java/com/reliancy/jabba/AppModule.java new file mode 100644 index 0000000..751c68b --- /dev/null +++ b/src/main/java/com/reliancy/jabba/AppModule.java @@ -0,0 +1,9 @@ +package com.reliancy.jabba; +/** Special controller interface which is implemented by modules. + * Modules are classes that publish or retract middleware or endpoints into application. + * They allow us to compose an application of disparate apis. + */ +public interface AppModule { + void publish(App app); + default void retract(App app){}; +} diff --git a/src/main/java/com/reliancy/jabba/AppSession.java b/src/main/java/com/reliancy/jabba/AppSession.java index 92aa5e4..62a4279 100644 --- a/src/main/java/com/reliancy/jabba/AppSession.java +++ b/src/main/java/com/reliancy/jabba/AppSession.java @@ -11,6 +11,9 @@ import java.util.HashMap; import com.reliancy.jabba.sec.SecurityActor; import com.reliancy.jabba.ui.Feedback; +/** AppSession is recovered early on and holds per-user app instance data. + * Unless it is owned by user db terminals should not be held here. + */ public class AppSession implements Session{ public static interface Factory{ diff --git a/src/main/java/com/reliancy/jabba/ArgsConfig.java b/src/main/java/com/reliancy/jabba/ArgsConfig.java new file mode 100644 index 0000000..013629e --- /dev/null +++ b/src/main/java/com/reliancy/jabba/ArgsConfig.java @@ -0,0 +1,191 @@ +package com.reliancy.jabba; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import com.reliancy.util.Handy; +import com.reliancy.util.Log; + +/** + * ArgsConfig takes in command line arguments and parses them on load. + * If also deals with environmental variables which are returned if not present. + * Note that in java we have System properties which are platform independent + * then we have environment variables which are not unless fed by us directly. + */ +public class ArgsConfig extends Config.Base{ + final String[] args; + final ArrayList> schema=new ArrayList<>(); + String id; + public ArgsConfig(String... args){ + this.args=args; + this.id=""; + } + @Override + public Config load() throws IOException { + if(props.isEmpty()==false) return this; // not gona load again if loaded + final Map> params = new HashMap<>(); + List values=null; + ArrayList positional=new ArrayList<>(); + String app_invoked=""; + for (int i = 0; i < args.length; i++) { + final String a = args[i]; + if (a.charAt(0) == '-') { + if(!positional.isEmpty()){ + // positional filled - we are entering new level + throw new RuntimeException("multi level arguments not supported"); + } + String key=a.substring(1); + boolean brief=!key.startsWith("-"); + if(!brief) key=key.substring(1); + if (key.length() < 1) { + throw new IllegalArgumentException("bad argument -"); + } + values=new ArrayList<>(); + params.put(key,values); + if(key.contains("=")){ + // we got value after equals + String[] toks =key.split("=",1); + key=toks[0]; + String val=toks[1]; + val=Handy.unwrap(val,"\"","\""); + val=Handy.unwrap(val,"'","'"); + values.add(val); + } + }else if (values != null) { + //TODO: if property had a max count we could delay + // this way multiple values require repeated --key val + String val=a; + val=Handy.unwrap(val,"\"","\""); + val=Handy.unwrap(val,"'","'"); + values.add(a); + values=null; + }else { + // these are positional arguments + if(i==0) app_invoked=a; else positional.add(a); + } + } + // now process parsed + id=app_invoked; + this.setProperty(Config.APP_INVOKED,app_invoked); + int lastDelim=Math.max(app_invoked.lastIndexOf('/'),app_invoked.lastIndexOf('\\')); + this.setProperty(Config.APP_NAME,app_invoked.substring(lastDelim+1)); + this.setProperty(Config.APP_ARGS,positional); + //System.out.println("App invoked:"+app_invoked); + //System.out.println("params:"+params); + //System.out.println("pos:"+positional); + for(String pkey:params.keySet()){ + values=params.get(pkey); + if(values.isEmpty()){ + // we got a boolean + Property prop=new Property<>(pkey,Boolean.class); + setProperty(prop,true); + }else if(values.size()==1){ + // we got single value + String val=values.get(0); + if("true".equalsIgnoreCase(val) || "false".equalsIgnoreCase(val)){ + Property prop=new Property<>(pkey,Boolean.class); + Boolean val2=prop.adaptValue(val); + setProperty(prop,val2); + }else if(Handy.isNumeric(val)){ + Property prop=new Property<>(pkey,Float.class); + Float val2=prop.adaptValue(val); + setProperty(prop,val2); + }else{ + Property prop=new Property<>(pkey,String.class); + setProperty(prop,val); + } + }else{ + // we got a list + Property prop=new Property<>(pkey,List.class); + setProperty(prop,values); + } + } + // finally post process such as choosing APP_WORKDIR + String cwd=null; + String[] cwds=new String[]{APP_WORKDIR.get(this),"./var","./data","../var","../data"}; + for(String c:cwds){ + if(c==null) continue; + File f=new File(c); + if(f.exists() && f.isDirectory()){ + cwd=c;break; + } + } + if(cwd!=null){ // we got working dir + cwd=cwd.replace("\\", "/").replace("/./","/"); + if(!cwd.endsWith("/")) cwd+="/"; + APP_WORKDIR.set(this, cwd); + } + // also logging level and format + Logger root=Log.setup(); + Log.setLevel(root,LOG_LEVEL.get(this)); + return this; + } + @Override + public Config save() { + return this; + } + @Override + public String getId() { + return id; + } + @Override + public boolean hasProperty(Property key){ + String BADVAL="___BADVAL___"; + if(props.containsKey(key)){ + return true; + }else if(System.getProperty(key.getName(),BADVAL)!=BADVAL){ + return true; + }else if(System.getenv().containsKey(key.getName())){ + return true; + }else{ + return false; + } + } + @Override + public T getProperty(Property key, T def) { + String BADVAL="___BADVAL___"; + if(props.containsKey(key)){ + return key.getTyp().cast(props.get(key)); + } + String ret=System.getProperty(key.getName(),BADVAL); + if(ret!=BADVAL){ + Object val=Handy.normalize(key.getTyp(),ret); + return key.getTyp().cast(val); + }else if(System.getenv().containsKey(key.getName())){ + Object val=System.getenv(key.getName()); + val=Handy.normalize(key.getTyp(),val); + return key.getTyp().cast(val); + }else{ + return def; + } + } + + @Override + public Config setProperty(Property key, T val) { + props.put(key,val); + return this; + } + @Override + public T delProperty(Property key) { + Object val=props.remove(key); + return key.getTyp().cast(val); + } + @Override + public Iterator> iterator() { + ArrayList> keys=new ArrayList<>(props.keySet()); + return keys.iterator(); + } + @Override + public Config setSchema(Property ...p){ + this.schema.clear(); + for(Property pp:p) schema.add(pp); + return this; + } + +} diff --git a/src/main/java/com/reliancy/jabba/Config.java b/src/main/java/com/reliancy/jabba/Config.java index 51844fa..0d5020e 100644 --- a/src/main/java/com/reliancy/jabba/Config.java +++ b/src/main/java/com/reliancy/jabba/Config.java @@ -6,35 +6,130 @@ You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en You may not use this file except in compliance with the License. */ package com.reliancy.jabba; +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + import org.slf4j.Logger; -public interface Config { +import com.reliancy.util.Handy; + +public interface Config extends Iterable>{ public static class Property { final String name; final Class typ; + V initial; + boolean required; + boolean writable; public Property(String name,Class typ){ this.name=name; this.typ=typ; + required=false; + writable=true; + } + @Override + public String toString(){ + return String.format("%s:%s",name,typ.getSimpleName()); } public String getName(){return name;} public Class getTyp(){return typ;} + public Property setInitial(V val){initial=val;return this;} + public V getInitial(){return initial;} + public Property setRequired(boolean f){required=f;return this;} + public boolean isRequired(){return required;} + public Property setWritable(boolean f){writable=f;return this;} + public boolean isWritable(){return writable;} public V get(Config store,V def){ return store.getProperty(this,def); } public V get(Config store){ - return get(store,null); + return get(store,initial); } public void set(Config store,V val){ store.setProperty(this, val); } + /** converts value such as string to expected type if possible. */ + public V adaptValue(Object val){ + val=Handy.normalize(this.getTyp(),val); + return this.getTyp().cast(val); + } + @Override + public int hashCode(){ + return name.toLowerCase().hashCode(); + } + @Override + public boolean equals(Object other){ + if(other==this) return true; + if(other instanceof Property){ + Property pother=(Property) other; + return name.equalsIgnoreCase(pother.getName()) && typ==pother.getTyp(); + } + return false; + } + } + public static abstract class Base implements Config { + protected final HashMap,Object> props=new HashMap<>(); + protected Object modified; + public boolean isModified(){ + return modified!=null; + } + public Config setModified(Object p){ + if(p==null){ + // reset modified state + modified=null; + }else{ + if(modified==null) modified=new TreeSet(); + if(modified instanceof Collection){ + ((Collection)modified).add(p); + }else{ + // modified is set already but not appendable + } + } + return this; + } + @Override + public Config clear(){ + props.clear(); + modified=null; + return this; + } + @Override + public Config setProperty(Property key, T val) { + setModified(key); + props.put(key,val); + return this; + } + @Override + public T delProperty(Property key) { + setModified(key); + Object val=props.remove(key); + return key.getTyp().cast(val); + } } - public static final Property LOG_LEVEL=new Property("LOG_LEVEL",String.class); - public static final Property LOGGER=new Property("LOGGER",Logger.class); - public void load(); - public void save(); + public static final Property LOG_LEVEL=new Property<>("LOG_LEVEL",String.class); + public static final Property LOGGER=new Property<>("LOGGER",Logger.class); + public static final Property APP_INVOKED=new Property<>("APP_INVOKED",String.class); + public static final Property APP_NAME=new Property<>("APP_NAME",String.class); + public static final Property APP_TITLE=new Property<>("APP_TITLE",String.class); + public static final Property APP_INFO=new Property<>("APP_INFO",String.class); + public static final Property APP_WORKDIR=new Property<>("APP_WORKDIR",String.class); + public static final Property APP_CLASS=new Property<>("APP_CLASS",String.class); + public static final Property APP_ARGS=new Property<>("APP_ARGS",List.class); + + public default Config getParent(){return null;}; + public Config clear(); + public Config load() throws IOException; + public Config save() throws IOException; public String getId(); + public boolean hasProperty(Property key); public Config setProperty(Property key,T val); public T getProperty(Property key,T def); + public T delProperty(Property key); + public Config setSchema(Property ...p); + } diff --git a/src/main/java/com/reliancy/jabba/DemoEP.java b/src/main/java/com/reliancy/jabba/DemoEP.java new file mode 100644 index 0000000..151319e --- /dev/null +++ b/src/main/java/com/reliancy/jabba/DemoEP.java @@ -0,0 +1,118 @@ +package com.reliancy.jabba; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import com.reliancy.jabba.sec.NotAuthentic; +import com.reliancy.jabba.sec.Secured; +import com.reliancy.jabba.sec.SecurityActor; +import com.reliancy.jabba.sec.SecurityPolicy; +import com.reliancy.jabba.ui.Feedback; +import com.reliancy.jabba.ui.FeedbackLine; +import com.reliancy.jabba.ui.Rendering; +import com.reliancy.jabba.ui.Template; + +public class DemoEP implements AppModule{ + @Override + public void publish(App app) { + app.getRouter().importMethods(this); + } + @Routed() + public String hello(){ + Map context = new HashMap<>(); + context.put("name", "Jared"); + String ret=""; + try { + Template t=Template.find("/templates/login.hbs"); + System.out.println("Template:"+t); + ret = t.render(context).toString(); + } catch (IOException e) { + e.printStackTrace(); + } + return ret; + //#return "Hello World"; + } + @Routed( + path="/helloPlain" + ) + public void hello2(com.reliancy.jabba.Request req,Response resp) throws IOException{ + resp.getEncoder().writeln("Hi There"); + } + @Routed( + path="/hello3/{idd:int}" + ) + public String hello3(int id){ + return "Hello3:"+id; + } + @Routed( + path="/" + ) + public String home(){ + StringBuilder buf=new StringBuilder(); + buf.append("

Sample pages:

"); + buf.append("
plain
"); + buf.append("
parametric
"); + buf.append("
templated
"); + buf.append("
secured http
"); + buf.append("
secured form
"); + return buf.toString(); + } + @Routed + @Secured + public String secured(){ + return "We are secured"; + } + @Routed + @Secured( + login_form = "/login" + ) + public String secured_form(){ + return "We are secured by form"; + } + @Routed + public void login(com.reliancy.jabba.Request req,Response resp){ + //return "login form here"; + if(req.getVerb().equals("POST")){ + // here we need to process login and redirect + AppSession ass=AppSession.getInstance(); + try{ + System.out.println("Post login"); + String userid=(String)req.getParam("userid",null); + String pwd=(String)req.getParam("password",null); + System.out.println("SS:"+ass); + System.out.println("P:"+userid+"/"+pwd); + SecurityPolicy secpol=ass.getApp().getSecurityPolicy(); + SecurityActor user=secpol.authenticate(userid, pwd); + if(user==null) throw new NotAuthentic("invalid credentials"); + resp.setStatus(Response.HTTP_FOUND_REDIRECT); + //String old_url=request.getPath(); + //old_url=URLEncoder.encode(old_url,StandardCharsets.UTF_8.toString()); + resp.setHeader("Location","/home"); + }catch(Exception ex){ + ass.getApp().log().error("error:",ex); + Feedback.get().push(FeedbackLine.error(ex.getLocalizedMessage())); + } + } + //Map context = new HashMap<>(); + //context.put("app_title", "Jabba Login"); + //context.put("name", "Jared"); + //ArrayList events=new ArrayList<>(); + + //Feedback.get().push(FeedbackLine.error("Error")); + //Feedback.get().push(FeedbackLine.info("Error")); + //Feedback.get().push(FeedbackLine.warn("Error")); + //context.put("feedback",events); + try { + resp.setContentType("text/html"); + Rendering.begin("/templates/login.hbs") + //.with("feedback",events) + .end(resp); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + + } + +} diff --git a/src/main/java/com/reliancy/jabba/EndPoint.java b/src/main/java/com/reliancy/jabba/EndPoint.java index 501cad2..30a7f35 100644 --- a/src/main/java/com/reliancy/jabba/EndPoint.java +++ b/src/main/java/com/reliancy/jabba/EndPoint.java @@ -9,6 +9,9 @@ package com.reliancy.jabba; import java.io.IOException; +/** EndPoint is a special processor usually the last in chain. + * + */ public abstract class EndPoint extends Processor{ public EndPoint(String id) { diff --git a/src/main/java/com/reliancy/jabba/FileConfig.java b/src/main/java/com/reliancy/jabba/FileConfig.java index a35acaf..5bdffda 100644 --- a/src/main/java/com/reliancy/jabba/FileConfig.java +++ b/src/main/java/com/reliancy/jabba/FileConfig.java @@ -7,46 +7,91 @@ You may not use this file except in compliance with the License. */ package com.reliancy.jabba; -import java.util.HashMap; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; -public class FileConfig implements Config{ +public class FileConfig extends Config.Base{ + final Config parent; + final ArrayList> schema=new ArrayList<>(); String path; - final HashMap props=new HashMap<>(); - public FileConfig(String p){ + public FileConfig(Config parent,String p){ + this.parent=parent; path=p; - load(); } - public FileConfig(){ - this(null); + public FileConfig(String p){ + this(null,p); } - public void clear(){ + public Config clear(){ props.clear(); + return this; } @Override - public void load() { - + public Config getParent(){ + return parent; + }; + @Override + public Config load() throws IOException{ + if(parent!=null) parent.load(); + if(props.isEmpty()==false) return this; // not gona load again if loaded + return this; } @Override - public void save() { + public Config save() throws IOException{ + return this; } @Override public String getId() { return path; } - - public void setId(String path) { + public FileConfig setId(String path) { this.path = path; + return this; } @Override + public boolean hasProperty(Property key){ + if(props.containsKey(key)){ + return true; + }else if(parent!=null && parent.hasProperty(key)){ + return true; + }else{ + return false; + } + } + /** + * FileConfig defers to parent first (it could be argsconfig) then returns own. + */ + @Override public T getProperty(Config.Property key, T def) { - if(props.containsKey(key.getName())) return key.getTyp().cast(props.get(key.getName())); - else return def; + if(parent!=null && parent.hasProperty(key)){ + return parent.getProperty(key, def); + }else if(props.containsKey(key)){ + return key.getTyp().cast(props.get(key)); + }else{ + return def; + } } - + /** + * FileConfig will save property localy so it is perserved even if not later provided. + */ @Override public Config setProperty(Config.Property key, T val) { - props.put(key.getName(),val); + props.put(key,val); + return this; + } + @Override + public T delProperty(Property key) { + return (T)props.remove(key); + } + @Override + public Iterator> iterator() { + ArrayList> keys=new ArrayList<>(props.keySet()); + return keys.iterator(); + } + public Config setSchema(Property ...p){ + this.schema.clear(); + for(Property pp:p) schema.add(pp); return this; } diff --git a/src/main/java/com/reliancy/jabba/FileServer.java b/src/main/java/com/reliancy/jabba/FileServer.java index b9fd69b..c0c0bd2 100644 --- a/src/main/java/com/reliancy/jabba/FileServer.java +++ b/src/main/java/com/reliancy/jabba/FileServer.java @@ -10,70 +10,90 @@ 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.util.HashMap; +import java.util.ArrayList; import java.util.Iterator; -import java.util.stream.Stream; +import org.slf4j.Logger; -public class FileServer extends EndPoint implements Resources.PathRewrite{ - public static interface Filter{ - boolean isAcceptable(String path); +/** FileServer is an module and endpoint that exposes multiple URLs thru which files are served. + * First it will be just get(tting), later + * TODO: putting, posting and maybe full DAV. + * TODO: We will need proper security. + * TODO: We will also add in memory serving. + * 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. + */ + 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; } - public static class ExtFilter implements Filter{ - final String[] allowed; - public ExtFilter(String ...ext){allowed=ext;} + public static class FileBucket implements Bucket{ + final String prefix; + String[] extAllowed; + Object[] domain; + public FileBucket(String prefix){ + this.prefix=prefix; + extAllowed=new String[]{}; + domain=new Object[]{}; + } @Override - public boolean isAcceptable(String path) { - if(allowed.length==0) return true; - for(String ext:allowed) if(path.endsWith(ext)) return true; + 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; + } + } + return null; + } + @Override + public boolean equals(Object o){ + if(o instanceof FileBucket) return prefix.equals(((FileBucket)o).getPrefix()); + if(o instanceof String) return prefix.equals((String)o); return false; } - } - public static Filter NOFILTER=new ExtFilter(); - String diskPrefix; - String classPrefix; - final HashMap map; - final HashMap filt; - public FileServer(String url_path,Filter f,Object ... disk_path){ - super("fileserver"); - diskPrefix=classPrefix=null; - filt=new HashMap<>(); - map=new HashMap<>(); - addRoute(url_path,f, disk_path); - } - public FileServer(String url_path,Object ... disk_path){ - this(url_path,NOFILTER,disk_path); - } - @Override - public void serve(Request request, Response response) throws IOException { - String path=request.getPath(); - log().debug("to serve:"+path); - for(String prefix:map.keySet()){ - boolean match=path.startsWith(prefix); - if(match){ - Object[] sp=getSearchPath(prefix); - String rpath=path.replace(prefix,""); - if(!filt.get(prefix).isAcceptable(rpath)) continue; // not acceptable to filter - URL f=Resources.findFirst(this, rpath, sp); - if(f==null) continue; // skip if rpath not located - this.log().debug("\tfound:"+f); - writeResource(f,response); - return; - } + @Override + public boolean equals(String pref){return prefix.equals(pref);} + public FileBucket setDomain(Object...sp){ + 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 + return f.openStream(); + } + public OutputStream openSink(String local_path,FileServer user) throws IOException{ + return null; } - response.setStatus(Response.HTTP_NOT_FOUND); - response.getEncoder().writeln("missing file:{0}",path); - this.log().error("not found:"+path); } - /** - * we prefix our path for disk and class contexts. - */ - @Override - public String rewritePath(String path, Object context) { - if(diskPrefix!=null && context instanceof String) return this.diskPrefix+path; - if(diskPrefix!=null && context instanceof File) return this.diskPrefix+path; - if(classPrefix!=null && context instanceof Class) return this.classPrefix+path; - return path; + final ArrayList buckets=new ArrayList<>(); + String diskPrefix; // will be prefixed to source if file + String classPrefix; // will be prefixed to source if class + String urlPrefix; // will be prefixed to source if URL + public FileServer(String url_path,String offset,Object ... source){ + super(null); + diskPrefix=classPrefix=offset; + addBucket(new FileBucket(url_path).setDomain(source)); + } + public FileServer(){ + super(null); } public FileServer setDiskPrefix(String prefix){ diskPrefix=prefix; @@ -83,12 +103,50 @@ public class FileServer extends EndPoint implements Resources.PathRewrite{ classPrefix=prefix; return this; } + public FileServer setURLOffset(String offset){ + urlPrefix=offset; + return this; + } /** - * Will render a file to response. + * we prefix our path for disk and class contexts. + */ + @Override + public String rewritePath(String path, Object context) { + if(diskPrefix!=null && context instanceof String) return this.diskPrefix+path; + if(diskPrefix!=null && context instanceof File) return this.diskPrefix+path; + if(classPrefix!=null && context instanceof Class) return this.classPrefix+path; + if(urlPrefix!=null && context instanceof URL) return this.urlPrefix+path; + return path; + } + @Override + public void serve(Request request, Response response) throws IOException { + String path=request.getPath(); + Logger logger=log(); + boolean atDebug=logger.isDebugEnabled(); + if(atDebug) logger.debug("to serve:"+path); + 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(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; + } + } + response.setStatus(Response.HTTP_NOT_FOUND); + response.getEncoder().writeln("missing file:{0}",path); + logger.error("not found:"+path); + } + /** + * Will render a URL resource to response. * @param f * @param response */ - protected void writeResource(URL f, Response response) throws IOException{ + protected static void writeResource(URL f, Response response) throws IOException{ //log().info("writing:"+f); ResponseEncoder enc=response.getEncoder(); try(InputStream is=f.openStream()){ @@ -98,28 +156,29 @@ public class FileServer extends EndPoint implements Resources.PathRewrite{ enc.writeStream(is); } } - public final void addRoute(String url_path,Filter f,Object... disk_path){ - if(disk_path!=null){ - map.put(url_path,disk_path); - filt.put(url_path,f!=null?f:NOFILTER); - }else{ - map.remove(url_path); - filt.remove(url_path); - } + /** adds a route which serves files. + * if disk_path is ommited (0 len) or null we use Resources.search_path. + * @param url_path + * @param f + * @param disk_path search path for resource + */ + public final FileServer addBucket(Bucket bucket){ + buckets.add(bucket); + return this; } - public Object[] getSearchPath(String url_path){ - return map.get(url_path); + public FileServer delBucket(Bucket bucket){ + buckets.remove(bucket); + return this; } - public Filter getFilter(String url_path){ - return filt.get(url_path); + public Bucket getBucket(String url_path){ + for(Bucket b:buckets) if(b.equals(url_path)) return b; + return null; } - public Stream streamRoutes() { - return map.keySet().stream(); + public Iterator enumBuckets(){ + return buckets.iterator(); } - public Iterator enumRoutes(){ - return map.keySet().iterator(); - } - public void exportRoutes(RoutedEndPoint rep) { - streamRoutes().forEach(up->rep.addRoute("GET",up+".*",this)); + public void publish(App app) { + Router rep=app.getRouter(); + for(Bucket b:buckets) rep.addRoute("GET",b.getPrefix()+".*",this); } } diff --git a/src/main/java/com/reliancy/jabba/HTTP.java b/src/main/java/com/reliancy/jabba/HTTP.java index c2f8067..b0c57e6 100644 --- a/src/main/java/com/reliancy/jabba/HTTP.java +++ b/src/main/java/com/reliancy/jabba/HTTP.java @@ -11,8 +11,15 @@ import java.io.File; import java.net.URL; import java.util.HashMap; -/** HTTP related methods and classes. */ +/** HTTP related methods and classes. + * +*/ public final class HTTP { + public static String MIME_PLAIN="text/plain"; + public static String MIME_JSON="application/json"; + public static String MIME_BYTES="application/octet-stream"; + public static String MIME_HTML="text/html"; + public static HashMap MIME_MAP=new HashMap<>(); public static class Header{ public String key; @@ -30,6 +37,11 @@ public final class HTTP { key=k;value=v;this.maxAge=maxAge;secure=sec; } } + /** maps extension to mime type. + * it will extract everything after last dot so we can pass a path too. + * @param ext + * @return + */ public static String ext2mime(String ext){ if(MIME_MAP.isEmpty()){ MIME_MAP.put("ico","image/x-icon"); @@ -42,35 +54,44 @@ public final class HTTP { MIME_MAP.put("jpeg","image/jpeg"); MIME_MAP.put("png","image/png"); MIME_MAP.put("webp","image/webp"); - MIME_MAP.put("txt","text/plain"); + MIME_MAP.put("txt",MIME_PLAIN); MIME_MAP.put("css","text/css"); MIME_MAP.put("csv","text/csv"); - MIME_MAP.put("html","text/html"); - MIME_MAP.put("htm","text/html"); + MIME_MAP.put("html",MIME_HTML); + MIME_MAP.put("htm",MIME_HTML); MIME_MAP.put("xml","text/xml"); + MIME_MAP.put("json",MIME_JSON); } + ext=ext.substring(ext.lastIndexOf(".")+1).toLowerCase(); return MIME_MAP.get(ext); } + /** guesses mime type based on content. + * for url,path or file looks at the path otherwise it examines content. + * if you pass in charsequence it will look for indicators of html or json. + * for bytes nothing yet but we could examine headers to images. + * @param ret + * @return + */ public static String guess_mime(Object ret) { if(ret instanceof CharSequence){ CharSequence retstr=(CharSequence)ret; for(int index=0;index { + //System.out.println("closing endpoint:"+endpoint); + endpoint.close(); + }); + }finally{ + cc.close(); + //System.out.println("closing connecor:"+cc.getState()); + } + } } @Override @@ -132,7 +154,6 @@ public class JettyApp extends App implements Handler{ HttpServletResponse response) throws IOException { - baseRequest.setHandled(true); com.reliancy.jabba.Request req=new com.reliancy.jabba.Request(request); Response resp=new Response(response); @@ -143,40 +164,32 @@ public class JettyApp extends App implements Handler{ }catch(IOException ioex){ Template t=Template.find("/templates/error.hbs"); if(t==null) throw ioex; - Rendering.begin(t) - .with(ioex) - .end(resp.getEncoder().getWriter()); + Rendering.begin(t).with(ioex).end(resp); log().error("error:",ioex); }catch(RuntimeException rex){ Template t=Template.find("/templates/error.hbs"); if(t==null) throw rex; - Rendering.begin(t) - .with(rex) - .end(resp.getEncoder().getWriter()); + Rendering.begin(t).with(rex).end(resp); log().error("error:",rex); }finally{ + baseRequest.setHandled(true); ss.end(); } } /** our own interface specific to jetty engine*/ - - public Connector[] getConnectors(){ - if(connectors!=null) return connectors; - ServerConnector connector = new ServerConnector(jetty); - connector.setReuseAddress(false); - connector.setPort(8090); - connectors=new Connector[] {connector}; - return connectors; - } public void begin(Config conf) throws Exception{ + // step 2: configure application, might add processors, adjust config + configure(conf); + // step 1: install config then begin by signaling all middleware super.begin(conf); - jetty.setConnectors(getConnectors()); + // step 2: start jetty try{ + log().info("starting..."); jetty.start(); }catch(Exception ex){ setState(State.FAILED); if(ex.getCause() instanceof java.net.BindException){ - log().error("Bind issue",ex); + log().error("bind issue",ex); Thread.sleep(3000); }else throw ex; } @@ -186,150 +199,61 @@ public class JettyApp extends App implements Handler{ if(jetty!=null) jetty.join(); } public void end() throws Exception{ - //setState(State.STOPPING); super.end(); - Connector[] connectors=jetty.getConnectors(); - // System.out.println(connectors); - if(connectors!=null) for(Connector c:connectors){ - ServerConnector cc=(ServerConnector) c; - //System.out.println("stopping connecor:"+cc); - try{ - cc.stop(); - cc.getConnectedEndPoints().forEach((endpoint)-> { - //System.out.println("closing endpoint:"+endpoint); - endpoint.close(); - }); - }finally{ - cc.close(); - //System.out.println("closing connecor:"+cc.getState()); - } - } - //System.out.println("signaling..."); - jetty.stop(); - //setState(State.STOPPED); - //System.out.println("cleanup..."); - System.gc(); - //System.out.println("return..."); + Log.cleanup(); // release logging in case we deferred + System.gc(); // sweep memory just in caser } - public static void main( String[] args ) throws Exception{ - //System.out.println("Hello World!"); - //String rt=new File(".").getAbsolutePath(); - //System.out.println("ROOT:"+rt); - String work_dir="./var"; - if(new File(work_dir).exists()==false){ - work_dir="../var"; - } - Template.search_path(work_dir,App.class); - JettyApp app=new JettyApp(); + /** called from begin just before jetty starts. + * this method is called before middleware is notified so we can add or adjust config. + * override to hook up your application. + * normally follows configuraion and does common sense steps. + * might install middleware (processors) which are later passed config. + */ + public void configure(Config conf) throws Exception{ + App app=this; + // setup global search path - include workdir first, then get class and app.class + Class cls=getClass(); + if(cls!=JettyApp.class) Resources.appendSearch(0,JettyApp.class); + Resources.appendSearch(0,cls); + String work_dir=ArgsConfig.APP_WORKDIR.get(conf); + if(work_dir!=null) Resources.appendSearch(0,work_dir); + //for(Object p:Resources.search_path){ + // System.out.println("sp:"+p); + //} + //Template.search_path(work_dir,App.class); -- not needed anymore + // install app session middleware app.addAppSession(); + // set security policy SecurityPolicy secpol=new SecurityPolicy().setStore(new PlainSecurityStore()); app.setSecurityPolicy(secpol); - RoutedEndPoint rep=new RoutedEndPoint().importMethods(app); - app.setRouter(rep); - FileServer fs=new FileServer("/static",work_dir+"/public"); - fs.exportRoutes(app.getRouter()); + // install router + app.setRouter(new Router()); + DemoEP ep=new DemoEP(); + ep.publish(app); + // install file sever endpoint + FileServer fs=new FileServer("/static","/public"); + fs.publish(app); Menu top_menu=Menu.request(Menu.TOP); top_menu.add(new MenuItem("home")).addSpacer().add(new MenuItem("login")); - top_menu.setTitle("Jabba"); - app.run(new FileConfig()); - //System.out.println("Goodbye World!"); + top_menu.setTitle("Jabba3"); } - - @Routed() - public String hello(){ - Map context = new HashMap<>(); - context.put("name", "Jared"); - String ret=""; - try { - Template t=Template.find("/templates/login.hbs"); - System.out.println("Template:"+t); - ret = t.render(context).toString(); - } catch (IOException e) { - e.printStackTrace(); - } - return ret; - //#return "Hello World"; - } - @Routed( - path="/helloPlain" - ) - public void hello2(com.reliancy.jabba.Request req,Response resp) throws IOException{ - resp.getEncoder().writeln("Hi There"); - } - @Routed( - path="/hello3/{idd:int}" - ) - public String hello3(int id){ - return "Hello3:"+id; - } - @Routed( - path="/" - ) - public String home(){ - StringBuilder buf=new StringBuilder(); - buf.append("

Sample pages:

"); - buf.append("
plain
"); - buf.append("
parametric
"); - buf.append("
templated
"); - buf.append("
secured http
"); - buf.append("
secured form
"); - return buf.toString(); - } - @Routed - @Secured - public String secured(){ - return "We are secured"; - } - @Routed - @Secured( - login_form = "/login" - ) - public String secured_form(){ - return "We are secured by form"; - } - @Routed - public void login(com.reliancy.jabba.Request req,Response resp){ - //return "login form here"; - if(req.getVerb().equals("POST")){ - // here we need to process login and redirect - try{ - System.out.println("Post login"); - String userid=(String)req.getParam("userid",null); - String pwd=(String)req.getParam("password",null); - AppSession ass=AppSession.getInstance(); - System.out.println("SS:"+ass); - System.out.println("P:"+userid+"/"+pwd); - SecurityPolicy secpol=ass.getApp().getSecurityPolicy(); - SecurityActor user=secpol.authenticate(userid, pwd); - if(user==null) throw new NotAuthentic("invalid credentials"); - resp.setStatus(Response.HTTP_FOUND_REDIRECT); - //String old_url=request.getPath(); - //old_url=URLEncoder.encode(old_url,StandardCharsets.UTF_8.toString()); - resp.setHeader("Location","/home"); - }catch(Exception ex){ - log().error("error:",ex); - Feedback.get().push(FeedbackLine.error(ex.getLocalizedMessage())); + public static void main( String[] args ) throws Exception{ + Config cnf=new ArgsConfig(args).load(); + JettyApp app=new JettyApp(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + if(app.isRunning()){ + try { + app.jetty.stop(); + synchronized(app){ + app.wait(5000); + } + } catch (Exception e) { + app.log().error("shutdown cleanup:", e); + } } - } - //Map context = new HashMap<>(); - //context.put("app_title", "Jabba Login"); - //context.put("name", "Jared"); - //ArrayList events=new ArrayList<>(); - - //Feedback.get().push(FeedbackLine.error("Error")); - //Feedback.get().push(FeedbackLine.info("Error")); - //Feedback.get().push(FeedbackLine.warn("Error")); - //context.put("feedback",events); - try { - resp.setContentType("text/html"); - Rendering.begin("/templates/login.hbs") - //.with("feedback",events) - .end(resp.getEncoder().getWriter()); - } catch (IOException e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - + })); + app.run(cnf); } + } diff --git a/src/main/java/com/reliancy/jabba/Processor.java b/src/main/java/com/reliancy/jabba/Processor.java index b899914..3d84fb1 100644 --- a/src/main/java/com/reliancy/jabba/Processor.java +++ b/src/main/java/com/reliancy/jabba/Processor.java @@ -10,7 +10,12 @@ import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** Abstract base class of request/response handlers. + * App is a processor and under it a router and a chain of filters are also processors. + * Also endpoints are processors too. + */ public abstract class Processor { + protected Processor parent; protected Processor next; protected String id; protected boolean active; @@ -32,21 +37,30 @@ public abstract class Processor { public void setNext(Processor next) { this.next = next; } + public Processor getParent() { + return parent; + } + public void setParent(Processor p) { + if(parent!=null && p!=null && p!=parent){ + throw new IllegalStateException("processor is attached to different parent"); + } + this.parent = p; + } public boolean isActive() { return active; } public void setActive(boolean active) { this.active = active; } - public Config getConfig() { - return config; + if(config!=null) return config; + if(parent!=null) return parent.getConfig(); + return null; } - /* - public void setConfig(Config config) { - this.config = config; - } - */ + // using config as a marker of a run so set during begin + // public void setConfig(Config config) { + // this.config = config; + // } /** * Main event processing chain. * Will go down the chain until result code is set. @@ -70,9 +84,19 @@ public abstract class Processor { ss.leave(this); } } + /** Place to prepare for a run. */ public void begin(Config conf) throws Exception{ this.config=conf; } + /** Special null config begin only useful for middleware (to force them to use parent). */ + protected void begin() throws Exception { + this.begin(null); + } + /** + * cleans up by detaching from config. + * Also notifies any waiting clients that it is done. + * @throws Exception + */ public void end() throws Exception{ this.config=null; } diff --git a/src/main/java/com/reliancy/jabba/Request.java b/src/main/java/com/reliancy/jabba/Request.java index 15a69ec..823bc74 100644 --- a/src/main/java/com/reliancy/jabba/Request.java +++ b/src/main/java/com/reliancy/jabba/Request.java @@ -32,7 +32,7 @@ public class Request { return http_request.getMethod(); } /** - * Look for this parameter in pathParan, queryParams and forms. + * Look for this parameter in pathParam, queryParams and forms. * @param pname * @return */ diff --git a/src/main/java/com/reliancy/jabba/ResponseEncoder.java b/src/main/java/com/reliancy/jabba/ResponseEncoder.java index e895181..aa23e20 100644 --- a/src/main/java/com/reliancy/jabba/ResponseEncoder.java +++ b/src/main/java/com/reliancy/jabba/ResponseEncoder.java @@ -8,6 +8,7 @@ You may not use this file except in compliance with the License. package com.reliancy.jabba; import java.io.ByteArrayOutputStream; +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -15,6 +16,7 @@ import java.io.OutputStreamWriter; import java.io.Reader; import java.io.StringWriter; import java.io.Writer; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.text.MessageFormat; import java.util.Collection; @@ -25,22 +27,30 @@ import java.util.Locale; * This class will replace the Java writer. * It will have chainable calls. It will inherit lower level calls * and then extend with higher level. For example write, writeln but then writeJson etc. + * implements closeable,autocloseable,appendable. + * we do not close */ -public class ResponseEncoder { +public class ResponseEncoder implements Appendable,Closeable{ protected final Response response; - protected final Locale locale; + //protected final Locale locale; protected Writer writer; protected OutputStream out; - + protected Charset charSet; public ResponseEncoder(Response r){ - response=r; - locale=Locale.getDefault(); + this(r,StandardCharsets.UTF_8); + //response=r; + //locale=Locale.getDefault(); } - public ResponseEncoder(Response r,Locale loc){ + public ResponseEncoder(Response r,Charset chset){ response=r; - locale=loc; + //locale=loc; + charSet=StandardCharsets.UTF_8; } - protected OutputStream getOutputStream() throws IOException{ + public ResponseEncoder setCharSet(Charset set){ + charSet=set; + return this; + } + public OutputStream getOutputStream() throws IOException{ if(out!=null) return out; if(response.getStatus()==null) response.setStatus(Response.HTTP_OK); if(response.getContentType()==null) response.setContentType("application/octet-stream"); @@ -51,10 +61,10 @@ public class ResponseEncoder { }else{ out=new ByteArrayOutputStream(); } - writer=new OutputStreamWriter(out,StandardCharsets.UTF_8); + writer=new OutputStreamWriter(out,charSet); return out; } - protected Writer getWriter() throws IOException{ + public Writer getWriter() throws IOException{ if(writer!=null) return writer; if(response.getStatus()==null) response.setStatus(Response.HTTP_OK); if(response.getContentType()==null) response.setContentType("text/plain;charset=utf-8"); @@ -64,7 +74,7 @@ public class ResponseEncoder { writer=response.char_response; }else if(response.byte_response!=null){ out=response.byte_response; - writer=new OutputStreamWriter(out,StandardCharsets.UTF_8); + writer=new OutputStreamWriter(out,charSet); }else{ writer=new StringWriter(); } @@ -132,4 +142,23 @@ public class ResponseEncoder { //wr.append("\n"); return this; } + ////// Interface implementations + @Override + public void close() throws IOException { + getWriter().close(); + } + @Override + public Appendable append(CharSequence csq) throws IOException { + return append(csq,0,csq.length()); + } + @Override + public Appendable append(CharSequence csq, int start, int end) throws IOException { + this.getWriter().append(csq,start,end); + return this; + } + @Override + public Appendable append(char c) throws IOException { + this.getWriter().append(c); + return this; + } } diff --git a/src/main/java/com/reliancy/jabba/RoutedEndPoint.java b/src/main/java/com/reliancy/jabba/Router.java similarity index 90% rename from src/main/java/com/reliancy/jabba/RoutedEndPoint.java rename to src/main/java/com/reliancy/jabba/Router.java index ca7a93f..aee5a98 100644 --- a/src/main/java/com/reliancy/jabba/RoutedEndPoint.java +++ b/src/main/java/com/reliancy/jabba/Router.java @@ -18,16 +18,24 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; -public class RoutedEndPoint extends EndPoint{ +/** Router is a special Processor which redirects requests to endpoints. + * + */ +public class Router extends Processor{ HashMap routes=new HashMap<>(); // route pattern to endpoint ArrayList detectors=new ArrayList<>(); // route patterns ordered int[] indexes; // indexes for each route within regex Pattern regex; - public RoutedEndPoint() { + public Router() { super("router"); } - + @Override + public void before(Request request, Response response) throws IOException { + } + @Override + public void after(Request request, Response response) throws IOException { + } @Override public void serve(Request req, Response resp) throws IOException { //System.out.println(req.http_request); @@ -135,7 +143,7 @@ public class RoutedEndPoint extends EndPoint{ * @param target * @return */ - public RoutedEndPoint importMethods(Object target){ + public Router importMethods(Object target){ //RoutedEndPoint ret=new RoutedEndPoint(); LinkedList routes=new LinkedList<>(); Class type=target.getClass(); diff --git a/src/main/java/com/reliancy/jabba/ui/Menu.java b/src/main/java/com/reliancy/jabba/ui/Menu.java index 419d429..eb3561b 100644 --- a/src/main/java/com/reliancy/jabba/ui/Menu.java +++ b/src/main/java/com/reliancy/jabba/ui/Menu.java @@ -37,6 +37,10 @@ public class Menu extends MenuItem{ public int getSize(){ return items.size(); } + public Menu clear(){ + items.clear(); + return this; + } public Menu add(MenuItem itm){ items.add(itm); return this; diff --git a/src/main/java/com/reliancy/jabba/ui/Rendering.java b/src/main/java/com/reliancy/jabba/ui/Rendering.java index d2f44f5..e64251b 100644 --- a/src/main/java/com/reliancy/jabba/ui/Rendering.java +++ b/src/main/java/com/reliancy/jabba/ui/Rendering.java @@ -8,11 +8,11 @@ You may not use this file except in compliance with the License. package com.reliancy.jabba.ui; import java.io.IOException; -import java.io.Writer; import java.util.HashMap; import java.util.Map; import com.github.jknack.handlebars.Context; +import com.reliancy.jabba.Response; import com.reliancy.util.CodeException; /** @@ -26,6 +26,7 @@ public class Rendering extends HashMap{ ret.with("menu",Menu.request(Menu.TOP)); ret.with("toolbar",Menu.request(Menu.LEFT)); ret.with("feedback",Feedback.get()); + ret.with("layout","land-app"); return ret; } public static Rendering begin(String path,Object ...sp){ @@ -48,9 +49,9 @@ public class Rendering extends HashMap{ ctx.destroy(); } } - public void end(Writer _out) throws IOException{ + public void end(Response resp) throws IOException{ try{ - template.render(ctx,_out); + template.render(ctx,resp.getEncoder().getWriter()); }finally{ ctx.destroy(); } diff --git a/src/main/java/com/reliancy/jabba/ui/Template.java b/src/main/java/com/reliancy/jabba/ui/Template.java index 5b73dd6..d723c45 100644 --- a/src/main/java/com/reliancy/jabba/ui/Template.java +++ b/src/main/java/com/reliancy/jabba/ui/Template.java @@ -59,16 +59,29 @@ public class Template { @Override public TemplateSource sourceAt(String location) throws IOException { String fullpath=this.resolve(location); - URL loc=Resources.findFirst(null,fullpath,Template.search_path); + URL loc=Resources.findFirst(null,fullpath,Template.search_path()); //System.out.println(location+":"+loc+":"+fullpath); if (loc == null) { - Logger.getLogger(Template.class.getSimpleName()).warning("Missing template"+fullpath); + Logger.getLogger(Template.class.getSimpleName()).warning("Template missing:"+fullpath); throw new FileNotFoundException(location); } return new URLTemplateSource(location,loc); } + public String resolve(String uri){ + if(uri==null || uri.isEmpty()) return uri; + // we strip prefix and suffic just in case + if(uri.startsWith(this.getPrefix())) uri=uri.substring(this.getPrefix().length()); + if(uri.endsWith(this.getSuffix())) uri=uri.substring(0,uri.indexOf(this.getSuffix())); + if(Template.partial_map.containsKey(uri)){ + uri=Template.partial_map.get(uri); + } + return super.resolve(uri); + } } static Handlebars handlebars; + static HashMap partial_map=new HashMap<>(); + static Object[] search_path; + static HashMap cache=new HashMap<>(); static{ handlebars= new Handlebars(new HBLoader()); StringHelpers.register(handlebars); @@ -77,10 +90,8 @@ public class Template { } */ + partial_map.put("__frame__","frame-land"); } - - static Object[] search_path; - static HashMap cache=new HashMap<>(); /** renders a template to string, possibly locates it first. * * @param path @@ -89,7 +100,7 @@ public class Template { * @throws IOException */ public static CharSequence render(String path,Map context) throws IOException{ - Template t=find(path,search_path); + Template t=find(path); if(t==null){ return null; }else{ @@ -102,7 +113,7 @@ public class Template { public static Template find(String path,Object ... sp) { Template ret=cache.get(path); if(ret!=null) return ret; - URL loc=Resources.findFirst(null, path, (sp!=null && sp.length>0?sp:search_path)); + URL loc=Resources.findFirst(null, path, (sp!=null && sp.length>0?sp:search_path())); if(loc==null) return null; ret=new Template(loc); cache.put(path,ret); @@ -110,7 +121,17 @@ public class Template { } public static Object[] search_path(Object...sp){ if(sp!=null && sp.length>0) search_path=sp; - return search_path; + return search_path!=null?search_path:Resources.search_path; + } + /** + * will register a partial mapping to let us dynamically switch partials. + * this is useful in writing components for various layouts. + * all components derive from frame while frame is switched to frame-dash or frame-land. + * @param src + * @param dst + */ + public static void remap_partial(String src,String dst){ + Template.partial_map.put(src,dst); } public static final int ERR_BADTEMPLATE=ResultCode.defineFailure(0x01,Template.class,"bad template: ${template}"); com.github.jknack.handlebars.Template recipe; diff --git a/src/main/java/com/reliancy/util/Handy.java b/src/main/java/com/reliancy/util/Handy.java index 2a450e1..6a99470 100644 --- a/src/main/java/com/reliancy/util/Handy.java +++ b/src/main/java/com/reliancy/util/Handy.java @@ -13,6 +13,7 @@ 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; @@ -84,7 +85,7 @@ public final class Handy { */ public static Object normalize(Class clazz, Object val ) { if(val==null) return null; // we are null - if(clazz.isAssignableFrom(val.getClass())) return clazz; // we are assignable + 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; @@ -96,7 +97,6 @@ public final class Handy { if( Float.class==( clazz ) || float.class==( clazz ) ) return Float.parseFloat( value ); if( Double.class==( clazz ) || double.class==( clazz )) return Double.parseDouble( value ); } - return val; } /** @@ -523,4 +523,39 @@ public final class Handy { } return buf.toString(); } + /** splitting without using regex. + * leading or trailing delims will produce empty tokens. + * if you need to split on a set of single chars please use tokenizer. + * @param delim delim string + * @param str body of text to chop + * @param delim_count maximal number of splits or -1 for all + * @return array of tokens + */ + public static String[] split(String delim,String str,int delim_count) { + ArrayList ret=new ArrayList<>(); + int delimLen=delim!=null?delim.length():0; + int len=str.length(); + int index=0; + int delimCnt=0; // track splits + int delimAt=0; // last delim position + while(index0 && delimCnt>=delim_count) break; // reached limit of delims + delimAt=delimLen>0?str.indexOf(delim, index):index+1; + if(delimAt<0) break; // no more delims + // we got a hit + delimCnt+=1; + ret.add(str.substring(index, delimAt)); // add token + index=delimAt+delimLen; + } + if(index0){ + // add remainder (no more delim or delim at end) + ret.add(str.substring(index)); + } + return ret.toArray(new String[ret.size()]); + } + /** split a string as often as possible. */ + public static String[] split(String delim,String str) { + return split(delim,str,-1); + } + } diff --git a/src/main/java/com/reliancy/util/Log.java b/src/main/java/com/reliancy/util/Log.java new file mode 100644 index 0000000..5283442 --- /dev/null +++ b/src/main/java/com/reliancy/util/Log.java @@ -0,0 +1,69 @@ +package com.reliancy.util; + +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +/** Logging support based on JUL. + * We implement a deferred logmanager that survives shutdownhook until we release. + */ +public class Log { + static { + // must be called before any Logger method is used. + System.setProperty("java.util.logging.manager", DeferredMgr.class.getName()); + System.setProperty("java.util.logging.SimpleFormatter.format","%1$tF %1$tT %4$-7s [%3$s] %5$s%6$s%n"); + } + public static class DeferredMgr extends LogManager { + @Override public void reset() { /* don't reset yet. */ } + private void resetFinally() { super.reset(); } + } + public static Logger setup(){ + Logger root_logger=Logger.getLogger(""); + return root_logger; + } + public static void cleanup(){ + LogManager mgr=LogManager.getLogManager(); + if(mgr instanceof DeferredMgr){ + ((DeferredMgr)mgr).resetFinally(); + } + } + public static void setLevel(Logger logger,String level_name){ + if(level_name==null || level_name.isEmpty()) level_name="ERROR"; + level_name=level_name.toUpperCase(); + switch(level_name){ + case "v":{ + level_name="WARN"; + break; + } + case "vv":{ + level_name="INFO"; + break; + } + case "vvv":{ + level_name="DEBUG"; + break; + } + } + switch(level_name){ + case "WARN":{ + level_name="WARNING"; + break; + } + case "DEBUG":{ + level_name="FINER"; + break; + } + case "ERROR":{ + level_name="SEVERE"; + break; + } + } + Level lvl=Level.parse(level_name); + logger.setLevel(lvl); + for (Handler h : logger.getHandlers()) { + h.setLevel(lvl); + } + } + +} diff --git a/src/main/java/com/reliancy/util/Path.java b/src/main/java/com/reliancy/util/Path.java index 5c71891..b800cac 100644 --- a/src/main/java/com/reliancy/util/Path.java +++ b/src/main/java/com/reliancy/util/Path.java @@ -8,6 +8,7 @@ You may not use this file except in compliance with the License. package com.reliancy.util; import java.io.File; +import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; @@ -42,8 +43,15 @@ public class Path { 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) { - setConnectString(connect); + this(connect,true); } public Path(Path in) { @@ -59,7 +67,7 @@ public class Path { @Override public String toString() { - return getConnectString(); + return assemble(); } /** * Converts the path to a file. @@ -85,7 +93,7 @@ public class Path { if(Handy.isBlank(getHost()) && path.contains("://") && !path.contains(":///")) path=path.replace("://",":///"); return new URL(path); } - public void clear(){ + public Path clear(){ connectstring=null; protocol=null; userid=null; @@ -94,9 +102,9 @@ public class Path { port=null; database=null; properties=null; + return this; } - - public String getConnectString() { + public String assemble() { if(connectstring!=null) return connectstring; // assemble the connect string StringBuilder buf=new StringBuilder(); @@ -131,10 +139,10 @@ public class Path { return connectstring; } - public void setConnectString(String connect) { + public Path parse(String connect) { clear(); if (connect == null) { - return; + return this; } this.connectstring=connect; // first get protocol - everything up to : which is not followed by a symbol (includes :// but also c:/ @@ -204,6 +212,7 @@ public class Path { properties = database.substring(st); database = database.substring(0, st); } + return this; } /** @@ -411,12 +420,22 @@ public class Path { return str.split("&"); } public static String[] splitKeyValue(String str) { - String[] t=str.split("="); + String[] t=Handy.split("=",str,1); if(t==null || t.length==0) return null; t[0]=Handy.trim(t[0],"'\""); - return t; + 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 str.split("/"); + return Handy.split("/",str); } } diff --git a/src/main/java/com/reliancy/util/Resources.java b/src/main/java/com/reliancy/util/Resources.java index fbe08a8..dc6e1c4 100644 --- a/src/main/java/com/reliancy/util/Resources.java +++ b/src/main/java/com/reliancy/util/Resources.java @@ -19,18 +19,47 @@ 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 Object[] search_path; + /** appends one+ paths to search at position pos. + * neg pos substracts from end + */ + public static Object[] appendSearch(int pos,Object ...src){ + 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; + } public static interface PathRewrite{ public String rewritePath(String path,Object context); } 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(path,base); + if(remap!=null) path=remap.rewritePath(path0,base); if(base instanceof Class){ URL ret=((Class)base).getResource(path); return ret; @@ -57,11 +86,20 @@ public class Resources { URL ret=new URL((URL)base,path); String proto=ret.getProtocol(); if(proto.equals("http") || proto.equals("https")){ - HttpURLConnection huc = (HttpURLConnection) ret.openConnection(); - huc.setRequestMethod("HEAD"); - int responseCode = huc.getResponseCode(); - huc.disconnect(); - if(responseCode==HttpURLConnection.HTTP_OK) return ret; + HttpURLConnection huc = null; + try{ + huc=(HttpURLConnection) ret.openConnection(); + huc.setRequestMethod("HEAD"); + int responseCode = huc.getResponseCode(); + if(responseCode==HttpURLConnection.HTTP_OK) return ret; + }finally{ + if(huc!=null) huc.disconnect(); + } + } + if(proto.startsWith("jar")){ + JarURLConnection juc = null; + juc=(JarURLConnection) ret.openConnection(); + if(juc.getJarEntry()!=null) return ret; } if(proto.equals("file")){ File f=new File(ret.getPath()); diff --git a/src/main/web/public/theme.css b/src/main/web/public/theme.css new file mode 100644 index 0000000..db6bbc4 --- /dev/null +++ b/src/main/web/public/theme.css @@ -0,0 +1,14 @@ +html, body { + height: 100%; +} +.fa-2x { + vertical-align: middle; +} +.col-fixed { + margin: 1em auto; +} +@media (min-width: 768px) { +.col-fixed { + width: 600px; +} +} diff --git a/src/main/web/templates/base.hbs b/src/main/web/templates/base.hbs new file mode 100644 index 0000000..20e8637 --- /dev/null +++ b/src/main/web/templates/base.hbs @@ -0,0 +1,29 @@ + + + + + + {{app_title}} + + + + + + + {{#block "MetaEx"}} + {{/block}} + + + {{#block "Header"}} + {{/block}} + {{#block "Detail"}} + {{/block}} + {{#block "Footer"}} + {{/block}} + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/error.hbs b/src/main/web/templates/error.hbs similarity index 73% rename from src/main/resources/templates/error.hbs rename to src/main/web/templates/error.hbs index 5d13383..6a26b89 100644 --- a/src/main/resources/templates/error.hbs +++ b/src/main/web/templates/error.hbs @@ -2,7 +2,9 @@
-
{{error_title}}
+
+ {{error_title}} +
@@ -15,4 +17,4 @@
{{/partial}} -{{> frame-1c}} +{{> __frame__}} diff --git a/src/main/web/templates/frame-dash.hbs b/src/main/web/templates/frame-dash.hbs new file mode 100644 index 0000000..e046264 --- /dev/null +++ b/src/main/web/templates/frame-dash.hbs @@ -0,0 +1,79 @@ +{{#partial "Header"}} + + + +
+ {{#each feedback}} +
{{message}}
+ {{/each}} + +
+{{/partial}} +{{#partial "Detail"}} + +
+
+
+ {{#block "LeftSection"}} + Left Section + {{/block}} +
+
+ {{#block "MainSection"}} + Main Section + {{/block}} +
+
+ +{{/partial}} +{{> base}} diff --git a/src/main/web/templates/frame-land.hbs b/src/main/web/templates/frame-land.hbs new file mode 100644 index 0000000..a197919 --- /dev/null +++ b/src/main/web/templates/frame-land.hbs @@ -0,0 +1,71 @@ +{{#partial "Header"}} + + + +
+ {{#each feedback}} +
{{message}}
+ {{/each}} + +
+{{/partial}} +{{#partial "Detail"}} + +
+{{#block "MainSection"}} + Main Section +{{/block}} +
+ +{{/partial}} +{{> base}} diff --git a/src/main/resources/templates/login.hbs b/src/main/web/templates/login.hbs similarity index 96% rename from src/main/resources/templates/login.hbs rename to src/main/web/templates/login.hbs index 63130e4..9d75c7b 100644 --- a/src/main/resources/templates/login.hbs +++ b/src/main/web/templates/login.hbs @@ -21,4 +21,4 @@
{{/partial}} -{{> frame-1c}} +{{> __frame__}} diff --git a/src/test/java/com/reliancy/dbo/TerminalTest.java b/src/test/java/com/reliancy/dbo/TerminalTest.java index ba1e2c5..697998f 100644 --- a/src/test/java/com/reliancy/dbo/TerminalTest.java +++ b/src/test/java/com/reliancy/dbo/TerminalTest.java @@ -60,7 +60,7 @@ public class TerminalTest { @BeforeClass public static void beforeAllTestMethods() { System.out.println("Invoked once before all test methods"); - String url="jdbc:postgresql://postgres:Ramudin99@bigbang:5432/Test"; + String url=System.getenv("DB_URL"); t=new SQLTerminal(url); } diff --git a/src/test/java/com/reliancy/jabba/ArgsConfigTest.java b/src/test/java/com/reliancy/jabba/ArgsConfigTest.java new file mode 100644 index 0000000..050c761 --- /dev/null +++ b/src/test/java/com/reliancy/jabba/ArgsConfigTest.java @@ -0,0 +1,34 @@ +package com.reliancy.jabba; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; + +import org.junit.Test; + +public class ArgsConfigTest { + @Test + public void testEnv(){ + System.out.println("testing sys env..."); + ArgsConfig args=new ArgsConfig("prog","--verbose","--key","value","cmd"); + try { + args.load(); + ArgsConfig.Property env_user=new ArgsConfig.Property<>("USER",String.class); + ArgsConfig.Property sys_user=new ArgsConfig.Property<>("user.name",String.class); + ArgsConfig.Property verbose=new ArgsConfig.Property<>("verbose",Boolean.class); + String usr_val1=args.getProperty(env_user,"None1"); + String usr_val2=args.getProperty(sys_user,"None2"); + System.out.println("Env User:"+usr_val1); + System.out.println("Sys User:"+usr_val2); + assertTrue(usr_val1.equals(usr_val2)); + System.out.println("Positional:"+args.getProperty(Config.APP_ARGS, null)); + System.out.println("Verbose:"+verbose.get(args)); + for(ArgsConfig.Property p:args){ + System.out.println("p:"+p+"="+p.get(args)); + } + + } catch (IOException e) { + e.printStackTrace(); + assertTrue(false); + } + } +} diff --git a/src/test/java/com/reliancy/jabba/RouterTest.java b/src/test/java/com/reliancy/jabba/RouterTest.java index aa7bcd2..614a683 100644 --- a/src/test/java/com/reliancy/jabba/RouterTest.java +++ b/src/test/java/com/reliancy/jabba/RouterTest.java @@ -39,7 +39,7 @@ public class RouterTest //assertTrue( true ); System.out.println("Test router init..."); JettyApp r=new JettyApp(); - RoutedEndPoint rep=new RoutedEndPoint(); + Router rep=new Router(); rep.importMethods(r); rep.compile(); //Matcher m=rep.match("GET","/helloPlain"); diff --git a/src/test/java/com/reliancy/util/HandyTest.java b/src/test/java/com/reliancy/util/HandyTest.java new file mode 100644 index 0000000..2905fd3 --- /dev/null +++ b/src/test/java/com/reliancy/util/HandyTest.java @@ -0,0 +1,40 @@ +/* +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); + } + + } + +}