From feb5d4c163d9b1441013b769e48db9191bb9fcac Mon Sep 17 00:00:00 2001 From: Amer Agovic Date: Sun, 21 Jan 2024 17:02:31 -0600 Subject: [PATCH] fixes and support for h2c --- build.gradle | 37 +++--- src/main/java/com/reliancy/jabba/App.java | 106 +++++++++++++++- .../java/com/reliancy/jabba/AppModule.java | 4 +- .../java/com/reliancy/jabba/ArgsConfig.java | 3 + src/main/java/com/reliancy/jabba/DemoEP.java | 118 ------------------ .../java/com/reliancy/jabba/FileServer.java | 36 +++--- .../java/com/reliancy/jabba/JettyApp.java | 48 ++++--- .../java/com/reliancy/jabba/Processor.java | 2 +- src/main/java/com/reliancy/jabba/Request.java | 39 +++++- .../java/com/reliancy/jabba/Response.java | 1 + .../com/reliancy/jabba/ResponseEncoder.java | 27 ++++ src/main/java/com/reliancy/jabba/Router.java | 21 +++- .../java/com/reliancy/jabba/StatusMod.java | 25 ++++ .../java/com/reliancy/rec/JSONEncoder.java | 22 ++-- .../java/com/reliancy/util/CodeException.java | 8 +- src/test/java/com/reliancy/jabba/DemoApp.java | 6 +- 16 files changed, 309 insertions(+), 194 deletions(-) delete mode 100644 src/main/java/com/reliancy/jabba/DemoEP.java create mode 100644 src/main/java/com/reliancy/jabba/StatusMod.java diff --git a/build.gradle b/build.gradle index be879a3..f9cf74e 100644 --- a/build.gradle +++ b/build.gradle @@ -17,15 +17,24 @@ application{ mainClass=(group+'.'+name+'.JettyApp') } java{ - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + // make our library a bit more compatible (jetty forced 11 else it would have been 1.8) + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } -// print some info for orientation -//println("group:"+group); -//println("name:"+name); -//println("version:"+version); -//println("entry:"+mainClassName); +dependencies { + def jettyVersion="11.0.18" + implementation "org.eclipse.jetty:jetty-server:${jettyVersion}" + implementation "org.eclipse.jetty.http2:http2-server:${jettyVersion}" + implementation "org.slf4j:slf4j-jdk14:2.0.10" + //implementation "org.slf4j:slf4j-simple:2.0.10" + //implementation 'com.hubspot.jinjava:jinjava:2.5.10' + implementation 'com.github.jknack:handlebars:4.3.0' + implementation 'com.h2database:h2:2.1.214' + implementation 'org.postgresql:postgresql:42.5.0' + implementation 'com.zaxxer:HikariCP:5.0.0' + testImplementation "junit:junit:4.12" +} sourceSets { main { @@ -97,20 +106,6 @@ javadoc { links "https://docs.oracle.com/javase/8/docs/api/" } } -dependencies { - implementation "org.eclipse.jetty:jetty-server:11.0.1" - 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' - // https://mvnrepository.com/artifact/org.postgresql/postgresql - implementation 'org.postgresql:postgresql:42.5.0' - // https://mvnrepository.com/artifact/com.zaxxer/HikariCP - implementation 'com.zaxxer:HikariCP:5.0.0' - - testImplementation "junit:junit:4.12" -} test { environment "DB_URL", project.db_url testLogging { diff --git a/src/main/java/com/reliancy/jabba/App.java b/src/main/java/com/reliancy/jabba/App.java index bed92b8..6c32a6a 100644 --- a/src/main/java/com/reliancy/jabba/App.java +++ b/src/main/java/com/reliancy/jabba/App.java @@ -9,8 +9,13 @@ You may not use this file except in compliance with the License. package com.reliancy.jabba; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + import com.reliancy.dbo.Terminal; import com.reliancy.jabba.sec.SecurityPolicy; +import com.reliancy.jabba.ui.Rendering; +import com.reliancy.jabba.ui.Template; import com.reliancy.util.CodeException; import com.reliancy.util.ResultCode; @@ -40,7 +45,8 @@ public abstract class App extends Processor{ protected Router router=null; protected SecurityPolicy policy=null; protected Terminal storage=null; - + protected Map modules; + public App(String id) { super(id); } @@ -57,6 +63,71 @@ public abstract class App extends Processor{ if(first!=null && resp.getStatus()==null) first.process(req, resp); if(router!=null && resp.getStatus()==null) router.process(req,resp); } + /** When an error occurs we need properly render exception. + * if html is accepted we try to render a valid response with n error within a template so it fits with the app. + * for all others we set error status code. + * for json,xml and plain we render into a message template for the rest we do nothing. + * this method returns true if a response was generated. in overloaded methods + * if false is returned we can generate response the status is set to 500 already. + * @param req incoming request + * @param ex exception state + * @param resp response to generate + * @return true if handled else it signifies we should do somthing in overloads. + * @throws IOException + */ + public boolean processError(com.reliancy.jabba.Request req,Throwable ex,com.reliancy.jabba.Response resp) throws IOException{ + log().error("error:",ex); + String accepted_format=req.getHeader("Accept"); + boolean present=accepted_format!=null; + if(present && ( + accepted_format.contains("/html") + || accepted_format.contains("/xhtml") + )){ + // we have html request + resp.setContentType(HTTP.MIME_HTML); + Template t=Template.find("/templates/error.hbs"); + if(t==null){ // no template found + resp.setStatus(Response.HTTP_INTERNAL_ERROR); + if(ex instanceof IOException) throw ((IOException)ex); + else throw new RuntimeException(ex); + } + Rendering.begin(t).with(ex).end(resp); + return true; + }else{ + // for all other cases we first flag it as error + resp.setStatus(Response.HTTP_INTERNAL_ERROR); + } + // next we format a few common and supported messages + if(present && accepted_format.contains("/json")){ + ResponseEncoder enc=resp.getEncoder(); + if(enc.getErrorFormat()==null){ + String template="'{'\n\t\"status\":\"error\",\n\t\"title\":\"{0}\",\n\t\"message\":\"{1}\"\n'}'\n"; + enc.setErrorFormat(template); + } + enc.writeError(ex); + return true; + } + if(present && accepted_format.contains("/xml")){ + ResponseEncoder enc=resp.getEncoder(); + if(enc.getErrorFormat()==null){ + String template="\n\terror\n\t{0}\n\t{1}\n\n"; + enc.setErrorFormat(template); + } + enc.writeError(ex); + return true; + } + if(present && accepted_format.contains("text/plain")){ + ResponseEncoder enc=resp.getEncoder(); + if(enc.getErrorFormat()==null){ + String template="status=error\n\ntitle={0}\n\nmessage={1}\n\n"; + enc.setErrorFormat(template); + } + enc.writeError(ex); + return true; + } + return false; + } + /** add one or a chain of processors. */ public T addMiddleWare(T m){ if(m==null) return null; @@ -183,4 +254,37 @@ public abstract class App extends Processor{ public Terminal getStorage(){ return storage; } + /** register module under one or more names. + * if no names are provided just calls module to publish itself. + * @param module + * @param names + */ + public void publishModule(AppModule module,String...names){ + if(names.length==0){ + module.publish(this); + }else{ + if(modules==null) modules=new HashMap<>(); + for(String nm:names) if(nm!=null) modules.put(nm.toLowerCase(),module); + } + } + /** retracts module under given names. + * if no names provided calls on module to retract itself. + */ + public void retractModule(AppModule module,String...names){ + if(names.length==0){ + module.retract(this); + // if injected from outside we remove those too + if(modules!=null) while(modules.values().remove(module)); + }else{ + if(modules==null) return; + for(String nm:names){ + if("*".equals(nm)){ // special case to remove all + while(modules.values().remove(module)); + break; + } + modules.remove(nm.toLowerCase()); + } + } + } + } diff --git a/src/main/java/com/reliancy/jabba/AppModule.java b/src/main/java/com/reliancy/jabba/AppModule.java index 751c68b..208c0ea 100644 --- a/src/main/java/com/reliancy/jabba/AppModule.java +++ b/src/main/java/com/reliancy/jabba/AppModule.java @@ -5,5 +5,7 @@ package com.reliancy.jabba; */ public interface AppModule { void publish(App app); - default void retract(App app){}; + default void retract(App app){ + app.retractModule(this,"*"); + }; } diff --git a/src/main/java/com/reliancy/jabba/ArgsConfig.java b/src/main/java/com/reliancy/jabba/ArgsConfig.java index 5729f4b..0df06fb 100644 --- a/src/main/java/com/reliancy/jabba/ArgsConfig.java +++ b/src/main/java/com/reliancy/jabba/ArgsConfig.java @@ -130,6 +130,9 @@ public class ArgsConfig extends Config.Base{ APP_SETTINGS.set(this, cwd); } // also logging level and format + // System.out.println("LogLog:"+LOG_LEVEL.get(this)); + // System.out.println("ENV:"+System.getenv("LOG_LEVEL")); + // LOG_LEVEL.set(this,"INFO"); Logger root=Log.setup(); Log.setLevel(root,LOG_LEVEL.get(this)); return this; diff --git a/src/main/java/com/reliancy/jabba/DemoEP.java b/src/main/java/com/reliancy/jabba/DemoEP.java deleted file mode 100644 index 151319e..0000000 --- a/src/main/java/com/reliancy/jabba/DemoEP.java +++ /dev/null @@ -1,118 +0,0 @@ -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/FileServer.java b/src/main/java/com/reliancy/jabba/FileServer.java index 17a8486..977b975 100644 --- a/src/main/java/com/reliancy/jabba/FileServer.java +++ b/src/main/java/com/reliancy/jabba/FileServer.java @@ -140,11 +140,11 @@ public class FileServer extends EndPoint implements AppModule,Resources.PathRewr String path=request.getPath(); Logger logger=log(); boolean atDebug=logger.isDebugEnabled(); - if(atDebug) logger.debug("{0}:{1}",verb,path); - for(Bucket bucket:buckets){ - String local_path=bucket.asContained(path); - if(local_path==null) continue; // this bucket is not accepting - if(HTTP.VERB_GET.equals(verb)){ + 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); @@ -166,30 +166,38 @@ public class FileServer extends EndPoint implements AppModule,Resources.PathRewr enc.writeStream(ins); return; // we got something } - }else{ - // these verbs are not supported } + }else{ + // these verbs are not supported } response.setStatus(Response.HTTP_NOT_FOUND); - response.getEncoder().writeln("missing file:{0}",path); - logger.error("not found:{0}",path); + response.getEncoder().writeln("missing file:"+path); + logger.error("not found:{}",path); } /** * Will render a URL resource to response. * @param f * @param response */ - protected static void writeResource(URL f, Response response) throws IOException{ - //log().info("writing:"+f); - ResponseEncoder enc=response.getEncoder(); + public static boolean sendData(URL f, Response response) throws IOException{ try(InputStream is=f.openStream()){ - String ctype=HTTP.guess_mime(f); + if(is==null) return false; response.setStatus(Response.HTTP_OK); + String ctype=HTTP.guess_mime(f); response.setContentType(ctype); + ResponseEncoder enc=response.getEncoder(); enc.writeStream(is); + return true; } } - /** adds a route which serves files. + public static boolean sendData(InputStream istr, Response response) throws IOException{ + if(istr==null) return false; + ResponseEncoder enc=response.getEncoder(); + response.setStatus(Response.HTTP_OK); + enc.writeStream(istr); + return true; + } + /** adds a route which serves files. * if disk_path is ommited (0 len) or null we use Resources.search_path. * @param bucket resource holder to add */ diff --git a/src/main/java/com/reliancy/jabba/JettyApp.java b/src/main/java/com/reliancy/jabba/JettyApp.java index 9e3d322..7e0ad15 100644 --- a/src/main/java/com/reliancy/jabba/JettyApp.java +++ b/src/main/java/com/reliancy/jabba/JettyApp.java @@ -8,6 +8,7 @@ You may not use this file except in compliance with the License. package com.reliancy.jabba; import java.io.IOException; +import java.text.MessageFormat; import java.util.EventListener; import com.reliancy.jabba.sec.SecurityPolicy; @@ -16,11 +17,19 @@ import com.reliancy.jabba.ui.Menu; import com.reliancy.jabba.ui.MenuItem; import com.reliancy.jabba.ui.Rendering; import com.reliancy.jabba.ui.Template; +import com.reliancy.rec.JSONEncoder; +import com.reliancy.util.CodeException; import com.reliancy.util.Log; import com.reliancy.util.Resources; +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.ForwardedRequestCustomizer; import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.MultiPartFormDataCompliance; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -28,10 +37,14 @@ import org.eclipse.jetty.server.ServerConnector; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; + /** * Router is entry point and servlet implementation that dispatches messages to our endpoints. * It will launch an embedded jetty server. - * It will provide facilities to register endpoints. + * It will provide facilities to register endpoints via router. + * Mostly new routes are injected via AppModules which publish themselves. + * JettyApp installs ForwardCustomizer to react to reverse proxy setups. + * */ public class JettyApp extends App implements Handler{ enum State{ @@ -52,15 +65,22 @@ public class JettyApp extends App implements Handler{ jetty.setHandler(this); _state=State.STOPPED; } - /** implementation of jetty handler interface */ public Connector[] getConnectors(){ if(connectors!=null) return connectors; - ServerConnector connector = new ServerConnector(jetty); - connector.setReuseAddress(false); - connector.setPort(8090); - connectors=new Connector[] {connector}; + // Create HTTP Config + HttpConfiguration httpConfig = new HttpConfiguration(); + // Add support for X-Forwarded headers + httpConfig.addCustomizer( new ForwardedRequestCustomizer() ); + // Create the http connector + HttpConnectionFactory http11 = new HttpConnectionFactory( httpConfig ); + HTTP2ServerConnectionFactory h2c = new HTTP2CServerConnectionFactory(httpConfig); + ServerConnector httpConn = new ServerConnector(jetty,http11,h2c); + httpConn.setReuseAddress(false); + httpConn.setPort(8090); + connectors=new Connector[] {httpConn}; return connectors; } + /** implementation of jetty handler interface */ @Override public Server getServer() { return jetty; @@ -68,12 +88,10 @@ public class JettyApp extends App implements Handler{ @Override public void setServer(Server arg0) { - System.out.println("setServer..."+jetty+"/"+arg0); jetty=arg0; } @Override public boolean addEventListener(EventListener arg0) { - System.out.println("adding evt listener..."); return false; } @Override @@ -160,16 +178,8 @@ public class JettyApp extends App implements Handler{ try{ ss.begin(null, req, resp); process(req,resp); - }catch(IOException ioex){ - Template t=Template.find("/templates/error.hbs"); - if(t==null) throw ioex; - 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); - log().error("error:",rex); + }catch(Exception ioex){ + processError(req,ioex,resp); }finally{ baseRequest.setHandled(true); ss.end(); @@ -246,7 +256,7 @@ public class JettyApp extends App implements Handler{ app.setSecurityPolicy(secpol); // install router app.setRouter(new Router()); - DemoEP ep=new DemoEP(); + StatusMod ep=new StatusMod(); ep.publish(app); // install file sever endpoint FileServer fs=new FileServer("/static","/public"); diff --git a/src/main/java/com/reliancy/jabba/Processor.java b/src/main/java/com/reliancy/jabba/Processor.java index 3d84fb1..5aacb42 100644 --- a/src/main/java/com/reliancy/jabba/Processor.java +++ b/src/main/java/com/reliancy/jabba/Processor.java @@ -24,7 +24,7 @@ public abstract class Processor { public Processor(String id){ next=null; - this.id=id!=null?id:this.getClass().getSimpleName().toLowerCase(); + this.id=id!=null?id:this.getClass().getSimpleName(); active=true; } public String getId(){ diff --git a/src/main/java/com/reliancy/jabba/Request.java b/src/main/java/com/reliancy/jabba/Request.java index 823bc74..77aa8aa 100644 --- a/src/main/java/com/reliancy/jabba/Request.java +++ b/src/main/java/com/reliancy/jabba/Request.java @@ -18,6 +18,7 @@ import jakarta.servlet.http.HttpServletRequest; public class Request { final HttpServletRequest http_request; final HashMap pathParams=new HashMap<>(); + String pathOverride; public Request(HttpServletRequest http_request) { this.http_request = http_request; } @@ -25,9 +26,16 @@ public class Request { return pathParams; } public String getPath() { - return http_request.getPathInfo(); + if(pathOverride!=null){ + return pathOverride; + }else{ + return http_request.getPathInfo(); + } + } + public Request setPath(String path){ + pathOverride=path; + return this; } - public String getVerb() { return http_request.getMethod(); } @@ -76,6 +84,10 @@ public class Request { "HTTP_FORWARDED", "HTTP_VIA", "REMOTE_ADDR" }; + /** + * This method will consult several headers to obain ip address. + * @return best guess for remote address. + */ public String getRemoteAddress() { for (String header : HEADERS4IP) { String ip = getHeader(header); @@ -84,4 +96,27 @@ public class Request { } return http_request.getRemoteAddr(); } + /** + * will return shema://host:port/context + * @return everything preceeding the path. + */ + public String getMount(){ + String scheme = http_request.getScheme(); + String host = http_request.getHeader("Host"); // includes server name and server port + if(host==null || host.trim().isEmpty()){ + // try differenty for host + String serverName = http_request.getServerName(); + int serverPort = http_request.getServerPort(); + host=serverName+":"+serverPort; + } + String resultPath = scheme + "://" + host; + String contextPath = http_request.getContextPath(); // includes leading forward slash + if(contextPath!=null){ + resultPath+= contextPath; + } + return resultPath; + } + public String getProtocol(){ + return http_request.getProtocol(); + } } diff --git a/src/main/java/com/reliancy/jabba/Response.java b/src/main/java/com/reliancy/jabba/Response.java index ff852ef..9a91dca 100644 --- a/src/main/java/com/reliancy/jabba/Response.java +++ b/src/main/java/com/reliancy/jabba/Response.java @@ -34,6 +34,7 @@ public class Response { public static final int HTTP_TEMPORARY_REDIRECT=HttpServletResponse.SC_TEMPORARY_REDIRECT; public static final int HTTP_FOUND_REDIRECT=HttpServletResponse.SC_FOUND; public static final int HTTP_NOT_MODIFIED=HttpServletResponse.SC_NOT_MODIFIED; + public static final int HTTP_INTERNAL_ERROR=HttpServletResponse.SC_INTERNAL_SERVER_ERROR; final protected HttpServletResponse http_response; final protected Writer char_response; diff --git a/src/main/java/com/reliancy/jabba/ResponseEncoder.java b/src/main/java/com/reliancy/jabba/ResponseEncoder.java index 3e02376..ed0c0e5 100644 --- a/src/main/java/com/reliancy/jabba/ResponseEncoder.java +++ b/src/main/java/com/reliancy/jabba/ResponseEncoder.java @@ -22,6 +22,9 @@ import java.text.MessageFormat; import java.util.Collection; import java.util.Iterator; +import com.reliancy.rec.JSONEncoder; +import com.reliancy.util.CodeException; + /** * This class will replace the Java writer. * It will have chainable calls. It will inherit lower level calls @@ -35,6 +38,8 @@ public class ResponseEncoder implements Appendable,Closeable{ protected Writer writer; protected OutputStream out; protected Charset charSet; + protected String errorFmt; + public ResponseEncoder(Response r){ this(r,StandardCharsets.UTF_8); //response=r; @@ -118,6 +123,28 @@ public class ResponseEncoder implements Appendable,Closeable{ } return this; } + public ResponseEncoder setErrorFormat(String fmt){ + errorFmt=fmt; + return this; + } + public String getErrorFormat(){ + return this.errorFmt; + } + public ResponseEncoder writeError(Throwable ex) throws IOException{ + if(errorFmt==null){ + this.writeString(ex.toString()); + }else{ + StringBuilder title=new StringBuilder(); + StringBuilder detail=new StringBuilder(); + CodeException.fillUserMessage(ex, detail, title); + String body=MessageFormat.format( + errorFmt, + JSONEncoder.escape(title), + JSONEncoder.escape(detail)); + writeString(body); + } + return this; + } public ResponseEncoder writeObject(Object ret) throws IOException{ if(ret==null) return this; Writer wr=getWriter(); diff --git a/src/main/java/com/reliancy/jabba/Router.java b/src/main/java/com/reliancy/jabba/Router.java index aee5a98..00005f9 100644 --- a/src/main/java/com/reliancy/jabba/Router.java +++ b/src/main/java/com/reliancy/jabba/Router.java @@ -28,7 +28,7 @@ public class Router extends Processor{ Pattern regex; public Router() { - super("router"); + super("Router"); } @Override public void before(Request request, Response response) throws IOException { @@ -64,9 +64,28 @@ public class Router extends Processor{ resp.getEncoder().writeln("could not resolve path:"+path); } } + /** Lookup of endpoints by full routing string. + * that includes verb. + * @param r routing path + * @return endpoint matching path + */ public EndPoint getRoute(String r){ return routes.get(r); } + /** Lookup of endpoint by method name or part of it. + * matches if endpoint id endswith method_name. + * matches case insensitively. + * @param method_name ending part of a method + * @return matched endpoint + */ + public EndPoint getRouteByMethod(String method_name){ + method_name=method_name.toLowerCase(); + for(EndPoint ep:routes.values()){ + String nm=ep.getId().toLowerCase(); + if(nm.endsWith(method_name)) return ep; + } + return null; + } public void addRoute(String verb,String path, EndPoint mm) { RouteDetector det=new RouteDetector(verb,path); detectors.add(det); diff --git a/src/main/java/com/reliancy/jabba/StatusMod.java b/src/main/java/com/reliancy/jabba/StatusMod.java new file mode 100644 index 0000000..a014770 --- /dev/null +++ b/src/main/java/com/reliancy/jabba/StatusMod.java @@ -0,0 +1,25 @@ +package com.reliancy.jabba; + +import java.io.IOException; + +public class StatusMod implements AppModule{ + @Override + public void publish(App app) { + app.publishModule(this,getClass().getSimpleName()); + app.getRouter().importMethods(this); + } + @Routed( + path="/status" + ) + public void status(Request req,Response resp) throws IOException{ + StringBuilder buf=new StringBuilder(); + if(buf!=null) throw new IOException("bummer!!!"); + buf.append("Hi there!!!\n"); + buf.append("mount:").append(req.getMount()).append("\n"); + buf.append("path:").append(req.getPath()).append("\n"); + buf.append("remote:").append(req.getRemoteAddress()).append("\n"); + buf.append("protocol:").append(req.getProtocol()).append("\n"); + resp.getEncoder().writeString(buf); + } + +} diff --git a/src/main/java/com/reliancy/rec/JSONEncoder.java b/src/main/java/com/reliancy/rec/JSONEncoder.java index c918f1e..f239c45 100644 --- a/src/main/java/com/reliancy/rec/JSONEncoder.java +++ b/src/main/java/com/reliancy/rec/JSONEncoder.java @@ -140,7 +140,7 @@ public class JSONEncoder{ // 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); + str = escape(str).toString(); if (o != null) { o.append('"'); } @@ -247,48 +247,48 @@ public class JSONEncoder{ * @param str input string * @return output after encoding special chars */ - public static String escape(String str) { + 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.substring(0,i)); + if(buf==null) buf=new StringBuilder(str.subSequence(0,i)); buf.append("\\\""); break; case '\\': - if(buf==null) buf=new StringBuilder(str.substring(0,i)); + if(buf==null) buf=new StringBuilder(str.subSequence(0,i)); buf.append("\\\\"); break; case '/': - if(buf==null) buf=new StringBuilder(str.substring(0,i)); + if(buf==null) buf=new StringBuilder(str.subSequence(0,i)); buf.append("\\/"); break; case '\b': - if(buf==null) buf=new StringBuilder(str.substring(0,i)); + if(buf==null) buf=new StringBuilder(str.subSequence(0,i)); buf.append("\\b"); break; case '\f': - if(buf==null) buf=new StringBuilder(str.substring(0,i)); + if(buf==null) buf=new StringBuilder(str.subSequence(0,i)); buf.append("\\f"); break; case '\n': - if(buf==null) buf=new StringBuilder(str.substring(0,i)); + if(buf==null) buf=new StringBuilder(str.subSequence(0,i)); buf.append("\\n"); break; case '\r': - if(buf==null) buf=new StringBuilder(str.substring(0,i)); + if(buf==null) buf=new StringBuilder(str.subSequence(0,i)); buf.append("\\r"); break; case '\t': - if(buf==null) buf=new StringBuilder(str.substring(0,i)); + 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.toString():str; + return buf!=null?buf:str; } } diff --git a/src/main/java/com/reliancy/util/CodeException.java b/src/main/java/com/reliancy/util/CodeException.java index 9b2a257..a63d310 100644 --- a/src/main/java/com/reliancy/util/CodeException.java +++ b/src/main/java/com/reliancy/util/CodeException.java @@ -85,7 +85,7 @@ public class CodeException extends RuntimeException { } public static Throwable fillUserMessage(Throwable ex,StringBuilder msg,StringBuilder title) { Throwable c = ex; - System.out.println(">>>"+c+"/"+c.getCause()); + //System.out.println(">>>"+c+"/"+c.getCause()); while(c.getCause()!=null){ Throwable cc= c.getCause(); if(c.getMessage()==null){ @@ -93,19 +93,19 @@ public class CodeException extends RuntimeException { } String cMsg=c.getMessage(); String ccMsg=cc.getMessage(); - System.out.println("!!!"+cMsg+"/"+c.getClass().getName()+"/"+cc.getClass().getName()); + //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()); + //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); + //System.out.println("CC:"+c); // take care of title String _title=c.getClass().getSimpleName(); if(c instanceof CodeException){ diff --git a/src/test/java/com/reliancy/jabba/DemoApp.java b/src/test/java/com/reliancy/jabba/DemoApp.java index d556e52..8113a64 100644 --- a/src/test/java/com/reliancy/jabba/DemoApp.java +++ b/src/test/java/com/reliancy/jabba/DemoApp.java @@ -41,6 +41,7 @@ public class DemoApp extends JettyApp implements AppModule{ Resources.appendSearch(0,cls); String work_dir=ArgsConfig.APP_WORKDIR.get(conf); if(work_dir!=null) Resources.appendSearch(0,work_dir); + log().info("work_dir:{}",work_dir); //for(Object p:Resources.search_path){ // System.out.println("sp:"+p); //} @@ -52,7 +53,7 @@ public class DemoApp extends JettyApp implements AppModule{ app.setSecurityPolicy(secpol); // install router app.setRouter(new Router()); - DemoEP ep=new DemoEP(); + StatusMod ep=new StatusMod(); ep.publish(app); // install file sever endpoint FileServer fs=new FileServer("/static","/public"); @@ -60,6 +61,8 @@ public class DemoApp extends JettyApp implements AppModule{ Menu top_menu=Menu.request(Menu.TOP); top_menu.add(new MenuItem("home")).addSpacer().add(new MenuItem("login")); top_menu.setTitle("Jabba3"); + app.getRouter().compile(); + System.out.println(app.getRouter().regex); } @Override public void publish(App app) { @@ -120,6 +123,7 @@ public class DemoApp extends JettyApp implements AppModule{ @Routed public void login(com.reliancy.jabba.Request req,Response resp){ //return "login form here"; + log().info("login here"); if(req.getVerb().equals("POST")){ // here we need to process login and redirect AppSession ass=AppSession.getInstance();