From 7f5c20ced14661762a396732487125d6d5bb8e49 Mon Sep 17 00:00:00 2001 From: ajohnston Date: Wed, 15 Apr 2020 11:18:39 -0700 Subject: [PATCH 01/12] Add module for legacy dispatching classes --- gemini-legacy-dispatching/pom.xml | 40 +++++++++++++++++++++++++++++++ pom.xml | 1 + 2 files changed, 41 insertions(+) create mode 100644 gemini-legacy-dispatching/pom.xml diff --git a/gemini-legacy-dispatching/pom.xml b/gemini-legacy-dispatching/pom.xml new file mode 100644 index 00000000..304d6628 --- /dev/null +++ b/gemini-legacy-dispatching/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + jar + + + TechEmpower, Inc. + https://www.techempower.com/ + + + + + Revised BSD License, 3-clause + repo + + + + + gemini-parent + com.techempower + 3.1.0-SNAPSHOT + + + com.techempower + gemini-legacy-dispatching + + An extension for Gemini that provides the old request dispatching functionality. + + + + + com.techempower + gemini + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 454d05d0..02bd9f3f 100755 --- a/pom.xml +++ b/pom.xml @@ -48,6 +48,7 @@ gemini-log4j2 gemini-logback gemini-log4j12 + gemini-legacy-dispatching From f5e38e283476a984b95692e4fcf1702feed0cd29 Mon Sep 17 00:00:00 2001 From: ajohnston Date: Wed, 15 Apr 2020 13:01:31 -0700 Subject: [PATCH 02/12] Add module for legacy resin classes and move relevant classes to it --- gemini-resin-legacy-dispatching/pom.xml | 63 +++++++++++++++++++ .../techempower/gemini/BasicDispatcher.java | 0 .../java/com/techempower/gemini/Handler.java | 0 .../com/techempower/gemini/LegacyContext.java | 0 .../gemini/annotation/AnnotationHandler.java | 0 .../techempower/gemini/annotation/CMD.java | 0 .../gemini/annotation/Default.java | 0 .../techempower/gemini/annotation/Role.java | 0 .../techempower/gemini/annotation/URL.java | 0 .../annotation/injector/BooleanParam.java | 0 .../injector/BooleanParamInjector.java | 0 .../annotation/injector/DoubleParam.java | 0 .../injector/DoubleParamInjector.java | 0 .../gemini/annotation/injector/Entity.java | 0 .../annotation/injector/EntityInjector.java | 0 .../gemini/annotation/injector/Injector.java | 0 .../gemini/annotation/injector/IntParam.java | 0 .../annotation/injector/IntParamInjector.java | 0 .../gemini/annotation/injector/LongParam.java | 0 .../injector/LongParamInjector.java | 0 .../gemini/annotation/injector/Param.java | 0 .../annotation/injector/ParamInjector.java | 0 .../injector/ParameterInjector.java | 0 .../annotation/injector/package-info.java | 0 .../intercept/FeatureIntercept.java | 0 .../annotation/intercept/GetIntercept.java | 0 .../annotation/intercept/GroupIntercept.java | 0 .../intercept/HandlerIntercept.java | 0 .../annotation/intercept/Intercept.java | 0 .../annotation/intercept/LoginIntercept.java | 0 .../annotation/intercept/PostIntercept.java | 0 .../gemini/annotation/intercept/Require.java | 0 .../annotation/intercept/RequireFeature.java | 0 .../annotation/intercept/RequireGet.java | 0 .../annotation/intercept/RequireGroup.java | 0 .../annotation/intercept/RequireLogin.java | 0 .../annotation/intercept/RequirePost.java | 0 .../annotation/intercept/package-info.java | 0 .../gemini/annotation/package-info.java | 0 .../annotation/response/FileResponse.java | 0 .../annotation/response/HandlerResponse.java | 0 .../gemini/annotation/response/JSON.java | 0 .../gemini/annotation/response/JSP.java | 0 .../annotation/response/JsonResponse.java | 0 .../annotation/response/JspResponse.java | 0 .../gemini/annotation/response/Response.java | 0 .../gemini/annotation/response/TossFile.java | 0 .../annotation/response/package-info.java | 0 .../BasicExceptionHandler.java | 0 .../filestore/BasicFileStoreHandler.java | 0 .../gemini/handler/BasicHandler.java | 0 .../gemini/handler/FileTossHandler.java | 0 .../gemini/handler/SecureHandler.java | 0 .../com/techempower/gemini/jsp/BasicJsp.java | 0 .../gemini/jsp/InfrastructureJsp.java | 0 .../gemini/pyxis/BasicSecureHandler.java | 0 .../gemini/ResinGeminiApplication.java | 10 +-- pom.xml | 11 ++++ 58 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 gemini-resin-legacy-dispatching/pom.xml rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/BasicDispatcher.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/Handler.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/LegacyContext.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/AnnotationHandler.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/CMD.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/Default.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/Role.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/URL.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/injector/BooleanParam.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/injector/BooleanParamInjector.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/injector/DoubleParam.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/injector/DoubleParamInjector.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/injector/Entity.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/injector/EntityInjector.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/injector/Injector.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/injector/IntParam.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/injector/IntParamInjector.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/injector/LongParam.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/injector/LongParamInjector.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/injector/Param.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/injector/ParamInjector.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/injector/ParameterInjector.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/injector/package-info.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/intercept/FeatureIntercept.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/intercept/GetIntercept.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/intercept/GroupIntercept.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/intercept/HandlerIntercept.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/intercept/Intercept.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/intercept/LoginIntercept.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/intercept/PostIntercept.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/intercept/Require.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/intercept/RequireFeature.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/intercept/RequireGet.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/intercept/RequireGroup.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/intercept/RequireLogin.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/intercept/RequirePost.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/intercept/package-info.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/package-info.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/response/FileResponse.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/response/HandlerResponse.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/response/JSON.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/response/JSP.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/response/JsonResponse.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/response/JspResponse.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/response/Response.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/response/TossFile.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/annotation/response/package-info.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/exceptionhandler/BasicExceptionHandler.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/filestore/BasicFileStoreHandler.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/handler/BasicHandler.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/handler/FileTossHandler.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/handler/SecureHandler.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/jsp/BasicJsp.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/jsp/InfrastructureJsp.java (100%) mode change 100755 => 100644 rename {gemini-resin => gemini-resin-legacy-dispatching}/src/main/java/com/techempower/gemini/pyxis/BasicSecureHandler.java (100%) mode change 100755 => 100644 diff --git a/gemini-resin-legacy-dispatching/pom.xml b/gemini-resin-legacy-dispatching/pom.xml new file mode 100644 index 00000000..1073a036 --- /dev/null +++ b/gemini-resin-legacy-dispatching/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + jar + + + TechEmpower, Inc. + https://www.techempower.com/ + + + + + Revised BSD License, 3-clause + repo + + + + + gemini-parent + com.techempower + 3.1.0-SNAPSHOT + + + com.techempower + gemini-resin-legacy-dispatching + + An extension for Gemini that provides the old request dispatching functionality for Resin. + + + + + com.techempower + gemini + + + com.techempower + gemini-resin + + + com.techempower + gemini-legacy-dispatching + + + com.caucho + resin + + + javax.servlet + javax.servlet-api + + + javax + javaee-api + + + javax + javaee-web-api + + + + \ No newline at end of file diff --git a/gemini-resin/src/main/java/com/techempower/gemini/BasicDispatcher.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/BasicDispatcher.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/BasicDispatcher.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/BasicDispatcher.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/Handler.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/Handler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/Handler.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/Handler.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/LegacyContext.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/LegacyContext.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/LegacyContext.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/LegacyContext.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/AnnotationHandler.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/AnnotationHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/AnnotationHandler.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/AnnotationHandler.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/CMD.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/CMD.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/CMD.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/CMD.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/Default.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/Default.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/Default.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/Default.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/Role.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/Role.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/Role.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/Role.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/URL.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/URL.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/URL.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/URL.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/BooleanParam.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/BooleanParam.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/BooleanParam.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/BooleanParam.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/BooleanParamInjector.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/BooleanParamInjector.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/BooleanParamInjector.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/BooleanParamInjector.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/DoubleParam.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/DoubleParam.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/DoubleParam.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/DoubleParam.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/DoubleParamInjector.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/DoubleParamInjector.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/DoubleParamInjector.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/DoubleParamInjector.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/Entity.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/Entity.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/Entity.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/Entity.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/EntityInjector.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/EntityInjector.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/EntityInjector.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/EntityInjector.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/Injector.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/Injector.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/Injector.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/Injector.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/IntParam.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/IntParam.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/IntParam.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/IntParam.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/IntParamInjector.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/IntParamInjector.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/IntParamInjector.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/IntParamInjector.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/LongParam.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/LongParam.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/LongParam.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/LongParam.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/LongParamInjector.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/LongParamInjector.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/LongParamInjector.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/LongParamInjector.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/Param.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/Param.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/Param.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/Param.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/ParamInjector.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/ParamInjector.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/ParamInjector.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/ParamInjector.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/ParameterInjector.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/ParameterInjector.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/ParameterInjector.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/ParameterInjector.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/package-info.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/package-info.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/package-info.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/package-info.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/FeatureIntercept.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/FeatureIntercept.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/FeatureIntercept.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/FeatureIntercept.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/GetIntercept.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/GetIntercept.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/GetIntercept.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/GetIntercept.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/GroupIntercept.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/GroupIntercept.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/GroupIntercept.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/GroupIntercept.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/HandlerIntercept.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/HandlerIntercept.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/HandlerIntercept.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/HandlerIntercept.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/Intercept.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/Intercept.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/Intercept.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/Intercept.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/LoginIntercept.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/LoginIntercept.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/LoginIntercept.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/LoginIntercept.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/PostIntercept.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/PostIntercept.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/PostIntercept.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/PostIntercept.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/Require.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/Require.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/Require.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/Require.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/RequireFeature.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/RequireFeature.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/RequireFeature.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/RequireFeature.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/RequireGet.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/RequireGet.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/RequireGet.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/RequireGet.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/RequireGroup.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/RequireGroup.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/RequireGroup.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/RequireGroup.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/RequireLogin.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/RequireLogin.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/RequireLogin.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/RequireLogin.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/RequirePost.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/RequirePost.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/RequirePost.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/RequirePost.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/package-info.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/package-info.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/package-info.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/package-info.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/package-info.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/package-info.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/package-info.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/package-info.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/response/FileResponse.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/FileResponse.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/response/FileResponse.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/FileResponse.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/response/HandlerResponse.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/HandlerResponse.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/response/HandlerResponse.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/HandlerResponse.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/response/JSON.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/JSON.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/response/JSON.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/JSON.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/response/JSP.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/JSP.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/response/JSP.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/JSP.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/response/JsonResponse.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/JsonResponse.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/response/JsonResponse.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/JsonResponse.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/response/JspResponse.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/JspResponse.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/response/JspResponse.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/JspResponse.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/response/Response.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/Response.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/response/Response.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/Response.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/response/TossFile.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/TossFile.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/response/TossFile.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/TossFile.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/response/package-info.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/package-info.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/response/package-info.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/package-info.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/exceptionhandler/BasicExceptionHandler.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/exceptionhandler/BasicExceptionHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/exceptionhandler/BasicExceptionHandler.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/exceptionhandler/BasicExceptionHandler.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/filestore/BasicFileStoreHandler.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/filestore/BasicFileStoreHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/filestore/BasicFileStoreHandler.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/filestore/BasicFileStoreHandler.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/handler/BasicHandler.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/handler/BasicHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/handler/BasicHandler.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/handler/BasicHandler.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/handler/FileTossHandler.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/handler/FileTossHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/handler/FileTossHandler.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/handler/FileTossHandler.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/handler/SecureHandler.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/handler/SecureHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/handler/SecureHandler.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/handler/SecureHandler.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/jsp/BasicJsp.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/jsp/BasicJsp.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/jsp/BasicJsp.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/jsp/BasicJsp.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/jsp/InfrastructureJsp.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/jsp/InfrastructureJsp.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/jsp/InfrastructureJsp.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/jsp/InfrastructureJsp.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/pyxis/BasicSecureHandler.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/BasicSecureHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/pyxis/BasicSecureHandler.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/BasicSecureHandler.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/ResinGeminiApplication.java b/gemini-resin/src/main/java/com/techempower/gemini/ResinGeminiApplication.java index 4ff38c7b..f67fe2cd 100755 --- a/gemini-resin/src/main/java/com/techempower/gemini/ResinGeminiApplication.java +++ b/gemini-resin/src/main/java/com/techempower/gemini/ResinGeminiApplication.java @@ -95,17 +95,17 @@ public abstract class ResinGeminiApplication * return toReturn; * */ + // TODO: It'd be nice if this was refactored so that you create the + // dispatcher during the initialize method, rather than in the constructor. @Override - protected Dispatcher constructDispatcher() - { - return new BasicDispatcher(this); - } + protected abstract Dispatcher constructDispatcher(); /** * Overload: Constructs an HttpSessionManager reference. Overload to return a * custom object. It is not likely that a application would need to subclass * HttpSessionManager. */ + // TODO?: Need to refactor this so that it's just part of the (legacy?) dispatcher? @Override protected SessionManager constructSessionManager() { @@ -127,12 +127,14 @@ protected GeminiMonitor constructMonitor() * LONGER necessary to overload this method if your application is not using * a special subclass of Context. */ + // TODO: Need to refactor this so that it's just part of the (legacy?) dispatcher. @Override public Context getContext(Request request) { return new ResinContext(request, this); } + // TODO: Need to refactor this so that it's just part of the legacy dispatcher. @Override protected MustacheManager constructMustacheManager() { diff --git a/pom.xml b/pom.xml index 02bd9f3f..c1df88c2 100755 --- a/pom.xml +++ b/pom.xml @@ -49,6 +49,7 @@ gemini-logback gemini-log4j12 gemini-legacy-dispatching + gemini-resin-legacy-dispatching @@ -138,6 +139,16 @@ gemini-hikaricp ${project.version} + + ${project.groupId} + gemini-legacy-dispatching + ${project.version} + + + ${project.groupId} + gemini-resin-legacy-dispatching + ${project.version} + com.fasterxml.jackson.core jackson-core From c944947a38fa16ed779a400ecef5b90df7f021fe Mon Sep 17 00:00:00 2001 From: ajohnston Date: Wed, 15 Apr 2020 14:52:01 -0700 Subject: [PATCH 03/12] Move legacy core dispatching classes to legacy dispatching module --- .../gemini/exceptionhandler/EmailExceptionHandler.java | 0 .../gemini/exceptionhandler/ExceptionHandler.java | 0 .../exceptionhandler/NotificationExceptionHandler.java | 0 .../techempower/gemini/handler/ThreadDumpHandler.java | 0 .../com/techempower/gemini/handler/package-info.java | 0 .../com/techempower/gemini/path/BasicPathHandler.java | 0 .../com/techempower/gemini/path/BasicPathManager.java | 0 .../com/techempower/gemini/path/DispatchLogger.java | 0 .../com/techempower/gemini/path/DispatchSegment.java | 0 .../techempower/gemini/path/FourZeroFourHandler.java | 0 .../gemini/path/JsonRequestBodyAdapter.java | 0 .../techempower/gemini/path/MethodSegmentHandler.java | 0 .../com/techempower/gemini/path/MethodUriHandler.java | 0 .../techempower/gemini/path/NotImplementedHandler.java | 0 .../com/techempower/gemini/path/PathDispatcher.java | 0 .../java/com/techempower/gemini/path/PathHandler.java | 0 .../java/com/techempower/gemini/path/PathSegments.java | 0 .../techempower/gemini/path/RequestBodyAdapter.java | 0 .../techempower/gemini/path/RequestBodyException.java | 0 .../com/techempower/gemini/path/RequestReferences.java | 0 .../gemini/path/StringRequestBodyAdapter.java | 0 .../java/com/techempower/gemini/path/UriAware.java | 0 .../com/techempower/gemini/path/annotation/Body.java | 0 .../gemini/path/annotation/ConsumesJson.java | 0 .../gemini/path/annotation/ConsumesString.java | 0 .../com/techempower/gemini/path/annotation/Delete.java | 0 .../com/techempower/gemini/path/annotation/Get.java | 0 .../com/techempower/gemini/path/annotation/Path.java | 0 .../gemini/path/annotation/PathDefault.java | 0 .../techempower/gemini/path/annotation/PathRoot.java | 0 .../gemini/path/annotation/PathSegment.java | 0 .../com/techempower/gemini/path/annotation/Post.java | 0 .../com/techempower/gemini/path/annotation/Put.java | 0 .../gemini/path/annotation/package-info.java | 0 .../gemini/path/legacy/LegacyDispatcherHandler.java | 0 .../techempower/gemini/path/legacy/package-info.java | 0 .../java/com/techempower/gemini/path/package-info.java | 0 .../com/techempower/gemini/prehandler/Prehandler.java | 0 .../gemini/prehandler/StrictTransportSecurity.java | 0 .../techempower/gemini/prehandler/package-info.java | 0 .../gemini/pyxis/annotation/PathBypassAuth.java | 0 .../gemini/pyxis/handler/EndMasqueradeHandler.java | 0 .../techempower/gemini/pyxis/handler/LoginHandler.java | 0 .../gemini/pyxis/handler/LogoutHandler.java | 0 .../gemini/pyxis/handler/PasswordResetHandler.java | 0 .../gemini/pyxis/handler/PyxisHandlerHelper.java | 0 .../pyxis/handler/SecureMethodSegmentHandler.java | 0 .../gemini/pyxis/handler/SecureMethodUriHandler.java | 0 .../java/com/techempower/gemini/seo/RobotsHandler.java | 0 .../src/main/resources/archetype-resources/pom.xml | 10 ++++++++++ .../java/com/techempower/gemini/DispatchListener.java | 3 +++ .../com/techempower/gemini/manager/BasicManager.java | 5 ++++- 52 files changed, 17 insertions(+), 1 deletion(-) rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/exceptionhandler/EmailExceptionHandler.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/exceptionhandler/ExceptionHandler.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/exceptionhandler/NotificationExceptionHandler.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/handler/ThreadDumpHandler.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/handler/package-info.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/BasicPathHandler.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/BasicPathManager.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/DispatchLogger.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/DispatchSegment.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/FourZeroFourHandler.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/JsonRequestBodyAdapter.java (100%) rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/MethodSegmentHandler.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/MethodUriHandler.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/NotImplementedHandler.java (100%) rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/PathDispatcher.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/PathHandler.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/PathSegments.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/RequestBodyAdapter.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/RequestBodyException.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/RequestReferences.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/StringRequestBodyAdapter.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/UriAware.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/annotation/Body.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/annotation/ConsumesJson.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/annotation/ConsumesString.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/annotation/Delete.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/annotation/Get.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/annotation/Path.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/annotation/PathDefault.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/annotation/PathRoot.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/annotation/PathSegment.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/annotation/Post.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/annotation/Put.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/annotation/package-info.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/legacy/LegacyDispatcherHandler.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/legacy/package-info.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/path/package-info.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/prehandler/Prehandler.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/prehandler/StrictTransportSecurity.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/prehandler/package-info.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/pyxis/annotation/PathBypassAuth.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/pyxis/handler/EndMasqueradeHandler.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/pyxis/handler/LoginHandler.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/pyxis/handler/LogoutHandler.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/pyxis/handler/PasswordResetHandler.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/pyxis/handler/PyxisHandlerHelper.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/pyxis/handler/SecureMethodSegmentHandler.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/pyxis/handler/SecureMethodUriHandler.java (100%) mode change 100755 => 100644 rename {gemini => gemini-legacy-dispatching}/src/main/java/com/techempower/gemini/seo/RobotsHandler.java (100%) mode change 100755 => 100644 diff --git a/gemini/src/main/java/com/techempower/gemini/exceptionhandler/EmailExceptionHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/exceptionhandler/EmailExceptionHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/exceptionhandler/EmailExceptionHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/exceptionhandler/EmailExceptionHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/exceptionhandler/ExceptionHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/exceptionhandler/ExceptionHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/exceptionhandler/ExceptionHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/exceptionhandler/ExceptionHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/exceptionhandler/NotificationExceptionHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/exceptionhandler/NotificationExceptionHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/exceptionhandler/NotificationExceptionHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/exceptionhandler/NotificationExceptionHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/handler/ThreadDumpHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/handler/ThreadDumpHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/handler/ThreadDumpHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/handler/ThreadDumpHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/handler/package-info.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/handler/package-info.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/handler/package-info.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/handler/package-info.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/BasicPathHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/BasicPathHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/BasicPathHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/BasicPathHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/BasicPathManager.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/BasicPathManager.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/BasicPathManager.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/BasicPathManager.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/DispatchLogger.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/DispatchLogger.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/DispatchLogger.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/DispatchLogger.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/DispatchSegment.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/DispatchSegment.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/DispatchSegment.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/DispatchSegment.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/FourZeroFourHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/FourZeroFourHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/FourZeroFourHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/FourZeroFourHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/JsonRequestBodyAdapter.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/JsonRequestBodyAdapter.java similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/JsonRequestBodyAdapter.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/JsonRequestBodyAdapter.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/MethodSegmentHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/MethodSegmentHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/MethodSegmentHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/MethodSegmentHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/MethodUriHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/MethodUriHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/MethodUriHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/MethodUriHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/NotImplementedHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/NotImplementedHandler.java similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/NotImplementedHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/NotImplementedHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/PathDispatcher.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/PathDispatcher.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/PathDispatcher.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/PathDispatcher.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/PathHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/PathHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/PathHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/PathHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/PathSegments.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/PathSegments.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/PathSegments.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/PathSegments.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/RequestBodyAdapter.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/RequestBodyAdapter.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/RequestBodyAdapter.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/RequestBodyAdapter.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/RequestBodyException.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/RequestBodyException.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/RequestBodyException.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/RequestBodyException.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/RequestReferences.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/RequestReferences.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/RequestReferences.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/RequestReferences.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/StringRequestBodyAdapter.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/StringRequestBodyAdapter.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/StringRequestBodyAdapter.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/StringRequestBodyAdapter.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/UriAware.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/UriAware.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/UriAware.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/UriAware.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/Body.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Body.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/Body.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Body.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/ConsumesJson.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/ConsumesJson.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/ConsumesJson.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/ConsumesJson.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/ConsumesString.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/ConsumesString.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/ConsumesString.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/ConsumesString.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/Delete.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Delete.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/Delete.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Delete.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/Get.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Get.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/Get.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Get.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/Path.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Path.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/Path.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Path.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/PathDefault.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/PathDefault.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/PathDefault.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/PathDefault.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/PathRoot.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/PathRoot.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/PathRoot.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/PathRoot.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/PathSegment.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/PathSegment.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/PathSegment.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/PathSegment.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/Post.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Post.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/Post.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Post.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/Put.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Put.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/Put.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Put.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/package-info.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/package-info.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/package-info.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/package-info.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/legacy/LegacyDispatcherHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/legacy/LegacyDispatcherHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/legacy/LegacyDispatcherHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/legacy/LegacyDispatcherHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/legacy/package-info.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/legacy/package-info.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/legacy/package-info.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/legacy/package-info.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/package-info.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/package-info.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/package-info.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/package-info.java diff --git a/gemini/src/main/java/com/techempower/gemini/prehandler/Prehandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/prehandler/Prehandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/prehandler/Prehandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/prehandler/Prehandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/prehandler/StrictTransportSecurity.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/prehandler/StrictTransportSecurity.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/prehandler/StrictTransportSecurity.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/prehandler/StrictTransportSecurity.java diff --git a/gemini/src/main/java/com/techempower/gemini/prehandler/package-info.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/prehandler/package-info.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/prehandler/package-info.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/prehandler/package-info.java diff --git a/gemini/src/main/java/com/techempower/gemini/pyxis/annotation/PathBypassAuth.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/annotation/PathBypassAuth.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/pyxis/annotation/PathBypassAuth.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/annotation/PathBypassAuth.java diff --git a/gemini/src/main/java/com/techempower/gemini/pyxis/handler/EndMasqueradeHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/EndMasqueradeHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/pyxis/handler/EndMasqueradeHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/EndMasqueradeHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/pyxis/handler/LoginHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/LoginHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/pyxis/handler/LoginHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/LoginHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/pyxis/handler/LogoutHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/LogoutHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/pyxis/handler/LogoutHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/LogoutHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/pyxis/handler/PasswordResetHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/PasswordResetHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/pyxis/handler/PasswordResetHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/PasswordResetHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/pyxis/handler/PyxisHandlerHelper.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/PyxisHandlerHelper.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/pyxis/handler/PyxisHandlerHelper.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/PyxisHandlerHelper.java diff --git a/gemini/src/main/java/com/techempower/gemini/pyxis/handler/SecureMethodSegmentHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/SecureMethodSegmentHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/pyxis/handler/SecureMethodSegmentHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/SecureMethodSegmentHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/pyxis/handler/SecureMethodUriHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/SecureMethodUriHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/pyxis/handler/SecureMethodUriHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/SecureMethodUriHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/seo/RobotsHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/seo/RobotsHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/seo/RobotsHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/seo/RobotsHandler.java diff --git a/gemini-resin-archetype/src/main/resources/archetype-resources/pom.xml b/gemini-resin-archetype/src/main/resources/archetype-resources/pom.xml index 116b893c..945beec6 100755 --- a/gemini-resin-archetype/src/main/resources/archetype-resources/pom.xml +++ b/gemini-resin-archetype/src/main/resources/archetype-resources/pom.xml @@ -46,6 +46,16 @@ gemini-logback ${geminiVersion} + + com.techempower + gemini-legacy-dispatching + ${geminiVersion} + + + com.techempower + gemini-resin-legacy-dispatching + ${geminiVersion} + com.techempower gemini-jdbc diff --git a/gemini/src/main/java/com/techempower/gemini/DispatchListener.java b/gemini/src/main/java/com/techempower/gemini/DispatchListener.java index 806dcb92..808ce0ed 100755 --- a/gemini/src/main/java/com/techempower/gemini/DispatchListener.java +++ b/gemini/src/main/java/com/techempower/gemini/DispatchListener.java @@ -30,6 +30,9 @@ /** * An interface allowing classes to monitor Dispatcher activity. */ +// TODO?: Move this to gemini-legacy-dispatching. Might be hard, transitively +// is depended on by gemini-jdbc's JdbcMonitorListener among many other things +// in gemini, gemini-resin, etc. public interface DispatchListener { diff --git a/gemini/src/main/java/com/techempower/gemini/manager/BasicManager.java b/gemini/src/main/java/com/techempower/gemini/manager/BasicManager.java index 663e792b..22753c7d 100755 --- a/gemini/src/main/java/com/techempower/gemini/manager/BasicManager.java +++ b/gemini/src/main/java/com/techempower/gemini/manager/BasicManager.java @@ -44,7 +44,10 @@ *

* It is common for applications to inherit from an intermediate subclass of * BasicManager such as BasicPathManager. - * + * + * TODO?: BasicPathManager is no longer in this Maven module. Will this be a + * problem for javadoc compilation? + * * @see com.techempower.gemini.path.BasicPathManager */ public class BasicManager From c7fb0e567edba61b77c9b920891d6687dd155136 Mon Sep 17 00:00:00 2001 From: ajohnston Date: Wed, 15 Apr 2020 18:03:57 -0700 Subject: [PATCH 04/12] Add gemini-jax-rs module and explanation to self on how to handle path dispatching --- gemini-jax-rs/pom.xml | 48 ++++++++++ .../gemini/jaxrs/core/JaxRsDispatcher.java | 95 +++++++++++++++++++ .../jaxrs/core/JaxRsDispatcherTest.java | 25 +++++ pom.xml | 7 ++ 4 files changed, 175 insertions(+) create mode 100644 gemini-jax-rs/pom.xml create mode 100644 gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java create mode 100644 gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java diff --git a/gemini-jax-rs/pom.xml b/gemini-jax-rs/pom.xml new file mode 100644 index 00000000..1161445b --- /dev/null +++ b/gemini-jax-rs/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + jar + + + TechEmpower, Inc. + https://www.techempower.com/ + + + + + Revised BSD License, 3-clause + repo + + + + + gemini-parent + com.techempower + 3.1.0-SNAPSHOT + + + com.techempower + gemini-jax-rs + + An extension for Gemini that provides dispatching implemented as a subset of JAX-RS. + + + + + com.techempower + gemini + + + jakarta.ws.rs + jakarta.ws.rs-api + + + junit + junit + test + + + + \ No newline at end of file diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java new file mode 100644 index 00000000..6279f786 --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java @@ -0,0 +1,95 @@ +package com.techempower.gemini.jaxrs.core; + +import com.techempower.gemini.Context; + +import java.util.List; + +public class JaxRsDispatcher +{ + private List resources; + + public void register(Object instance) { + + } + + // TODO: Not use Gemini's Context eventually + public void dispatch(Context context) { + // Steps: + // 1. Find the target resource + // 2. Determine what is "in front" of that resource + // 3. Dispatch to those things + // 4. Dispatch to the resource if necessary + // 5. Handle return value of resource if response not already complete + // based on annotations of the target resource. + + + // To find the target resource: + // - Regular Expressions are supported in the path variables + // - By default, those path variables can be simplified as index-based, + // because the non-regex form is equivalent to splitting the URI by the + // slashes then using the relative index. + + + // Approach: + // At startup, tokenize all the endpoints from any resource. + // To do so, break up every endpoint path into the tokens: + // PATH := SLASH? ( BLOCK SLASH? )* + // SLASH := "/" + // BLOCK := WORD | VARIABLE + // WORD := [^/]+ + // VARIABLE := "{" \s* NAME \s* ( ":" \s* REGEX \s* )? "}" + // NAME := \w[\w\.-]* + // REGEX := ( [^{}] | "{" [^{}]* "}" )* + // + // Place each BLOCK into a block class. There should be "word" blocks, + // "pure variable" blocks, and "regex variable" blocks. "word" blocks match + // a static word, "variable" blocks match a variable that does not have a + // regex, and "regex variable" blocks match variables with a regex. Each + // block should be given the information relevant to it, as well as its + // children. + // + // To dispatch a URI: + // 1) Consider the current block to be a general "root" block. + // 2) Split the URI by "/". + // 3) Consider the current index to be 0. + // 4) At the current index: + // 4..1) For each of the following, if the matching block is a + // method, also check to see if the block matches the relevant + // http method. + // 4.1) Check if there is a matching "word" block. If so, move to it + // and continue to step 5. + // 4.2) Check if there is a "pure variable" child block. If so, move to + // it and continue to step 5. + // 4.3) Check if there is a matching "regex variable" child block. To + // do so: + // 4.3.1) Join the path URI segments at and after the current + // index then apply the regex to that. + // 4.3.2) If it matches, remove the first matching group from the + // front of the joined subset of the URI, then restart from + // step 2 starting from this block with the new sub-URI as + // the URI mentioned in that step. + // 4.4) If no match has been found, this block does have not have a + // match. + // 5) If the current index not the last in the split URI, increment the + // current index by 1, consider the block from step 4 as the current + // block, and return to step 4. If the current index is the last in the + // split URI, check if the current block represents a method. If it + // does, the method is the method to dispatch to, using the chain of + // blocks used to find this method. If it does not, the current block + // does not have a match. + // 6) If a method was found, use the chain of blocks used to find the + // method to populate the path params (if any) required by the method + // and any interceptors. + + + // Once a method has been found, determine the interceptors relevant to it, + // which likely should be stored in a map by each handler instance/class, + // and run each of those in the appropriate order. If the handler method + // should still be dispatched to, dispatch to it. The parameters the + // handler requires should also be cached, likely in a class that wraps the + // method and stores all information relevant about how to both call it and + // handle the return value. Check for thrown exceptions and if any are + // present then apply any necessary ExceptionMappers. Then check to see if + // the response has already been sent before attempting to handle it. + } +} diff --git a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java new file mode 100644 index 00000000..6feefdf0 --- /dev/null +++ b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java @@ -0,0 +1,25 @@ +package com.techempower.gemini.jaxrs.core; + +import org.junit.Test; + +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.core.Response; + +import static org.junit.Assert.*; + +public class JaxRsDispatcherTest +{ + @Path("/foo/{dog: .+}{cat: .+}") + public static class Foo { + @Path("/bar") + public Response doIt(@PathParam("dog") String dog) { + return Response.ok().build(); + } + } + + @Test + public void testIt() { + + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index c1df88c2..5edb7e64 100755 --- a/pom.xml +++ b/pom.xml @@ -48,6 +48,7 @@ gemini-log4j2 gemini-logback gemini-log4j12 + gemini-jax-rs gemini-legacy-dispatching gemini-resin-legacy-dispatching @@ -91,6 +92,7 @@ 6.0.0 2.13.0 1.3.0-alpha2 + 2.1.6 @@ -250,6 +252,11 @@ ${javaee.version} provided + + jakarta.ws.rs + jakarta.ws.rs-api + ${jax-rs.version} + io.jsonwebtoken jjwt-impl From c656971e72061361e40100b7d39046386d0a7d11 Mon Sep 17 00:00:00 2001 From: ajohnston Date: Fri, 17 Apr 2020 15:46:52 -0700 Subject: [PATCH 05/12] WIP implement dispatcher --- gemini-jax-rs/pom.xml | 5 + .../gemini/jaxrs/core/JaxRsDispatcher.java | 781 ++++++++++++++-- .../jaxrs/core/JaxRsDispatcherTest.java | 722 ++++++++++++++- .../gemini/path/annotation/Path.java | 4 +- gemini-resin-legacy-dispatching/pom.xml | 10 + .../gemini/path/AnnotationDispatcher.java | 565 ++++++++++++ .../gemini/path/AnnotationDispatcherTest.java | 126 +++ .../gemini/path/AnnotationHandler.java | 844 ++++++++++++++++++ pom.xml | 7 + 9 files changed, 2969 insertions(+), 95 deletions(-) create mode 100644 gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationDispatcher.java create mode 100644 gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationDispatcherTest.java create mode 100644 gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationHandler.java diff --git a/gemini-jax-rs/pom.xml b/gemini-jax-rs/pom.xml index 1161445b..04b76724 100644 --- a/gemini-jax-rs/pom.xml +++ b/gemini-jax-rs/pom.xml @@ -43,6 +43,11 @@ junit test + + org.mockito + mockito-all + test + \ No newline at end of file diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java index 6279f786..6c2498be 100644 --- a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java @@ -1,95 +1,702 @@ package com.techempower.gemini.jaxrs.core; -import com.techempower.gemini.Context; +import com.esotericsoftware.reflectasm.MethodAccess; -import java.util.List; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; public class JaxRsDispatcher { - private List resources; - - public void register(Object instance) { - - } - - // TODO: Not use Gemini's Context eventually - public void dispatch(Context context) { - // Steps: - // 1. Find the target resource - // 2. Determine what is "in front" of that resource - // 3. Dispatch to those things - // 4. Dispatch to the resource if necessary - // 5. Handle return value of resource if response not already complete - // based on annotations of the target resource. - - - // To find the target resource: - // - Regular Expressions are supported in the path variables - // - By default, those path variables can be simplified as index-based, - // because the non-regex form is equivalent to splitting the URI by the - // slashes then using the relative index. - - - // Approach: - // At startup, tokenize all the endpoints from any resource. - // To do so, break up every endpoint path into the tokens: - // PATH := SLASH? ( BLOCK SLASH? )* - // SLASH := "/" - // BLOCK := WORD | VARIABLE - // WORD := [^/]+ - // VARIABLE := "{" \s* NAME \s* ( ":" \s* REGEX \s* )? "}" - // NAME := \w[\w\.-]* - // REGEX := ( [^{}] | "{" [^{}]* "}" )* - // - // Place each BLOCK into a block class. There should be "word" blocks, - // "pure variable" blocks, and "regex variable" blocks. "word" blocks match - // a static word, "variable" blocks match a variable that does not have a - // regex, and "regex variable" blocks match variables with a regex. Each - // block should be given the information relevant to it, as well as its - // children. - // - // To dispatch a URI: - // 1) Consider the current block to be a general "root" block. - // 2) Split the URI by "/". - // 3) Consider the current index to be 0. - // 4) At the current index: - // 4..1) For each of the following, if the matching block is a - // method, also check to see if the block matches the relevant - // http method. - // 4.1) Check if there is a matching "word" block. If so, move to it - // and continue to step 5. - // 4.2) Check if there is a "pure variable" child block. If so, move to - // it and continue to step 5. - // 4.3) Check if there is a matching "regex variable" child block. To - // do so: - // 4.3.1) Join the path URI segments at and after the current - // index then apply the regex to that. - // 4.3.2) If it matches, remove the first matching group from the - // front of the joined subset of the URI, then restart from - // step 2 starting from this block with the new sub-URI as - // the URI mentioned in that step. - // 4.4) If no match has been found, this block does have not have a - // match. - // 5) If the current index not the last in the split URI, increment the - // current index by 1, consider the block from step 4 as the current - // block, and return to step 4. If the current index is the last in the - // split URI, check if the current block represents a method. If it - // does, the method is the method to dispatch to, using the chain of - // blocks used to find this method. If it does not, the current block - // does not have a match. - // 6) If a method was found, use the chain of blocks used to find the - // method to populate the path params (if any) required by the method - // and any interceptors. - - - // Once a method has been found, determine the interceptors relevant to it, - // which likely should be stored in a map by each handler instance/class, - // and run each of those in the appropriate order. If the handler method - // should still be dispatched to, dispatch to it. The parameters the - // handler requires should also be cached, likely in a class that wraps the - // method and stores all information relevant about how to both call it and - // handle the return value. Check for thrown exceptions and if any are - // present then apply any necessary ExceptionMappers. Then check to see if - // the response has already been sent before attempting to handle it. + private List endpoints; + DispatchBlock rootBlock = new RootDispatchBlock(); + + public void register(Object instance) + { + // TODO: Refactor to support class-based registration. Shouldn't be hard. + // TODO: Refactor to support finding annotations in class hierarchy + if (instance.getClass().isAnnotationPresent(Path.class)) + { + registerResource(instance); + } + } + + private void registerResource(Object resourceInstance) + { + Method[] methods = Arrays.stream(resourceInstance.getClass().getMethods()) + .filter(method -> method.isAnnotationPresent(Path.class)) + .toArray(Method[]::new); + + // TODO: Refactor to support finding annotations in class hierarchy + for (Method method : methods) + { + registerMethod(resourceInstance, method); + } + } + + private void registerMethod(Object resourceInstance, Method method) + { + String classLevelPath = resourceInstance.getClass() + .getAnnotation(Path.class).value(); + String methodLevelPath = method.getAnnotation(Path.class).value(); + String separator = methodLevelPath.startsWith("/") ? "" : "/"; + String fullPath = classLevelPath + separator + methodLevelPath; + Resource resource = new SingletonResource(resourceInstance); + Endpoint endpoint = new Endpoint(resource, method); + registerEndpoint(fullPath, endpoint); + } + + // TODO: Per the jax-rs spec, this will need to "pick" a best match class/set + // of classes first by finding the best match class, then filtering out the + // classes that aren't in-line with that class. Then it proceeds to the + // method level. This will for now just go right to the method level, then + // later I can have it filter out the class stuff after the fact, or even + // optimize it so that it directly stops at the class level the first time + // so it can filter those down before proceeding to the method level. + + static class WordBlockToken + { + private final String word; + + public WordBlockToken(String word) + { + this.word = word; + } + } + + static class PureVariableBlockToken + { + private final String name; + + PureVariableBlockToken(String name) + { + this.name = name; + } + } + + static class RegexVariableBlockToken + { + private final String name; + // TODO: Turn this into a Pattern in the constructor. Needs to be URI + // encoded first, then have its regex characters escaped. + private final String regex; + + public RegexVariableBlockToken(String name, String regex) + { + this.name = name; + this.regex = regex; + } + } + + private void registerEndpoint(String path, Endpoint endpoint) + { + // TODO: Might want to gracefully handle the case where a resource could + // not be fully registered due to some issue with a warning, though + // blowing up could also be an option. Check the specs to see if it should + // blow up, but if it doesn't specify then just handle it gracefully and + // log a warning. Handling it gracefully means it should "revert" any + // additions made to the root block and any other blocks/etc. + // TODO: Need to handle URI encoding/decoding + StringBuilder parsedPath = new StringBuilder(); + String remainingPath = path; + if (remainingPath.startsWith("/")) + { + remainingPath = remainingPath.substring(1); + } + // TODO: Break these up into classes, might as well just set these up for + // unit tests to make it easier to test without needing the whole thing to + // be implemented. + Pattern wordBlockToken = Pattern.compile("^[^/{]+"); + Pattern variableBlockToken = Pattern.compile("^\\{[ \t]*(?\\w[\\w.-]*)[ \t]*(:[ \t]*(?([^{}]|\\{[^{}]*})*)[ \t]*)?}"); + DispatchBlock currentBlock = rootBlock; + while (!remainingPath.isEmpty()) + { + List tokensSinceLastSlash = new ArrayList<>(); + while (remainingPath.length() > 0 && remainingPath.charAt(0) != '/') + { + { + Matcher matcher = wordBlockToken.matcher(remainingPath); + if (matcher.find()) + { + String group = matcher.group(); + parsedPath.append(group); + WordBlockToken token = new WordBlockToken(group); + tokensSinceLastSlash.add(token); + remainingPath = remainingPath.substring(matcher.end()); + continue; + } + } + { + Matcher matcher = variableBlockToken.matcher(remainingPath); + if (matcher.find()) + { + parsedPath.append(matcher.group()); + String name = matcher.group("name"); + String regex = matcher.group("regex"); + Object token = regex == null + ? new PureVariableBlockToken(name) + : new RegexVariableBlockToken(name, regex); + tokensSinceLastSlash.add(token); + remainingPath = remainingPath.substring(matcher.end()); + continue; + } + } + throw new RuntimeException("Could not parse URI at position: " + + parsedPath + "\u032D" + remainingPath); + } + if (tokensSinceLastSlash.isEmpty()) + { + // TODO: Probably two cases here: + // 1) double-stacked slash. This is bad. Throw an exception. + // b) It's an @Path("") or @Path("/") "root" handler. Currently not + // well-handled probably, but I'll get around to it once the main + // stuff is done. + throw new UnsupportedOperationException("TODO"); + } + else if (tokensSinceLastSlash.size() == 1) + { + Object token = tokensSinceLastSlash.get(0); + // TODO: Refactor to avoid using instanceof + if (token instanceof WordBlockToken) + { + String word = ((WordBlockToken) token).word; + WordDispatchBlock wordDispatchBlock; + if (currentBlock.childrenByWord.containsKey(word)) + { + wordDispatchBlock = currentBlock.childrenByWord.get(word); + } + else + { + wordDispatchBlock = new WordDispatchBlock(); + currentBlock.addWordChild(word, wordDispatchBlock); + } + currentBlock = wordDispatchBlock; + } + else if (token instanceof PureVariableBlockToken) + { + FullSegmentPureVariableDispatchBlock fullSegmentPureVariableDispatchBlock; + String name = ((PureVariableBlockToken) token).name; + if (currentBlock.fullSegmentPureVariableChild != null) + { + fullSegmentPureVariableDispatchBlock = currentBlock.fullSegmentPureVariableChild; + if (!name.equals(fullSegmentPureVariableDispatchBlock.name)) + { + throw new RuntimeException("Multiple variable names exist at the" + + " same exact path" /* TODO: Include the conflicting paths */); + } + } + else + { + fullSegmentPureVariableDispatchBlock = new FullSegmentPureVariableDispatchBlock(name); + currentBlock.fullSegmentPureVariableChild = fullSegmentPureVariableDispatchBlock; + } + currentBlock = fullSegmentPureVariableDispatchBlock; + } + else + { + // TODO: Form a regex block + throw new UnsupportedOperationException("TODO"); + } + } + else + { + // TODO: Form a regex block + throw new UnsupportedOperationException("TODO"); + } + if (!remainingPath.isEmpty()) + { + if (remainingPath.charAt(0) != '/') + { + throw new RuntimeException("Failure encountered during path" + + " parsing. Please report this path to the developers" + + " of Gemini: " + path); + } + else + { + parsedPath.append('/'); + remainingPath = remainingPath.substring(1); + } + } + } + Set httpMethods = new HashSet<>(); + for (Annotation annotation : endpoint.method.getAnnotations()) + { + Class annotationClass = annotation.annotationType(); + if (annotationClass.isAnnotationPresent(HttpMethod.class)) + { + HttpMethod httpMethod = annotationClass + .getAnnotation(HttpMethod.class); + httpMethods.add(httpMethod.value()); + } + } + for (Annotation annotation : endpoint.resource + .getInstanceClass().getAnnotations()) + { + Class annotationClass = annotation.annotationType(); + if (annotationClass.isAnnotationPresent(HttpMethod.class)) + { + HttpMethod httpMethod = annotationClass + .getAnnotation(HttpMethod.class); + httpMethods.add(httpMethod.value()); + } + } + for (String httpMethod : httpMethods) + { + if (currentBlock.endpointsByHttpMethod.containsKey(httpMethod)) + { + throw new RuntimeException("Path " + path + " and HttpMethod " + + httpMethod + " is already associated with another endpoint"); + // TODO: Would be nice to include the other endpoint. + // TODO: Include logging, not just thrown exceptions. + } + currentBlock.endpointsByHttpMethod.put(httpMethod, endpoint); + } + } + + static class DispatchMatch + { + private final DispatchBlock block; + private final Endpoint endpoint; + final String value; + final List matchChildren; + + public DispatchMatch(DispatchBlock block, + Endpoint endpoint, + String value, + List matchChildren) + { + this.block = block; + this.endpoint = endpoint; + this.value = value; + this.matchChildren = matchChildren; + } + + // for debugging + List getLeafValues() + { + // TODO: I suppose this could have a value and children at the same time. + // Need to understand for myself what a "DispatchMatch" actually is, + // I've forgotten since last night. Should probably write-up a new + // explanation like the (incorrect) one in the dispatch method. + if (matchChildren != null) + { + return matchChildren.stream() + .map(DispatchMatch::getLeafValues) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + if (value != null) + { + return List.of(value); + } + return null; + } + + // for debugging + List getLeafMatches() + { + // TODO: See TODO above in #getLeafValues. + if (matchChildren != null) + { + return matchChildren.stream() + .map(DispatchMatch::getLeafMatches) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + return List.of(this); + } + + void getBestMatch(DispatchBestMatchInfo info) + { + if (block instanceof FullSegmentPureVariableDispatchBlock) + { + String name = ((FullSegmentPureVariableDispatchBlock) block).name; + info.values.put(name, value); + } + else if (block instanceof RegexVariableDispatchBlock) + { + // TODO: Need to do some refactoring b/c the regex variable dispatch + // blocks will be capable of having multiple name/value entries. + throw new UnsupportedOperationException("TODO"); + } + if (matchChildren != null) + { + matchChildren.get(0).getBestMatch(info); + } + else + { + info.endpoint = this.endpoint; + } + } + } + + static class DispatchBestMatchInfo + { + private Endpoint endpoint; + private Map values = new HashMap<>(); + } + + abstract static class DispatchBlock + { + Map childrenByWord = new HashMap<>(0); + FullSegmentPureVariableDispatchBlock fullSegmentPureVariableChild; + List regexChildren = new ArrayList<>(0); + Map endpointsByHttpMethod = new HashMap<>(0); + + void addWordChild(String word, WordDispatchBlock block) + { + childrenByWord.put(word, block); + } + + void setFullSegmentPureVariableChild(FullSegmentPureVariableDispatchBlock block) + { + fullSegmentPureVariableChild = block; + } + + void addRegexChild(RegexVariableDispatchBlock block) + { + regexChildren.add(block); + } + + // TODO: Can potentially optimize away the list initializations and + // additions by pre-caching the relative index of all endpoints for a + // global sort, then having the below simply discard the existing match + // found if a new match found is of a lower index AKA higher precedence. + final List getChildMatches(String httpMethod, + String[] segments, + int index) + { + String segment = segments[index]; + List matches = null; + + DispatchBlock childWordBlock = childrenByWord.get(segment); + if (childWordBlock != null) + { + DispatchMatch match = childWordBlock.getDispatchMatch(httpMethod, + segments, index); + if (match != null) + { + matches = new LinkedList<>(); + matches.add(match); + } + } + + if (fullSegmentPureVariableChild != null) + { + DispatchMatch match = fullSegmentPureVariableChild + .getDispatchMatch(httpMethod, segments, index); + if (match != null) + { + if (matches == null) + { + matches = new LinkedList<>(); + } + matches.add(match); + } + } + for (DispatchBlock regexChild : regexChildren) + { + DispatchMatch match = regexChild + .getDispatchMatch(httpMethod, segments, index); + if (match != null) + { + if (matches == null) + { + matches = new LinkedList<>(); + } + matches.add(match); + } + } + return matches; + } + + Endpoint getMatchingEndpoint(String httpMethod) + { + return endpointsByHttpMethod.get(httpMethod); + } + + abstract DispatchMatch getDispatchMatch(String httpMethod, + String[] segments, + int index); + } + + static class RootDispatchBlock extends DispatchBlock + { + + @Override + DispatchMatch getDispatchMatch(String httpMethod, + String[] segments, + int index) + { + if (segments.length == 0) + { + // TODO: In theory this could exist, but for now lets assume it doesn't + return null; + } + else + { + List childMatches = getChildMatches(httpMethod, + segments, index); + if (childMatches != null) + { + return new DispatchMatch(this, null, null, childMatches); + } + else + { + return null; + } + } + } + } + + static class WordDispatchBlock extends DispatchBlock + { + @Override + DispatchMatch getDispatchMatch(String httpMethod, + String[] segments, + int index) + { + if (segments.length - 1 == index) + { + // If this has a matching endpoint, consider it a dispatch candidate + Endpoint endpoint = getMatchingEndpoint(httpMethod); + if (endpoint != null) + { + return new DispatchMatch(this, endpoint, null, null); + } + return null; + } + else + { + // For word blocks it's assumed that you match once in this method, + // because the check-match is performed outside of it using the map. + List childMatches = getChildMatches(httpMethod, + segments, index + 1); + if (childMatches != null) + { + return new DispatchMatch(this, null, null, childMatches); + } + else + { + return null; + } + } + } + } + + static class FullSegmentPureVariableDispatchBlock extends DispatchBlock + { + private final String name; + + FullSegmentPureVariableDispatchBlock(String name) + { + this.name = name; + } + + @Override + DispatchMatch getDispatchMatch(String httpMethod, + String[] segments, + int index) + { + if (segments.length - 1 == index) + { + // If this has a matching endpoint, consider it a dispatch candidate + Endpoint endpoint = getMatchingEndpoint(httpMethod); + if (endpoint != null) + { + return new DispatchMatch(this, endpoint, segments[index], null); + } + return null; + } + else + { + // For full segment variable blocks it's assumed that your match is the + // entire segment, because that's simply the definition of this type of + // segment. That is what it represents. + List childMatches = getChildMatches(httpMethod, + segments, index + 1); + if (childMatches != null) + { + return new DispatchMatch(this, null, segments[index], childMatches); + } + else + { + return null; + } + } + } + } + + static class RegexVariableDispatchBlock extends DispatchBlock + { + @Override + DispatchMatch getDispatchMatch(String httpMethod, + String[] segments, + int index) + { + // TODO + throw new UnsupportedOperationException(); + } + } + + + interface Resource + { + Object getInstance(); + + Class getInstanceClass(); + } + + static class SingletonResource implements Resource + { + private final Object singleton; + + SingletonResource(Object singleton) + { + this.singleton = singleton; + } + + @Override + public Object getInstance() + { + return singleton; + } + + @Override + public Class getInstanceClass() + { + return singleton.getClass(); + } + } + + static class ClassResource implements Resource + { + private final Class resourceClass; + + ClassResource(Class resourceClass) + { + this.resourceClass = resourceClass; + } + + + @Override + public Object getInstance() + { + // TODO: Find out how to do dependency injection. Also might want to use + // the fast reflection library for this if/when possible. + try + { + return resourceClass.getConstructor().newInstance(); + } + catch (InstantiationException + | IllegalAccessException + | InvocationTargetException + | NoSuchMethodException e) + { + throw new RuntimeException(e); + } + } + + @Override + public Class getInstanceClass() + { + return resourceClass; + } + } + + static class Endpoint + { + private final Resource resource; + private final Method method; + + Endpoint(Resource resource, Method method) + { + this.resource = resource; + this.method = method; + } + } + + DispatchMatch getDispatchMatches(String httpMethod, String uri) + { + // TODO: This should be commented back in, but I'm leaving it commented + // out while I try to find a way around this to avoid the performance hit. + /*int uriStart = uri.startsWith("/") ? 1 : 0; + int uriEnd = uri.length() - (uri.endsWith("/") ? 1 : 0); + String normalizedUri = uri.substring(uriStart, uriEnd);*/ + String[] segments = uri.split("/"); + return rootBlock.getDispatchMatch(httpMethod, segments, 0); + } + + // TODO: Not use Gemini's Context eventually, probably. + //public void dispatch(Context context) + public Object dispatch(String httpMethod, String uri) + { + // TODO: Add `getServletPath` as a method in Context so that this can + // only match the URI relative to where the servlet is hosted. + // `getServletPath` is a method of HttpServletRequest. For + // non-servlet containers this can just default to "" or "/". Find + // out what the default is for Resin and use that. + int uriStart = uri.startsWith("/") ? 1 : 0; + int uriEnd = uri.length() - (uri.endsWith("/") ? 1 : 0); + String normalizedUri = uri.substring(uriStart, uriEnd); + DispatchMatch dispatchMatch = getDispatchMatches(httpMethod, normalizedUri); + + // TODO: Need to filter the dispatch match down to only the correct one so + // the path params can be extracted. There's a TO-DO...somewhere in this + // file (or maybe the test) for how to do that naturally and only come out + // with the right match per the sorting rules, and not have to deal with + // the tree of matches. + + DispatchBestMatchInfo matchInfo = new DispatchBestMatchInfo(); + dispatchMatch.getBestMatch(matchInfo); + Method method = matchInfo.endpoint.method; + // TODO: For now I'm just gonna support valueOf for simplicity, + // and assume it's static. + List arguments = new ArrayList<>(); + Object instance = matchInfo.endpoint.resource.getInstance(); + Class instanceClass = matchInfo.endpoint.resource.getInstanceClass(); + Class[] parameterTypes = method.getParameterTypes(); + Annotation[][] parameterAnnotations = method.getParameterAnnotations(); + for (Parameter parameter : method.getParameters()) + { + Class parameterType = parameter.getType(); + PathParam pathParam = parameter.getAnnotation(PathParam.class); + String stringValue = matchInfo.values.get(pathParam.value()); + Object val; + if (parameterType == String.class) + { + val = stringValue; + } + else + { + MethodAccess parameterTypeAccess = MethodAccess.get(parameterType); + int index = parameterTypeAccess.getIndex("fromString", String.class); + if (index < 0) { + index = parameterTypeAccess.getIndex("valueOf", String.class); + } + val = parameterTypeAccess.invoke(null, index, stringValue); + } + arguments.add(val); + } + try + { + // TODO: Temporarily having dispatch return be the method invocation + // return value so I can test it easier. Should refactor to make this + // easier. + return method.invoke(instance, arguments.toArray()); + } + catch (IllegalAccessException | InvocationTargetException e) + { + throw new RuntimeException(e); + } } } diff --git a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java index 6feefdf0..7c012a6c 100644 --- a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java +++ b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java @@ -2,24 +2,732 @@ import org.junit.Test; +import javax.ws.rs.GET; +import javax.ws.rs.HttpMethod; import javax.ws.rs.Path; import javax.ws.rs.PathParam; -import javax.ws.rs.core.Response; -import static org.junit.Assert.*; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.Assert.assertEquals; public class JaxRsDispatcherTest { - @Path("/foo/{dog: .+}{cat: .+}") - public static class Foo { + @Path("/test") + public static class TestCase1 + { + @GET + @Path("/bar") + public String doIt() + { + return "did-it-test-case-1"; + } + } + + @Path("/{test}") + public static class TestCase2 + { + @GET + @Path("/bar") + public String doIt(@PathParam("test") String test) + { + return "did-it-test-case-2" + test; + } + } + + @Path("/{test: .+}") + public static class TestCase3 + { + @GET + @Path("/bar") + public String doIt(@PathParam("test") String test) + { + return "did-it-test-case-3" + test; + } + } + + @Path("/{test1: \\d+}-dog-{test2}") + public static class TestCase4 + { + @GET + @Path("/bar") + public String doIt(@PathParam("test1") String test1, + @PathParam("test2") String test2) + { + return "did-it-test-case-4" + test1 + test2; + } + } + + @Path("/{test1: \\d+}/-dog-{test2}") + public static class TestCase5 + { + @GET + @Path("/bar") + public String doIt(@PathParam("test1") String test1, + @PathParam("test2") String test2) + { + return "did-it-test-case-5" + test1 + test2; + } + } + + @Path("foo") + public static class FooResource + { + @GET + @Path("bar") + public String bar() + { + return "did-it-foo-resource"; + } + } + + @Path("foo") + public static class FooResourceVar + { + @GET + @Path("{bar}") + public String bar(@PathParam("bar") String bar) + { + return "did-it-foo-resource-var" + bar; + } + } + + @Path("/foo") + public interface TestCase6 + { + @GET + @Path("/bar") + String doIt(); + } + + public static class TestCase6Impl + implements TestCase6 + { + @Override + public String doIt() + { + return "did-it-test-case-6"; + } + } + + @Path("/foo") + public static class TestCase7 + { + @GET + @Path("/{bar}") + public String doIt(@PathParam("bar") UUID test) + { + return "did-it-test-case-7" + test; + } + } + + @Path("/{test}") + public static class TestCase8 + { + @GET @Path("/bar") - public Response doIt(@PathParam("dog") String dog) { - return Response.ok().build(); + public String doIt(@PathParam("test") String test) + { + return "did-it-test-case-8" + test; + } + } + + @Test + public void simpleEndpointsShouldBeDispatched() + { + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestCase1()); + assertEquals("did-it-test-case-1", + dispatcher.dispatch(HttpMethod.GET, "/test/bar")); + } + + @Test + public void classRegistrationShouldBeSupported() + { + // TODO: Not implemented yet + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(TestCase1.class); + assertEquals("did-it-test-case-1", + dispatcher.dispatch(HttpMethod.GET, "/test/bar")); + } + + @Test + public void precedentsShouldBeRespected() + { + // TODO: Not implemented yet + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestCase3()); + dispatcher.register(new TestCase2()); + dispatcher.register(new TestCase1()); + assertEquals("did-it-test-case-3dog", + dispatcher.dispatch(HttpMethod.GET, "/dog/bar")); + assertEquals("did-it-test-case-1", + dispatcher.dispatch(HttpMethod.GET, "/test/bar")); + } + + @Test + public void pathParamsShouldBeProvided() + { + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestCase2()); + assertEquals("did-it-test-case-2dog", + dispatcher.dispatch(HttpMethod.GET, "/dog/bar")); + } + + @Test + public void uuidPathParamsShouldBeSupported() + { + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestCase7()); + UUID id = UUID.randomUUID(); + assertEquals("did-it-test-case-7" + id, + dispatcher.dispatch(HttpMethod.GET, "/foo/" + id)); + } + + @Test + public void pathParamsAtClassLevelShouldBeSupported() + { + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestCase8()); + assertEquals("did-it-test-case-8dog", + dispatcher.dispatch(HttpMethod.GET, "/dog/bar")); + } + + @Test + public void inheritanceShouldBeCaptured() + { + // TODO: Not yet implemented + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestCase6Impl()); + assertEquals("did-it-test-case-6", + dispatcher.dispatch(HttpMethod.GET, "/foo/bar")); + } + + static final int ITERATIONS = 2_700_000; + + @Test + public void perfTestRegexSlow() + { + long start; + for (int i = 0; i < ITERATIONS; i++) + { + Pattern pattern = Pattern.compile("/foo/bar"); + Matcher matcher = pattern.matcher("/foo/bar"); + boolean found = matcher.find(); + } + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + Pattern pattern = Pattern.compile("/foo/bar"); + Matcher matcher = pattern.matcher("/foo/bar"); + boolean found = matcher.find(); + } + System.out.println("Total time regex slow: " + + (System.currentTimeMillis() - start) + "ms"); + } + + @Test + public void perfTestRegexFast() + { + long start; + Pattern pattern = Pattern.compile("/foo/bar"); + for (int i = 0; i < ITERATIONS; i++) + { + Matcher matcher = pattern.matcher("/foo/bar"); + boolean found = matcher.find(); + } + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + Matcher matcher = pattern.matcher("/foo/bar"); + boolean found = matcher.find(); + } + System.out.println("Total time regex fast: " + + (System.currentTimeMillis() - start) + "ms"); + } + + @Test + public void perfTestMap() + { + Map>> foo = Map.of("foo", + Map.of("bar", Map.of(HttpMethod.GET, new Object()))); + String uriNoTrailingSlash = "foo/bar"; + long start; + for (int i = 0; i < ITERATIONS; i++) + { + String[] uriSegments = uriNoTrailingSlash.split("/"); + if (foo.containsKey(uriSegments[0])) + { + Map> inner = foo.get(uriSegments[0]); + if (inner.containsKey(uriSegments[1])) + { + Map endpointsByHttpMethod = inner.get( + uriSegments[1]); + Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + } + } + } + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + String[] uriSegments = uriNoTrailingSlash.split("/"); + if (foo.containsKey(uriSegments[0])) + { + Map> inner = foo.get(uriSegments[0]); + if (inner.containsKey(uriSegments[1])) + { + Map endpointsByHttpMethod = inner.get( + uriSegments[1]); + Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + } + } + } + System.out.println("Total time map: " + + (System.currentTimeMillis() - start) + "ms"); + } + + static class Foo { + public static void main(String... args) { + JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.register(new FooResource()); + JaxRsDispatcherTest test = new JaxRsDispatcherTest(); + test.warmUpJaxRs(jaxRsDispatcher); + long start; + start = System.currentTimeMillis(); + test.justJaxRs(jaxRsDispatcher); + System.out.println("Total time dispatchBlocks: " + + (System.currentTimeMillis() - start) + "ms"); + } + } + + public void warmUpJaxRs(JaxRsDispatcher jaxRsDispatcher) + { + String uriNoTrailingSlash = "foo/bar"; + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, + uriNoTrailingSlash); + if (i == 0) + { + JaxRsDispatcher.DispatchMatch match = jaxRsDispatcher + .getDispatchMatches(HttpMethod.GET, uriNoTrailingSlash); + System.out.println("Found " + match.getLeafValues()); + } + } + } + + public void justJaxRs(JaxRsDispatcher jaxRsDispatcher) + { + String uriNoTrailingSlash = "foo/bar"; + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, + uriNoTrailingSlash); + } + } + + @Test + public void perfTestJaxRs() + { + String uriNoTrailingSlash = "foo/bar"; + JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.register(new FooResource()); + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, + uriNoTrailingSlash); + if (i == 0) + { + JaxRsDispatcher.DispatchMatch match = jaxRsDispatcher + .getDispatchMatches(HttpMethod.GET, uriNoTrailingSlash); + System.out.println("Found " + match.getLeafValues()); + } + } + long start; + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, + uriNoTrailingSlash); + } + System.out.println("Total time dispatchBlocks: " + + (System.currentTimeMillis() - start) + "ms"); + } + + @Test + public void perfTestAll() + { + // Warm-up + { + long start; + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + Pattern pattern = Pattern.compile("/foo/bar"); + Matcher matcher = pattern.matcher("/foo/bar"); + boolean found = matcher.find(); + } + start = System.currentTimeMillis(); + Pattern pattern = Pattern.compile("/foo/bar"); + for (int i = 0; i < ITERATIONS; i++) + { + Matcher matcher = pattern.matcher("/foo/bar"); + boolean found = matcher.find(); + } + Map>> foo = Map.of("foo", + Map.of("bar", Map.of(HttpMethod.GET, new Object()))); + // This is better for the map approach + String uriNoTrailingSlash = "foo/bar"; + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + String[] uriSegments = uriNoTrailingSlash.split("/"); + if (foo.containsKey(uriSegments[0])) + { + Map> inner = foo.get(uriSegments[0]); + if (inner.containsKey(uriSegments[1])) + { + Map endpointsByHttpMethod = inner.get( + uriSegments[1]); + Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + } + } + } + { + String uri = "foo/bar/"; + JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.register(new FooResource()); + + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, + uriNoTrailingSlash); + if (i == 0) + { + JaxRsDispatcher.DispatchMatch match = jaxRsDispatcher + .getDispatchMatches(HttpMethod.GET, uri); + System.out.println("Found " + match.getLeafValues()); + } + } + System.out.println("Total time dispatchBlocks: " + + (System.currentTimeMillis() - start) + "ms"); + } + } + { + long start; + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + Pattern pattern = Pattern.compile("/foo/bar"); + Matcher matcher = pattern.matcher("/foo/bar"); + boolean found = matcher.find(); + } + System.out.println("Total time regex slow: " + + (System.currentTimeMillis() - start) + "ms"); + start = System.currentTimeMillis(); + Pattern pattern = Pattern.compile("/foo/bar"); + for (int i = 0; i < ITERATIONS; i++) + { + Matcher matcher = pattern.matcher("/foo/bar"); + boolean found = matcher.find(); + } + System.out.println("Total time regex fast: " + + (System.currentTimeMillis() - start) + "ms"); + Map>> foo = Map.of("foo", + Map.of("bar", Map.of(HttpMethod.GET, new Object()))); + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + String[] uriSegments = "foo/bar".split("/"); + if (foo.containsKey(uriSegments[0])) + { + Map> inner = foo.get(uriSegments[0]); + if (inner.containsKey(uriSegments[1])) + { + Map endpointsByHttpMethod = inner.get( + uriSegments[1]); + Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + } + } + } + System.out.println("Total time map: " + + (System.currentTimeMillis() - start) + "ms"); + { + String uri = "foo/bar/"; + // This is better for the map approach + String uriNoTrailingSlash = "foo/bar"; + JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.register(new FooResource()); + + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, + uriNoTrailingSlash); + } + System.out.println("Total time dispatchBlocks: " + + (System.currentTimeMillis() - start) + "ms"); + } + { + String uri = "foo/bar/"; + // This is better for the map approach + String uriNoTrailingSlash = "foo/bar"; + JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.register(new FooResource()); + + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + // TODO: Can possibly optimize so that it just naturally checks for + // empty strings for the final block to see if it should end early, + // thus avoiding the need to manipulate the string + int uriStart = uri.startsWith("/") ? 1 : 0; + int uriEnd = uri.length() - (uri.endsWith("/") ? 1 : 0); + String normalizedUri = uri.substring(uriStart, uriEnd); + jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, normalizedUri); + } + System.out.println("Total time dispatchBlocks: " + + (System.currentTimeMillis() - start) + "ms"); + } + { + String uri = "foo/bar/"; + // This is better for the map approach + String uriNoTrailingSlash = "foo/bar"; + JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.register(new FooResource()); + + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + // TODO: Can possibly optimize so that it just naturally checks for + // empty strings for the final block to see if it should end early, + // thus avoiding the need to manipulate the string + int uriStart = 0; + int uriEnd = 7; + String normalizedUri = uri.substring(uriStart, uriEnd); + jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, normalizedUri); + } + System.out.println("Total time dispatchBlocks: " + + (System.currentTimeMillis() - start) + "ms"); + } + } + } + + public static void main(String... args) + { + JaxRsDispatcherTest test = new JaxRsDispatcherTest(); + //test.perfTestAllComplicated(); + Map>> foo = Map.of("foo", + Map.of("bar", Map.of(HttpMethod.GET, new Object()))); + JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.register(new FooResource()); + test.combinedWarmUp(foo, jaxRsDispatcher); + test.mapApproach(foo); + test.dispatcherBlocksApproach(jaxRsDispatcher); + } + + public void combinedWarmUp(Map>> foo, + JaxRsDispatcher jaxRsDispatcher) + { + String uriNoTrailingSlash = "foo/bar"; + for (int i = 0; i < ITERATIONS; i++) + { + String[] uriSegments = uriNoTrailingSlash.split("/"); + if (foo.containsKey(uriSegments[0])) + { + Map> inner = foo.get(uriSegments[0]); + if (inner.containsKey(uriSegments[1])) + { + Map endpointsByHttpMethod = inner.get(uriSegments[1]); + Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + } + } + } + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, uriNoTrailingSlash); + } + } + + public void mapApproach(Map>> foo) + { + long start = System.currentTimeMillis(); + String uriNoTrailingSlash = "foo/bar"; + String httpMethod = HttpMethod.GET; + for (int i = 0; i < ITERATIONS; i++) + { + String[] uriSegments = uriNoTrailingSlash.split("/"); + if (foo.containsKey(uriSegments[0])) + { + Map> inner = foo.get(uriSegments[0]); + if (inner.containsKey(uriSegments[1])) + { + Map endpointsByHttpMethod = inner.get(uriSegments[1]); + Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + } + } } + System.out.println("Total time map: " + + (System.currentTimeMillis() - start) + "ms"); + } + + public void dispatcherBlocksApproach(JaxRsDispatcher jaxRsDispatcher) + { + // This is better for the blocks approach + String uriNoTrailingSlash = "foo/bar"; + long start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, uriNoTrailingSlash); + } + System.out.println("Total time dispatchBlocks: " + + (System.currentTimeMillis() - start) + "ms"); } @Test - public void testIt() { + public void perfTestAllComplicated() + { + // Warm-up + { + var uuidGroupNameStr = "g" + UUID.randomUUID().toString().replaceAll("-", ""); + var uuidStr = UUID.randomUUID().toString(); + String uri = "foo/" + uuidStr + "/"; + // This is better for the map approach + String uriNoTrailingSlash = "foo/" + uuidStr; + long start; + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + Pattern pattern = Pattern.compile("foo/(?<" + uuidGroupNameStr + ">[^/]+?)/.*"); + Matcher matcher = pattern.matcher(uri); + boolean found = matcher.find(); + String matchFound = matcher.group(uuidGroupNameStr); + if (i == 0) + { + System.out.println("Found " + matchFound); + } + } + start = System.currentTimeMillis(); + Pattern pattern = Pattern.compile("foo/(?<" + uuidGroupNameStr + ">[^/]+?)/.*"); + for (int i = 0; i < ITERATIONS; i++) + { + Matcher matcher = pattern.matcher(uri); + boolean found = matcher.find(); + String matchFound = matcher.group(uuidGroupNameStr); + if (i == 0) + { + System.out.println("Found " + matchFound); + } + } + JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.register(new FooResourceVar()); + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, uri); + if (i == 0) + { + JaxRsDispatcher.DispatchMatch match = jaxRsDispatcher + .getDispatchMatches(HttpMethod.GET, uri); + System.out.println("Found " + match.getLeafValues()); + } + } + } + { + var uuidGroupNameStr = "g" + UUID.randomUUID().toString().replaceAll("-", ""); + var uuidStr = UUID.randomUUID().toString(); + String uri = "foo/" + uuidStr + "/"; + // This is better for the map approach + String uriNoTrailingSlash = "foo/" + uuidStr; + long start; + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + Pattern pattern = Pattern.compile("foo/(?<" + uuidGroupNameStr + ">[^/]+?)/.*"); + Matcher matcher = pattern.matcher(uri); + boolean found = matcher.find(); + String matchFound = matcher.group(uuidGroupNameStr); + } + System.out.println("Total time regex slow: " + + (System.currentTimeMillis() - start) + "ms"); + start = System.currentTimeMillis(); + Pattern pattern = Pattern.compile("foo/(?<" + uuidGroupNameStr + ">[^/]+?)/.*"); + for (int i = 0; i < ITERATIONS; i++) + { + Matcher matcher = pattern.matcher(uri); + boolean found = matcher.find(); + String matchFound = matcher.group(uuidGroupNameStr); + } + System.out.println("Total time regex fast: " + + (System.currentTimeMillis() - start) + "ms"); + { + JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.register(new FooResourceVar()); + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, + uriNoTrailingSlash); + } + System.out.println("Total time dispatchBlocks: " + + (System.currentTimeMillis() - start) + "ms"); + } + { + JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.register(new FooResourceVar()); + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + int uriStart = uri.startsWith("/") ? 1 : 0; + int uriEnd = uri.length() - (uri.endsWith("/") ? 1 : 0); + String normalizedUri = uri.substring(uriStart, uriEnd); + jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, normalizedUri); + } + System.out.println("Total time dispatchBlocks: " + + (System.currentTimeMillis() - start) + "ms"); + } + { + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + List matches = new ArrayList<>(1); + matches.add( + new JaxRsDispatcher.DispatchMatch(null, null, null, null)); + List matches2 = new ArrayList<>(1); + matches2.add( + new JaxRsDispatcher.DispatchMatch(null, null, null, null)); + } + System.out.println("Total time create objects: " + + (System.currentTimeMillis() - start) + "ms"); + } + /*JaxRsDispatcher.DispatchBlock top = new JaxRsDispatcher.DispatchBlock(null); + top.fullSegmentChildren = new JaxRsDispatcher.ChildDispatchBlockGroup(); + JaxRsDispatcher.DispatchBlock foo = new JaxRsDispatcher.DispatchBlock(null); + top.fullSegmentChildren.addChildWordBlock("foo", foo); + + foo.fullSegmentChildren = new JaxRsDispatcher.ChildDispatchBlockGroup(); + JaxRsDispatcher.DispatchBlock variableBlock = new JaxRsDispatcher.DispatchBlock(null); + foo.fullSegmentChildren.setChildPureVariableBlock(variableBlock); + + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + String[] uriSegments = uriNoTrailingSlash.split("/"); + if (top.fullSegmentChildren != null) { + JaxRsDispatcher.DispatchBlock fooInner = top.fullSegmentChildren.getChildWordBlock(uriSegments[0]); + if (fooInner.fullSegmentChildren != null) { + JaxRsDispatcher.DispatchBlock pureVariableBlock = fooInner.fullSegmentChildren.getChildPureVariableBlock(); + if (pureVariableBlock != null) { + Map endpointsByHttpMethod = pureVariableBlock.endpointsByHttpMethod; + } + } + } + } + System.out.println("Total time dispatchBlocks: " + (System.currentTimeMillis() - start) + "ms");*/ + } } } \ No newline at end of file diff --git a/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Path.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Path.java index 06bb8896..87c85bf0 100644 --- a/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Path.java +++ b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Path.java @@ -54,7 +54,9 @@ * the root URI of the handler. Example /api/users => UserHandler; {@code @Path} * will handle `GET /api/users`. */ -@Target(ElementType.METHOD) +// TODO: Roll back the addition of ElementType.TYPE. Only added for testing +// kain's stuff. +@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface Path { diff --git a/gemini-resin-legacy-dispatching/pom.xml b/gemini-resin-legacy-dispatching/pom.xml index 1073a036..6bb0bd30 100644 --- a/gemini-resin-legacy-dispatching/pom.xml +++ b/gemini-resin-legacy-dispatching/pom.xml @@ -58,6 +58,16 @@ javax javaee-web-api + + junit + junit + test + + + org.mockito + mockito-all + test + \ No newline at end of file diff --git a/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationDispatcher.java b/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationDispatcher.java new file mode 100644 index 00000000..d1704ce6 --- /dev/null +++ b/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationDispatcher.java @@ -0,0 +1,565 @@ +/******************************************************************************* + * Copyright (c) 2020, TechEmpower, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name TechEmpower, Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL TECHEMPOWER, INC. BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************/ +package com.techempower.gemini.path; + +import com.techempower.classloader.PackageClassLoader; +import com.techempower.gemini.*; +import com.techempower.gemini.configuration.ConfigurationError; +import com.techempower.gemini.exceptionhandler.ExceptionHandler; +import com.techempower.gemini.path.annotation.Path; +import com.techempower.gemini.prehandler.Prehandler; +import com.techempower.helper.NetworkHelper; +import com.techempower.helper.StringHelper; +import org.reflections.Reflections; +import org.reflections.ReflectionsException; + +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static com.techempower.gemini.Request.*; +import static com.techempower.gemini.Request.HEADER_ACCESS_CONTROL_EXPOSED_HEADERS; +import static com.techempower.gemini.Request.HttpMethod.*; + +public class AnnotationDispatcher implements Dispatcher { + + // + // Member variables. + // + + //private final GeminiApplication app; + private final Map handlers; + private final ExceptionHandler[] exceptionHandlers; + private final Prehandler[] prehandlers; + private final DispatchListener[] listeners; + + private ExecutorService preinitializationTasks = Executors.newSingleThreadExecutor(); + private Reflections reflections = null; + + public AnnotationDispatcher(/*GeminiApplication application*/) + { + //app = application; + handlers = new HashMap<>(); + exceptionHandlers = new ExceptionHandler[]{}; + prehandlers = new Prehandler[]{}; + listeners = new DispatchListener[]{}; + + /*if (exceptionHandlers.length == 0) + { + throw new IllegalArgumentException("PathDispatcher must be configured with at least one ExceptionHandler."); + }*/ + + //startReflectionsThread(); + } + + /*private void startReflectionsThread() + { + // Start constructing Reflections on a new thread since it takes a + // bit of time. + preinitializationTasks.submit(new Runnable() { + @Override + public void run() { + try + { + reflections = PackageClassLoader.getReflectionClassLoader(app); + } + catch (Exception exc) + { + // todo +// log.log("Exception while instantiating Reflections component.", exc); + } + } + }); + }*/ + + /*public void initialize() { + // Wait for pre-initialization tasks to complete. + try + { +// log.log("Completing preinitialization tasks."); + preinitializationTasks.shutdown(); +// log.log("Awaiting termination of preinitialization tasks."); + preinitializationTasks.awaitTermination(5L, TimeUnit.MINUTES); +// log.log("Preinitialization tasks complete."); +// log.log("Reflections component: " + reflections); + } + catch (InterruptedException iexc) + { +// log.log("Preinitialization interrupted.", iexc); + } + + // Throw an exception if Reflections is not ready. + if (reflections == null) + { + throw new ConfigurationError("Reflections not ready; application cannot start."); + } + + //register(); + }*/ + + /*private void register() { +// log.log("Registering annotated entities, relations, and type adapters."); + try { + final ExecutorService service = Executors.newFixedThreadPool(1); + + // @Path-annotated classes. + service.submit(new Runnable() { + @Override + public void run() { + for (Class clazz : reflections.getTypesAnnotatedWith(Path.class)) { + final Path annotation = clazz.getAnnotation(Path.class); + + try { + handlers.put(annotation.value(), + new AnnotationHandler(annotation.value(), + clazz.getDeclaredConstructor().newInstance())); + } + catch (NoSuchMethodException nsme) { + // todo + } + catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + // todo + } + } + } + }); + + try + { + service.shutdown(); + service.awaitTermination(1L, TimeUnit.HOURS); + } + catch (InterruptedException iexc) + { +// log.log("Unable to register all entities in 1 hour!", LogLevel.CRITICAL); + } + +// log.log("Done registering annotated items."); + } + catch (ReflectionsException e) + { + throw new RuntimeException("Warn: problem registering class with reflection", e); + } + }*/ + + public void register(Object resource) { + Class clazz = resource.getClass(); + Path annotation = clazz.getAnnotation(Path.class); + try + { + handlers.put(annotation.value(), + new AnnotationHandler(annotation.value(), + clazz.getDeclaredConstructor().newInstance())); + } + catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) + { + throw new RuntimeException(e); + } + } + + /** + * Notify the listeners that a dispatch is starting. + */ + protected void notifyListenersDispatchStarting(Context context, String command) + { + final DispatchListener[] theListeners = listeners; + for (DispatchListener listener : theListeners) + { + listener.dispatchStarting(this, context, command); + } + } + + /** + * Send the request to all prehandlers. + */ + protected boolean prehandle(C context) + { + final Prehandler[] thePrehandlers = prehandlers; + for (Prehandler p : thePrehandlers) + { + if (p.prehandle(context)) + { + return true; + } + } + + // Returning false indicates we did not fully handle this request and + // processing should continue to the handle method. + return false; + } + + @Override + public boolean dispatch(Context plainContext) { + throw new UnsupportedOperationException(); + } + + public boolean dispatch(/*Context plainContext*/Request.HttpMethod httpMethod, String uri) { + boolean success = false; + + // Surround all logic with a try-catch so that we can send the request to + // our ExceptionHandlers if anything goes wrong. + try + { + // Cast the provided Context to a C. + //@SuppressWarnings("unchecked") + //final C context = (C)plainContext; + + // Convert the request URI into path segments. + final PathSegments segments = new PathSegments(uri); + + // Any request with an Origin header will be handled by the app directly, + // however there are some headers we need to set up to add support for + // cross-origin requests. + /*if(context.headers().get(HEADER_ORIGIN) != null) + { + //addCorsHeaders(context); + + if(((Request)context.getRequest()).getRequestMethod() == OPTIONS) + { + //addPreflightCorsHeaders(segments, context); + // Returning true indicates we did fully handle this request and + // processing should not continue. + return true; + } + }*/ + + // Make these references available thread-locally. + //RequestReferences.set(context, segments); + + // Notify listeners. + //notifyListenersDispatchStarting(plainContext, segments.getUriFromRoot()); + + // Find the associated Handler. + AnnotationHandler handler = null; + + if (segments.getCount() > 0) + { + handler = this.handlers.get(segments.get(0)); + + // If we've found a Handler to use, we have consumed the first path + // segment. + if (handler != null) + { + segments.increaseOffset(); + } + } + /** + * todo: We no longer have the notion of a 'rootHandler'. + * This can be accomplished by having a POJO annotated with + * `@Path("/")` to denote the root uri and a single method + * annotated with `@Path()` to handle the root request. + */ + // Use the root handler when the segment count is 0. +// else if (rootHandler != null) +// { +// handler = rootHandler; +// } + + /** + * todo: We no longer have the notion of a 'defaultHandler'. + * This can be accomplished by having a POJO annotated with + * `@Path("*")` to denote the wildcard uri and a single + * method annotated with `@Path("*")` to handle any request + * routed there. + */ + // Use the default handler if nothing else was provided. +// if (handler == null) +// { +// // The HTTP method for the request is not listed in the HTTPMethod enum, +// // so we are unable to handle the request and simply return a 501. +// if (((Request)plainContext.getRequest()).getRequestMethod() == null) +// { +// handler = notImplementedHandler; +// } +// else +// { +// handler = defaultHandler; +// } +// } + + // TODO: I don't know how I want to handle `prehandle` yet. + success = false; // this means we didn't prehandle + // Send the request to all Prehandlers. +// success = prehandle(context); + + // Proceed to normal Handlers if the Prehandlers did not fully handle + // the request. + if (!success) + { + try + { + // Proceed to the handle method if the prehandle method did not fully + // handle the request on its own. + success = handler.handle(segments, httpMethod); + } + finally + { + // todo: I'm not sure how to do `posthandle` yet. + // Do wrap-up processing even if the request was not handled correctly. +// handler.posthandle(segments, context); + } + } + + /** + * TODO: again, we don't have a `defaultHandler` anymore except by + * routing to a POJO annotated with `@Path("*")` and a method + * annotated with `@Path("*")`. + */ + // If the handler we selected did not successfully handle the request + // and it's NOT the default handler, let's ask the default handler to + // handle the request. +// if ( (!success) +// && (handler != defaultHandler) +// ) +// { +// try +// { +// // Result of prehandler is ignored because the default handler is +// // expected to handle any request. For the default handler, we'll +// // reset the PathSegments offset to 0. +// success = defaultHandler.prehandle(segments.offset(0), context); +// +// if (!success) +// { +// defaultHandler.handle(segments, context); +// } +// } +// finally +// { +// defaultHandler.posthandle(segments, context); +// } +// } + } + catch (Throwable exc) + { + throw new RuntimeException(exc); + //dispatchException(plainContext, exc, null); + } + finally + { + //RequestReferences.remove(); + } + + return success; + } + + /** + * Notify the listeners that a dispatch is complete. + */ + protected void notifyListenersDispatchComplete(Context context) + { + final DispatchListener[] theListeners = listeners; + for (DispatchListener listener : theListeners) + { + listener.dispatchComplete(this, context); + } + } + + @Override + public void dispatchComplete(Context context) { + notifyListenersDispatchComplete(context); + } + + @Override + public void renderStarting(Context context, String renderingName) { + // Intentionally left blank + } + + @Override + public void renderComplete(Context context) { + // Intentionally left blank + } + + @Override + public void dispatchException(Context context, Throwable exception, String description) { + if (exception == null) + { +// log.log("dispatchException called with a null reference.", +// LogLevel.ALERT); + return; + } + + try + { + final ExceptionHandler[] theHandlers = exceptionHandlers; + for (ExceptionHandler handler : theHandlers) + { + if (description != null) + { + handler.handleException(context, exception, description); + } + else + { + handler.handleException(context, exception); + } + } + } + catch (Exception exc) + { + // In the especially worrisome case that we've encountered an exception + // while attempting to handle another exception, we'll give up on the + // request at this point and just write the exception to the log. +// log.log("Exception encountered while processing earlier " + exception, +// LogLevel.ALERT, exc); + } + } + + /** + * Gets the Header-appropriate string representation of the http method + * names that this handler supports for the given path segments. + *

+ * For example, if this handler has two handle methods at "/" and + * one is GET and the other is POST, this method would return the string + * "GET, POST" for the PathSegments "/". + *

+ * By default, this method returns "GET, POST", but subclasses should + * override for more accurate return values. + */ + protected String getAccessControlAllowMethods(PathSegments segments, + C context) + { + // todo: map of routes-to-handler-tuples that expresses something like + // /foo/bar -> { class, method, HttpMethod } + // for lookup here. + // todo: this is also probably wrong in BasicPathHandler + return HttpMethod.GET + ", " + HttpMethod.POST; + } + + + /** + * Adds the standard headers required for CORS support in all requests + * regardless of being preflight. + * @see + * Access-Control-Allow-Origin + * @see + * Access-Control-Allow-Credentials + */ + /*private void addCorsHeaders(C context) + { + // Applications may configure whitelisted origins to which cross-origin + // requests are allowed. + if(NetworkHelper.isWebUrl(context.headers().get(HEADER_ORIGIN)) && + app.getSecurity().getSettings().getAccessControlAllowedOrigins() + .contains(context.headers().get(HEADER_ORIGIN).toLowerCase())) + { + // If the server specifies an origin host rather than wildcard, then it + // must also include Origin in the Vary response header. + context.headers().put(HEADER_VARY, HEADER_ORIGIN); + context.headers().put(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + context.headers().get(HEADER_ORIGIN)); + // Applications may configure the ability to allow credentials on CORS + // requests, but only for domain-specified requests. Wildcards cannot + // allow credentials. + if(app.getSecurity().getSettings().accessControlAllowCredentials()) + { + context.headers().put( + HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); + } + } + // Applications may also configure wildcard origins to be whitelisted for + // cross-origin requests, effectively making the application an open API. + else if(app.getSecurity().getSettings().getAccessControlAllowedOrigins() + .contains(HEADER_WILDCARD)) + { + context.headers().put(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + HEADER_WILDCARD); + } + // Applications may configure whitelisted headers which browsers may + // access on cross origin requests. + if(!app.getSecurity().getSettings().getAccessControlExposedHeaders().isEmpty()) + { + boolean first = true; + final StringBuilder exposed = new StringBuilder(); + for(final String header : app.getSecurity().getSettings() + .getAccessControlExposedHeaders()) + { + if(!first) + { + exposed.append(", "); + } + exposed.append(header); + first = false; + } + context.headers().put(HEADER_ACCESS_CONTROL_EXPOSED_HEADERS, + exposed.toString()); + } + }*/ + + /** + * Adds the headers required for CORS support for preflight OPTIONS requests. + * @see + * Preflighted requests + */ + /*private void addPreflightCorsHeaders(PathSegments segments, C context) + { + // Applications may configure whitelisted headers which may be sent to + // the application on cross origin requests. + if (StringHelper.isNonEmpty(context.headers().get( + HEADER_ACCESS_CONTROL_REQUEST_HEADERS))) + { + final String[] headers = StringHelper.splitAndTrim( + context.headers().get( + HEADER_ACCESS_CONTROL_REQUEST_HEADERS), ","); + boolean first = true; + final StringBuilder allowed = new StringBuilder(); + for(final String header : headers) + { + if(app.getSecurity().getSettings() + .getAccessControlAllowedHeaders().contains(header.toLowerCase())) + { + if(!first) + { + allowed.append(", "); + } + allowed.append(header); + first = false; + } + } + + context.headers().put(HEADER_ACCESS_CONTROL_ALLOW_HEADERS, + allowed.toString()); + } + + final String methods = getAccessControlAllowMethods(segments, context); + if(StringHelper.isNonEmpty(methods)) + { + context.headers().put(HEADER_ACCESS_CONTROL_ALLOW_METHOD, methods); + } + + if(((Request)context.getRequest()).getRequestMethod() == HttpMethod.OPTIONS) + { + context.headers().put(HEADER_ACCESS_CONTROL_MAX_AGE, + app.getSecurity().getSettings().getAccessControlMaxAge() + ""); + } + }*/ +} \ No newline at end of file diff --git a/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationDispatcherTest.java b/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationDispatcherTest.java new file mode 100644 index 00000000..c76d68e4 --- /dev/null +++ b/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationDispatcherTest.java @@ -0,0 +1,126 @@ +package com.techempower.gemini.path; + +import com.techempower.gemini.Context; +import com.techempower.gemini.Request; +import com.techempower.gemini.path.annotation.Get; +import com.techempower.gemini.path.annotation.Path; +import org.junit.Test; + +import java.util.Map; +import java.util.UUID; +import java.util.regex.Matcher; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AnnotationDispatcherTest +{ + @Path("foo") + public static class FooResource { + @Get + @Path("bar") + public Map bar() { + return Map.of(); + } + } + + @Path("foo") + public static class FooResourceVar { + @Get + @Path("{bar}") + public Map bar(String bar) { + return Map.of(); + } + } + + //static final int ITERATIONS = 2_700_000; + static final int ITERATIONS = 2_700_000; + + /*@Test + public void blah() { + AnnotationDispatcher dispatcher = new AnnotationDispatcher<>(); + dispatcher.register(new FooResource()); + long start; + Context context = mock(Context.class); + Request request = mock(Request.class); + when(context.getRequestUri()).thenReturn("foo/bar"); + when(context.getRequest()).thenReturn(request); + when(request.getRequestMethod()).thenReturn(Request.HttpMethod.GET); + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + dispatcher.dispatch(context); + } + System.out.println("Total time kain-approach: " + (System.currentTimeMillis() - start) + "ms"); + }*/ + + public static void main(String...args) { + AnnotationDispatcher dispatcher = new AnnotationDispatcher<>(); + dispatcher.register(new FooResource()); + AnnotationDispatcherTest test = new AnnotationDispatcherTest(); + test.warmUpBlah(dispatcher); + long start; + start = System.currentTimeMillis(); + test.doBlah(dispatcher); + System.out.println("Total time kain-approach: " + + (System.currentTimeMillis() - start) + "ms"); + } + + // I know these are identical, but it's easier to distinguish the warm up + // from the "real" in the profiler this way. + public void warmUpBlah(AnnotationDispatcher dispatcher) { + for (int i = 0; i < ITERATIONS; i++) + { + dispatcher.dispatch(Request.HttpMethod.GET, "foo/bar"); + } + } + + public void doBlah(AnnotationDispatcher dispatcher) { + for (int i = 0; i < ITERATIONS; i++) + { + dispatcher.dispatch(Request.HttpMethod.GET, "foo/bar"); + } + } + + @Test + public void blah() { + AnnotationDispatcher dispatcher = new AnnotationDispatcher<>(); + dispatcher.register(new FooResource()); + for (int i = 0; i < ITERATIONS; i++) + { + dispatcher.dispatch(Request.HttpMethod.GET, "foo/bar"); + } + long start; + /*Context context = mock(Context.class); + Request request = mock(Request.class);*/ + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + dispatcher.dispatch(Request.HttpMethod.GET, "foo/bar"); + } + System.out.println("Total time kain-approach: " + + (System.currentTimeMillis() - start) + "ms"); + } + + @Test + public void blahVariable() { + AnnotationDispatcher dispatcher = new AnnotationDispatcher<>(); + dispatcher.register(new FooResourceVar()); + var uuidStr = UUID.randomUUID().toString(); + var uri = "foo/" + uuidStr; + long start; + /*Context context = mock(Context.class); + Request request = mock(Request.class);*/ + for (int i = 0; i < ITERATIONS; i++) + { + dispatcher.dispatch(Request.HttpMethod.GET, uri); + } + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + dispatcher.dispatch(Request.HttpMethod.GET, uri); + } + System.out.println("Total time kain-approach: " + + (System.currentTimeMillis() - start) + "ms"); + } +} diff --git a/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationHandler.java b/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationHandler.java new file mode 100644 index 00000000..f99eeb48 --- /dev/null +++ b/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationHandler.java @@ -0,0 +1,844 @@ +package com.techempower.gemini.path; + + +import com.esotericsoftware.reflectasm.MethodAccess; +import com.techempower.gemini.Context; +import com.techempower.gemini.Request; +import com.techempower.gemini.path.annotation.*; +import com.techempower.helper.NumberHelper; +import com.techempower.helper.ReflectionHelper; +import com.techempower.helper.StringHelper; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import static com.techempower.gemini.Request.HEADER_ACCESS_CONTROL_REQUEST_METHOD; +import static com.techempower.gemini.Request.HttpMethod.*; + +/** + * Similar to MethodUriHandler, AnnotationHandler class does the same + * strategy of creating `PathUriTree`s for each Request.Method type + * and then inserting handler methods into the trees. + * @param + */ +class AnnotationHandler { + final String rootUri; + final Object handler; + + private final AnnotationHandler.PathUriTree getRequestHandleMethods; + private final AnnotationHandler.PathUriTree putRequestHandleMethods; + private final AnnotationHandler.PathUriTree postRequestHandleMethods; + private final AnnotationHandler.PathUriTree deleteRequestHandleMethods; + protected final MethodAccess methodAccess; + + public AnnotationHandler(String rootUri, Object handler) { + this.rootUri = rootUri; + this.handler = handler; + + getRequestHandleMethods = new AnnotationHandler.PathUriTree(); + putRequestHandleMethods = new AnnotationHandler.PathUriTree(); + postRequestHandleMethods = new AnnotationHandler.PathUriTree(); + deleteRequestHandleMethods = new AnnotationHandler.PathUriTree(); + + methodAccess = MethodAccess.get(handler.getClass()); + discoverAnnotatedMethods(); + } + + /** + * Adds the given PathUriMethod to the appropriate list given + * the request method type. + */ + private void addAnnotatedHandleMethod(AnnotationHandler.PathUriMethod method) + { + switch (method.httpMethod) + { + case PUT: + putRequestHandleMethods.addMethod(method); + break; + case POST: + postRequestHandleMethods.addMethod(method); + break; + case DELETE: + deleteRequestHandleMethods.addMethod(method); + break; + case GET: + getRequestHandleMethods.addMethod(method); + break; + default: + break; + } + } + + /** + * Analyze an annotated method and return its index if it's suitable for + * accepting requests. + * + * @param method The annotated handler method. + * @param httpMethod The http method name (e.g. "GET"). Null + * implies that all http methods are supported. + * @return The PathSegmentMethod for the given handler method. + */ + protected AnnotationHandler.PathUriMethod analyzeAnnotatedMethod(Path path, Method method, + Request.HttpMethod httpMethod) + { + // Only allow accessible (public) methods + if (Modifier.isPublic(method.getModifiers())) + { + return new AnnotationHandler.PathUriMethod( + method, + path.value(), + httpMethod, + methodAccess); + } + else + { + throw new IllegalAccessError("Methods annotated with @Path must be " + + "public. See" + getClass().getName() + "#" + method.getName()); + } + } + + /** + * Discovers annotated methods at instantiation time. + */ + private void discoverAnnotatedMethods() + { + final Method[] methods = handler.getClass().getMethods(); + + for (Method method : methods) + { + // Set up references to methods annotated as Paths. + final Path path = method.getAnnotation(Path.class); + if (path != null) + { + final Get get = method.getAnnotation(Get.class); + final Put put = method.getAnnotation(Put.class); + final Post post = method.getAnnotation(Post.class); + final Delete delete = method.getAnnotation(Delete.class); + // Enforce that only one http method type is on this segment. + if ((get != null ? 1 : 0) + (put != null ? 1 : 0) + + (post != null ? 1 : 0) + (delete != null ? 1 : 0) > 1) + { + throw new IllegalArgumentException( + "Only one request method type is allowed per @PathSegment. See " + + getClass().getName() + "#" + method.getName()); + } + final AnnotationHandler.PathUriMethod psm; + // Those the @Get annotation is implied in the absence of other + // method type annotations, this is left here to directly analyze + // the annotated method in case the @Get annotation is updated in + // the future to have differences between no annotations. + if (get != null) + { + psm = analyzeAnnotatedMethod(path, method, GET); + } + else if (put != null) + { + psm = analyzeAnnotatedMethod(path, method, PUT); + } + else if (post != null) + { + psm = analyzeAnnotatedMethod(path, method, POST); + } + else if (delete != null) + { + psm = analyzeAnnotatedMethod(path, method, DELETE); + } + else + { + // If no http request method type annotations are present along + // side the @PathSegment, then it is an implied GET. + psm = analyzeAnnotatedMethod(path, method, GET); + } + + addAnnotatedHandleMethod(psm); + } + } + } + + /** + * Determine the annotated method that should process the request. + */ + protected AnnotationHandler.PathUriMethod getAnnotatedMethod(PathSegments segments, + C context) + { + final AnnotationHandler.PathUriTree tree; + switch (((Request)context.getRequest()).getRequestMethod()) + { + case PUT: + tree = putRequestHandleMethods; + break; + case POST: + tree = postRequestHandleMethods; + break; + case DELETE: + tree = deleteRequestHandleMethods; + break; + case GET: + tree = getRequestHandleMethods; + break; + default: + // We do not want to handle this + return null; + } + + return tree.search(segments); + } + + /** + * Determine the annotated method that should process the request. + */ + protected AnnotationHandler.PathUriMethod getAnnotatedMethod(PathSegments segments, + Request.HttpMethod httpMethod) + { + final AnnotationHandler.PathUriTree tree; + switch (httpMethod) + { + case PUT: + tree = putRequestHandleMethods; + break; + case POST: + tree = postRequestHandleMethods; + break; + case DELETE: + tree = deleteRequestHandleMethods; + break; + case GET: + tree = getRequestHandleMethods; + break; + default: + // We do not want to handle this + return null; + } + + return tree.search(segments); + } + + /** + * Locates the annotated method to call, invokes it given the path segments + * and context. + * @param segments The URI segments to route + * @param context The current context + * @return + */ + public boolean handle(PathSegments segments, C context) { + getAnnotatedMethod(segments, context); + if (true) return true; + return dispatchToAnnotatedMethod(segments, getAnnotatedMethod(segments, context), + context); + } + + public boolean handle(PathSegments segments, Request.HttpMethod httpMethod) { + getAnnotatedMethod(segments, httpMethod); + /*if (true)*/ return true; + /*return dispatchToAnnotatedMethod(segments, getAnnotatedMethod(segments, context), + context);*/ + } + + protected String getAccessControlAllowMethods(PathSegments segments, C context) + { + final StringBuilder reqMethods = new StringBuilder(); + final List methods = new ArrayList<>(); + + if(context.headers().get(HEADER_ACCESS_CONTROL_REQUEST_METHOD) != null) + { + final AnnotationHandler.PathUriMethod put = this.putRequestHandleMethods.search(segments); + if (put != null) + { + methods.add(put); + } + final AnnotationHandler.PathUriMethod post = this.postRequestHandleMethods.search(segments); + if (post != null) + { + methods.add(this.postRequestHandleMethods.search(segments)); + } + final AnnotationHandler.PathUriMethod delete = this.deleteRequestHandleMethods.search(segments); + if (delete != null) + { + methods.add(this.deleteRequestHandleMethods.search(segments)); + } + final AnnotationHandler.PathUriMethod get = this.getRequestHandleMethods.search(segments); + if (get != null) + { + methods.add(this.getRequestHandleMethods.search(segments)); + } + + boolean first = true; + for(AnnotationHandler.PathUriMethod method : methods) + { + if(!first) + { + reqMethods.append(", "); + } + else + { + first = false; + } + reqMethods.append(method.httpMethod); + } + } + + return reqMethods.toString(); + } + + /** + * Dispatch the request to the appropriately annotated methods in subclasses. + */ + protected boolean dispatchToAnnotatedMethod(PathSegments segments, + AnnotationHandler.PathUriMethod method, + C context) + { + // If we didn't find an associated method and have no default, we'll + // return false, handing the request back to the default handler. + if (method != null && method.index >= 0) + { + // TODO: I think defaultTemplate is going away; maybe put a check + // here that the method can be serialized in the annotated way. + // Set the default template to the method's name. Handler methods can + // override this default by calling template(name) themselves before + // rendering a response. +// defaultTemplate(method.method.getName()); + + if (method.method.getParameterTypes().length == 0) + { + return (Boolean)methodAccess.invoke(this, method.index, + ReflectionHelper.NO_VALUES); + } + else + { + // We have already enforced that the @Path annotations have the correct + // number of args in their declarations to match the variable count + // in the respective URI. So, create an array of values and try to set + // them via retrieving them as segments. + try + { + return (Boolean)methodAccess.invoke(this, method.index, + getVariableArguments(segments, method, context)); + } + catch (RequestBodyException e) + { + // todo +// log().log("Got RequestBodyException.", LogLevel.DEBUG, e); +// return this.error(e.getStatusCode(), e.getMessage()); + } + } + } + + return false; + } + + /** + * Private helper method for capturing the values of the variable annotated + * methods and returning them as an argument array (in order or appearance). + *

+ * Example: @Path("foo/{var1}/{var2}") + * public boolean handleFoo(int var1, String var2) + * + * The array returned for `GET /foo/123/asd` would be: [123, "asd"] + * @param method the annotated method. + * @return Array of corresponding values. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Object[] getVariableArguments(PathSegments segments, + AnnotationHandler.PathUriMethod method, + C context) + throws RequestBodyException + { + final Object[] args = new Object[method.method.getParameterTypes().length]; + int argsIndex = 0; + for (int i = 0; i < method.segments.length; i++) + { + if (method.segments[i].isVariable) + { + if (argsIndex >= args.length) + { + // No reason to continue - we found all are variables. + break; + } + // Try to read it from the context. + if(method.segments[i].type.isPrimitive()) + { + // int + if (method.segments[i].type.isAssignableFrom(int.class)) + { + args[argsIndex] = segments.getInt(i); + } + // long + else if (method.segments[i].type.isAssignableFrom(long.class)) + { + args[argsIndex] = NumberHelper.parseLong(segments.get(i)); + } + // boolean + else if (method.segments[i].type.isAssignableFrom(boolean.class)) + { + // bool variables are NOT simply whether they are present. + // Rather, it should be a truthy value. + args[argsIndex] = StringHelper.equalsIgnoreCase( + segments.get(i), + new String[]{ + "true", "yes", "1" + }); + } + // float + else if (method.segments[i].type.isAssignableFrom(float.class)) + { + args[argsIndex] = NumberHelper.parseFloat(segments.get(i), 0f); + } + // double + else if (method.segments[i].type.isAssignableFrom(double.class)) + { + args[argsIndex] = NumberHelper.parseDouble(segments.get(i), 0f); + } + // default + else + { + // We MUST have something here, set the default to zero. + // This is undefined behavior. If the method calls for a + // char/byte/etc and we pass 0, it is probably unexpected. + args[argsIndex] = 0; + } + } + // String, and technically Object too. + else if (method.segments[i].type.isAssignableFrom(String.class)) + { + args[argsIndex] = segments.get(i); + } + else + { + int indexOfMethodToInvoke; + Class type = method.segments[i].type; + MethodAccess methodAccess = method.segments[i].methodAccess; + if (hasStringInputMethod(type, methodAccess, "fromString")) + { + indexOfMethodToInvoke = methodAccess + .getIndex("fromString", String.class); + } + else if (hasStringInputMethod(type, methodAccess, "valueOf")) + { + indexOfMethodToInvoke = methodAccess + .getIndex("valueOf", String.class); + } + else + { + indexOfMethodToInvoke = -1; + } + if (indexOfMethodToInvoke >= 0) + { + try + { + args[argsIndex] = methodAccess.invoke(null, + indexOfMethodToInvoke, segments.get(i)); + } + catch (IllegalArgumentException iae) + { + // In the case where the developer has specified that only + // enumerated values should be accepted as input, either + // one of those values needs to exist in the URI, or this + // IllegalArgumentException will be thrown. We will limp + // on and pass a null in this case. + args[argsIndex] = null; + } + } + else + { + // We don't know the type, so we cannot create it. + args[argsIndex] = null; + } + } + // Bump argsIndex + argsIndex ++; + } + } + + // Handle adapting and injecting the request body if configured. + final BasicPathHandler.RequestBodyParameter bodyParameter = method.bodyParameter; + if (bodyParameter != null && argsIndex < args.length) + { + args[argsIndex] = bodyParameter.readBody(context); + } + + return args; + } + + private static boolean hasStringInputMethod(Class type, + MethodAccess methodAccess, + String methodName) { + String[] methodNames = methodAccess.getMethodNames(); + Class[][] parameterTypes = methodAccess.getParameterTypes(); + for (int index = 0; index < methodNames.length; index++) + { + String foundMethodName = methodNames[index]; + Class[] params = parameterTypes[index]; + if (foundMethodName.equals(methodName) + && params.length == 1 + && params[0].equals(String.class)) + { + try + { + // Only bother with the slowness of normal reflection if + // the method passes all the other checks. + Method method = type.getMethod(methodName, String.class); + if (Modifier.isStatic(method.getModifiers())) + { + return true; + } + } + catch (NoSuchMethodException e) + { + // Should not happen + } + } + } + return false; + } + + + protected static class PathUriTree + { + private final AnnotationHandler.PathUriTree.Node root; + + public PathUriTree() + { + root = new AnnotationHandler.PathUriTree.Node(null); + } + + /** + * Searches the tree for a node that best handles the given segments. + */ + public final AnnotationHandler.PathUriMethod search(PathSegments segments) + { + return search(root, segments, 0); + } + + /** + * Searches the given segments at the given offset with the given node + * in the tree. If this node is a leaf node and matches the segment + * stack perfectly, it is returned. If this node is a leaf node and + * either a variable or a wildcard node and the segment stack has run + * out of segments to check, return that if we have not found a true + * match. + */ + private AnnotationHandler.PathUriMethod search(AnnotationHandler.PathUriTree.Node node, PathSegments segments, int offset) + { + if (node != root && + offset >= segments.getCount()) + { + // Last possible depth; must be a leaf node + if (node.method != null) + { + return node.method; + } + return null; + } + else + { + // Not yet at a leaf node + AnnotationHandler.PathUriMethod bestVariable = null; // Best at this depth + AnnotationHandler.PathUriMethod bestWildcard = null; // Best at this depth + AnnotationHandler.PathUriMethod toReturn = null; + for (AnnotationHandler.PathUriTree.Node child : node.children) + { + // Only walk the path that can handle the new segment. + if (child.segment.segment.equals(segments.get(offset,""))) + { + // Direct hits only happen here. + toReturn = search(child, segments, offset + 1); + } + else if (child.segment.isVariable) + { + // Variables are not necessarily leaf nodes. + AnnotationHandler.PathUriMethod temp = search(child, segments, offset + 1); + // We may be at a variable node, but not the variable + // path segment handler method. Don't set it in this case. + if (temp != null) + { + bestVariable = temp; + } + } + else if (child.segment.isWildcard) + { + // Wildcards are leaf nodes by design. + bestWildcard = child.method; + } + } + // By here, we are as deep as we can be. + if (toReturn == null && bestVariable != null) + { + // Could not find a direct route + toReturn = bestVariable; + } + else if (toReturn == null && bestWildcard != null) + { + toReturn = bestWildcard; + } + return toReturn; + } + } + + /** + * Adds the given PathUriMethod to this tree at the + * appropriate depth. + */ + public final void addMethod(AnnotationHandler.PathUriMethod method) + { + root.addChild(root, method, 0); + } + + /** + * A node in the tree of PathUriMethod. + */ + public static class Node + { + private AnnotationHandler.PathUriMethod method; + private final AnnotationHandler.PathUriMethod.UriSegment segment; + private final List children; + + public Node(AnnotationHandler.PathUriMethod.UriSegment segment) + { + this.segment = segment; + this.children = new ArrayList<>(); + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder() + .append("{") + .append("method: ") + .append(method) + .append(", segment: ") + .append(segment) + .append(", childrenCount: ") + .append(this.children.size()) + .append("}"); + + return sb.toString(); + } + + /** + * Returns the immediate child node for the given segment and creates + * if it does not exist. + */ + private AnnotationHandler.PathUriTree.Node getChildForSegment(AnnotationHandler.PathUriTree.Node node, AnnotationHandler.PathUriMethod.UriSegment[] segments, int offset) + { + AnnotationHandler.PathUriTree.Node toRet = null; + for(AnnotationHandler.PathUriTree.Node child : node.children) + { + if (child.segment.segment.equals(segments[offset].segment)) + { + toRet = child; + break; + } + } + if (toRet == null) + { + // Add a new node at this segment to return. + toRet = new AnnotationHandler.PathUriTree.Node(segments[offset]); + node.children.add(toRet); + } + return toRet; + } + + /** + * Recursively adds the given PathUriMethod to this tree at the + * appropriate depth. + */ + private void addChild(AnnotationHandler.PathUriTree.Node node, AnnotationHandler.PathUriMethod uriMethod, int offset) + { + if (uriMethod.segments.length > offset) + { + final AnnotationHandler.PathUriTree.Node child = getChildForSegment(node, uriMethod.segments, offset); + if (uriMethod.segments.length == offset + 1) + { + child.method = uriMethod; + } + else + { + this.addChild(child, uriMethod, offset + 1); + } + } + } + + /** + * Returns the PathUriMethod for this node. + * May be null. + */ + public final AnnotationHandler.PathUriMethod getMethod() + { + return this.method; + } + } + } + + /** + * Details of an annotated path segment method. + */ + protected static class PathUriMethod extends BasicPathHandler.BasicPathHandlerMethod + { + public final Method method; + public final String uri; + public final AnnotationHandler.PathUriMethod.UriSegment[] segments; + public final int index; + + public PathUriMethod(Method method, String uri, Request.HttpMethod httpMethod, + MethodAccess methodAccess) + { + super(method, httpMethod); + + this.method = method; + this.uri = uri; + this.segments = this.parseSegments(this.uri); + int variableCount = 0; + final Class[] classes = + new Class[method.getGenericParameterTypes().length]; + for (AnnotationHandler.PathUriMethod.UriSegment segment : segments) + { + if (segment.isVariable) + { + classes[variableCount] = + (Class)method.getGenericParameterTypes()[variableCount]; + segment.type = classes[variableCount]; + if (!segment.type.isPrimitive()) + { + segment.methodAccess = MethodAccess.get(segment.type); + } + // Bump variableCount + variableCount ++; + } + } + + // Check for and configure the method to receive a parameter for the + // request body. If desired, it's expected that the body parameter is + // the last one. So it's only worth checking if variableCount indicates + // that there's room left in the classes array. If there is a mismatch + // where there is another parameter and no @Body annotation, or there is + // a @Body annotation and no extra parameter for it, the below checks + // will find that and throw accordingly. + if (variableCount < classes.length && this.bodyParameter != null) + { + classes[variableCount] = method.getParameterTypes()[variableCount]; + variableCount++; + } + + if (variableCount == 0) + { + try + { + this.index = methodAccess.getIndex(method.getName(), + ReflectionHelper.NO_PARAMETERS); + } + catch(IllegalArgumentException e) + { + throw new IllegalArgumentException("Methods with argument " + + "variables must have @Path annotations with matching " + + "variable capture(s) (ex: @Path(\"{var}\"). See " + + getClass().getName() + "#" + method.getName()); + } + } + else + { + if (classes.length == variableCount) + { + this.index = methodAccess.getIndex(method.getName(), classes); + } + else + { + throw new IllegalAccessError("@Path annotations with variable " + + "notations must have method parameters to match. See " + + getClass().getName() + "#" + method.getName()); + } + } + } + + private AnnotationHandler.PathUriMethod.UriSegment[] parseSegments(String uriToParse) + { + String[] segmentStrings = uriToParse.split("/"); + final AnnotationHandler.PathUriMethod.UriSegment[] uriSegments = new AnnotationHandler.PathUriMethod.UriSegment[segmentStrings.length]; + + for (int i = 0; i < segmentStrings.length; i++) + { + uriSegments[i] = new AnnotationHandler.PathUriMethod.UriSegment(segmentStrings[i]); + } + + return uriSegments; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(); + boolean empty = true; + for (AnnotationHandler.PathUriMethod.UriSegment segment : segments) + { + if (!empty) + { + sb.append(","); + } + sb.append(segment.toString()); + empty = false; + } + + return "PSM [" + method.getName() + "; " + httpMethod + "; " + + index + "; " + sb.toString() + "]"; + } + + protected static class UriSegment + { + public static final String WILDCARD = "*"; + public static final String VARIABLE_PREFIX = "{"; + public static final String VARIABLE_SUFFIX = "}"; + public static final String EMPTY = ""; + + public final boolean isWildcard; + public final boolean isVariable; + public final String segment; + public Class type; + public MethodAccess methodAccess; + + public UriSegment(String segment) + { + this.isWildcard = segment.equals(WILDCARD); + this.isVariable = segment.startsWith(VARIABLE_PREFIX) + && segment.endsWith(VARIABLE_SUFFIX); + if (this.isVariable) + { + // Minor optimization - no reason to potentially create multiple + // nodes all of which are variables since the inside of the variable + // is ignored in the end. Treating the segment of all variable nodes + // as "{}" regardless of whether the actual segment is "{var}" or + // "{foo}" forces all branches with variables at a given depth to + // traverse the same sub-tree. That is, "{var}/foo" and "{var}/bar" + // as the only two annotated methods in a handler will result in a + // maximum of 3 comparisons instead of 4. Mode variables at same + // depths would make this optimization felt more strongly. + this.segment = VARIABLE_PREFIX + VARIABLE_SUFFIX; + } + else + { + this.segment = segment; + } + } + + public final String getVariableName() + { + if (this.isVariable) + { + return this.segment + .replace(AnnotationHandler.PathUriMethod.UriSegment.VARIABLE_PREFIX, AnnotationHandler.PathUriMethod.UriSegment.EMPTY) + .replace(AnnotationHandler.PathUriMethod.UriSegment.VARIABLE_SUFFIX, AnnotationHandler.PathUriMethod.UriSegment.EMPTY); + } + + return null; + } + + @Override + public String toString() + { + return "{segment: '" + segment + + "', isVariable: " + isVariable + + ", isWildcard: " + isWildcard + "}"; + } + } + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 5edb7e64..89e5b7ba 100755 --- a/pom.xml +++ b/pom.xml @@ -93,6 +93,7 @@ 2.13.0 1.3.0-alpha2 2.1.6 + 1.10.19 @@ -313,6 +314,12 @@ flyway-core ${flyway.version} + + org.mockito + mockito-all + ${mockito.version} + test + From 74b4031ea748b32c2760f62e60b4a17c8c95fffe Mon Sep 17 00:00:00 2001 From: ajohnston Date: Fri, 17 Apr 2020 18:02:06 -0700 Subject: [PATCH 06/12] Add support for regex variables --- .../gemini/jaxrs/core/JaxRsDispatcher.java | 142 +++++++++++++++--- .../jaxrs/core/JaxRsDispatcherTest.java | 101 +++++++++++++ 2 files changed, 220 insertions(+), 23 deletions(-) diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java index 6c2498be..55df7c08 100644 --- a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java @@ -96,6 +96,11 @@ public RegexVariableBlockToken(String name, String regex) } } + static class SlashToken + { + // Basically just a flag. + } + private void registerEndpoint(String path, Endpoint endpoint) { // TODO: Might want to gracefully handle the case where a resource could @@ -117,9 +122,10 @@ private void registerEndpoint(String path, Endpoint endpoint) Pattern wordBlockToken = Pattern.compile("^[^/{]+"); Pattern variableBlockToken = Pattern.compile("^\\{[ \t]*(?\\w[\\w.-]*)[ \t]*(:[ \t]*(?([^{}]|\\{[^{}]*})*)[ \t]*)?}"); DispatchBlock currentBlock = rootBlock; + List tokensSinceLastSlash = new ArrayList<>(); + boolean regexBlockMode = false; while (!remainingPath.isEmpty()) { - List tokensSinceLastSlash = new ArrayList<>(); while (remainingPath.length() > 0 && remainingPath.charAt(0) != '/') { { @@ -202,14 +208,12 @@ else if (token instanceof PureVariableBlockToken) } else { - // TODO: Form a regex block - throw new UnsupportedOperationException("TODO"); + regexBlockMode = true; } } else { - // TODO: Form a regex block - throw new UnsupportedOperationException("TODO"); + regexBlockMode = true; } if (!remainingPath.isEmpty()) { @@ -223,8 +227,30 @@ else if (token instanceof PureVariableBlockToken) { parsedPath.append('/'); remainingPath = remainingPath.substring(1); + if (regexBlockMode) + { + tokensSinceLastSlash.add(new SlashToken()); + } } } + if (!regexBlockMode) + { + tokensSinceLastSlash.clear(); + } + } + if (regexBlockMode) + { + RegexVariableDispatchBlock block = new RegexVariableDispatchBlock( + tokensSinceLastSlash); + if (currentBlock.regexChildren.stream() + .map(RegexVariableDispatchBlock::getPatternString) + .anyMatch(block.getPatternString()::equals)) + { + throw new RuntimeException("Duplicate regex block found: " + + block.getPatternString()); + } + currentBlock.regexChildren.add(block); + currentBlock = block; } Set httpMethods = new HashSet<>(); for (Annotation annotation : endpoint.method.getAnnotations()) @@ -265,17 +291,17 @@ static class DispatchMatch { private final DispatchBlock block; private final Endpoint endpoint; - final String value; + final Map values; final List matchChildren; public DispatchMatch(DispatchBlock block, Endpoint endpoint, - String value, + Map values, List matchChildren) { this.block = block; this.endpoint = endpoint; - this.value = value; + this.values = values; this.matchChildren = matchChildren; } @@ -294,9 +320,9 @@ List getLeafValues() .flatMap(Collection::stream) .collect(Collectors.toList()); } - if (value != null) + if (values != null) { - return List.of(value); + return List.copyOf(values.values()); } return null; } @@ -317,16 +343,9 @@ List getLeafMatches() void getBestMatch(DispatchBestMatchInfo info) { - if (block instanceof FullSegmentPureVariableDispatchBlock) + if (values != null) { - String name = ((FullSegmentPureVariableDispatchBlock) block).name; - info.values.put(name, value); - } - else if (block instanceof RegexVariableDispatchBlock) - { - // TODO: Need to do some refactoring b/c the regex variable dispatch - // blocks will be capable of having multiple name/value entries. - throw new UnsupportedOperationException("TODO"); + info.values.putAll(values); } if (matchChildren != null) { @@ -513,7 +532,8 @@ DispatchMatch getDispatchMatch(String httpMethod, Endpoint endpoint = getMatchingEndpoint(httpMethod); if (endpoint != null) { - return new DispatchMatch(this, endpoint, segments[index], null); + return new DispatchMatch(this, endpoint, + Map.of(name, segments[index]), null); } return null; } @@ -526,7 +546,8 @@ DispatchMatch getDispatchMatch(String httpMethod, segments, index + 1); if (childMatches != null) { - return new DispatchMatch(this, null, segments[index], childMatches); + return new DispatchMatch(this, null, + Map.of(name, segments[index]), childMatches); } else { @@ -538,13 +559,88 @@ DispatchMatch getDispatchMatch(String httpMethod, static class RegexVariableDispatchBlock extends DispatchBlock { + private final Pattern pattern; + private final Map variableNameToGroupName = new HashMap<>(); + + static String generateGroupName() + { + return "g" + UUID.randomUUID().toString().replaceAll("-", ""); + } + + public RegexVariableDispatchBlock(List tokens) + { + StringBuilder patternString = new StringBuilder(); + for (Object token : tokens) + { + if (token instanceof WordBlockToken) + { + // TODO: Encode and escape stuff. + patternString.append(((WordBlockToken) token).word); + } + else if (token instanceof PureVariableBlockToken) + { + String groupName = generateGroupName(); + // TODO: Encode and escape stuff. + String pattern = String.format("(?<%s>[^/]+?)", groupName); + String name = ((PureVariableBlockToken) token).name; + patternString.append(pattern); + variableNameToGroupName.put(name, groupName); + } + else if (token instanceof RegexVariableBlockToken) + { + String groupName = generateGroupName(); + String name = ((RegexVariableBlockToken) token).name; + String regex = ((RegexVariableBlockToken) token).regex; + // TODO: Encode and escape stuff. + String pattern = String.format("(?<%s>%s)", groupName, regex); + patternString.append(pattern); + variableNameToGroupName.put(name, groupName); + } + else if (token instanceof SlashToken) + { + patternString.append("/"); + } + else + { + throw new RuntimeException("Unexpected token type."); + } + } + if (!patternString.toString().endsWith("/")) { + patternString.append("/"); + } + pattern = Pattern.compile(patternString.toString()); + } + + String getPatternString() + { + return pattern.pattern(); + } + @Override DispatchMatch getDispatchMatch(String httpMethod, String[] segments, int index) { - // TODO - throw new UnsupportedOperationException(); + String uri = String.join("/", segments); + if (!uri.endsWith("/")) { + uri += "/"; + } + Matcher matcher = pattern.matcher(uri); + if (matcher.find()) + { + Map values = new HashMap<>(); + for (var entry : variableNameToGroupName.entrySet()) + { + values.put(entry.getKey(), matcher.group(entry.getValue())); + } + // If this has a matching endpoint, consider it a dispatch candidate + Endpoint endpoint = getMatchingEndpoint(httpMethod); + if (endpoint != null) + { + return new DispatchMatch(this, endpoint, values, null); + } + } + return null; } } diff --git a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java index 7c012a6c..0199d755 100644 --- a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java +++ b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java @@ -72,6 +72,19 @@ public String doIt(@PathParam("test1") String test1, } } + @Path("/foo") + public static class TestCase5B + { + @GET + @Path("/{test1}-dog-{test2}") + public String doIt(@PathParam("test1") String test1, + @PathParam("test2") String test2) + { + return "did-it-test-case-5B" + + String.format("{test1:%s,test2:%s}", test1, test2); + } + } + @Path("foo") public static class FooResource { @@ -176,6 +189,33 @@ public void pathParamsShouldBeProvided() dispatcher.dispatch(HttpMethod.GET, "/dog/bar")); } + @Test + public void pathParamRegexesShouldBeSupported1() + { + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestCase4()); + assertEquals("did-it-test-case-449cat", + dispatcher.dispatch(HttpMethod.GET, "/49-dog-cat/bar")); + } + + @Test + public void pathParamRegexesShouldBeSupported2() + { + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestCase5()); + assertEquals("did-it-test-case-549cat", + dispatcher.dispatch(HttpMethod.GET, "/49/-dog-cat/bar")); + } + + @Test + public void pathParamRegexesShouldBeSupported3() + { + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestCase5B()); + assertEquals("did-it-test-case-5B{test1:-dog-,test2:-dog--dog-}", + dispatcher.dispatch(HttpMethod.GET, "/foo/-dog--dog--dog--dog-")); + } + @Test public void uuidPathParamsShouldBeSupported() { @@ -356,6 +396,67 @@ public void perfTestJaxRs() + (System.currentTimeMillis() - start) + "ms"); } + @Test + public void perfTestMaps() + { + Map>> foo = Map.of("foo", + Map.of("bar", Map.of(HttpMethod.GET, new Object()))); + String uriNoTrailingSlash = "foo/bar"; + long start; + for (int i = 0; i < ITERATIONS; i++) + { + String[] uriSegments = uriNoTrailingSlash.split("/"); + if (foo.containsKey(uriSegments[0])) + { + Map> inner = foo.get(uriSegments[0]); + if (inner.containsKey(uriSegments[1])) + { + Map endpointsByHttpMethod = inner.get( + uriSegments[1]); + Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + } + } + } + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + String[] uriSegments = uriNoTrailingSlash.split("/"); + if (foo.containsKey(uriSegments[0])) + { + Map> inner = foo.get(uriSegments[0]); + if (inner.containsKey(uriSegments[1])) + { + Map endpointsByHttpMethod = inner.get( + uriSegments[1]); + Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + } + } + } + System.out.println("Total time map a: " + + (System.currentTimeMillis() - start) + "ms"); + Map> fooB = Map.of("foo/bar", + Map.of(HttpMethod.GET, new Object())); + for (int i = 0; i < ITERATIONS; i++) + { + if (fooB.containsKey(uriNoTrailingSlash)) + { + Map endpointsByHttpMethod = fooB.get(uriNoTrailingSlash); + Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + } + } + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + if (fooB.containsKey(uriNoTrailingSlash)) + { + Map endpointsByHttpMethod = fooB.get(uriNoTrailingSlash); + Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + } + } + System.out.println("Total time map b: " + + (System.currentTimeMillis() - start) + "ms"); + } + @Test public void perfTestAll() { From a092bcf517e4b035c5e83b560e651e89c23b73b7 Mon Sep 17 00:00:00 2001 From: ajohnston Date: Fri, 24 Apr 2020 17:07:51 -0700 Subject: [PATCH 07/12] Add semi-temporary performance tests A lot of this will be removed eventually. Committing it just makes it easier to distinguish from any new functionality and some near-future refactors while doing dev. --- gemini-jax-rs/pom.xml | 13 + .../gemini/jaxrs/core/JaxRsDispatcher.java | 137 +++++++- .../jaxrs/core/JaxRsDispatcherTest.java | 318 ++++++++++++++---- pom.xml | 6 + 4 files changed, 389 insertions(+), 85 deletions(-) diff --git a/gemini-jax-rs/pom.xml b/gemini-jax-rs/pom.xml index 04b76724..7beeda8b 100644 --- a/gemini-jax-rs/pom.xml +++ b/gemini-jax-rs/pom.xml @@ -48,6 +48,19 @@ mockito-all test + + + com.caucho + resin + false + + + com.github.ben-manes.caffeine + caffeine + \ No newline at end of file diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java index 55df7c08..2819bee1 100644 --- a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java @@ -1,6 +1,11 @@ package com.techempower.gemini.jaxrs.core; +import com.caucho.util.LruCache; import com.esotericsoftware.reflectasm.MethodAccess; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; import javax.ws.rs.HttpMethod; import javax.ws.rs.Path; @@ -10,6 +15,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.*; +import java.util.concurrent.ExecutionException; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -17,7 +23,51 @@ public class JaxRsDispatcher { private List endpoints; + private LoadingCache bestMatches = CacheBuilder.newBuilder() + .maximumSize(100) + .build(new CacheLoader<>() { + @Override public DispatchBestMatchInfo load(DispatchMatchSource dispatchMatchSource) throws Exception { + return getBestMatchInfo(dispatchMatchSource); + } + }); DispatchBlock rootBlock = new RootDispatchBlock(); + LruCache bestMatchesResinCache = new LruCache<>(100); + com.github.benmanes.caffeine.cache.LoadingCache bestMatchesCaff = Caffeine.newBuilder() + .maximumSize(100) + .build(this::getBestMatchInfo); + private boolean cacheEnabled; + private boolean useResinCache; + private boolean useCaffCache; + + public boolean isCacheEnabled() + { + return cacheEnabled; + } + + public void setCacheEnabled(boolean cacheEnabled) + { + this.cacheEnabled = cacheEnabled; + } + + public boolean isUseResinCache() + { + return useResinCache; + } + + public void setUseResinCache(boolean useResinCache) + { + this.useResinCache = useResinCache; + } + + public boolean isUseCaffCache() + { + return useCaffCache; + } + + public void setUseCaffCache(boolean useCaffCache) + { + this.useCaffCache = useCaffCache; + } public void register(Object instance) { @@ -362,6 +412,11 @@ static class DispatchBestMatchInfo { private Endpoint endpoint; private Map values = new HashMap<>(); + + public Map getValues() + { + return values; + } } abstract static class DispatchBlock @@ -644,6 +699,32 @@ DispatchMatch getDispatchMatch(String httpMethod, } } + static class DispatchMatchSource { + private final String uri; + private final String httpMethod; + + public DispatchMatchSource(String httpMethod, String uri) + { + this.uri = uri; + this.httpMethod = httpMethod; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DispatchMatchSource that = (DispatchMatchSource) o; + return uri.equals(that.uri) && + httpMethod.equals(that.httpMethod); + } + + @Override + public int hashCode() + { + return Objects.hash(uri, httpMethod); + } + } interface Resource { @@ -732,28 +813,60 @@ DispatchMatch getDispatchMatches(String httpMethod, String uri) return rootBlock.getDispatchMatch(httpMethod, segments, 0); } - // TODO: Not use Gemini's Context eventually, probably. - //public void dispatch(Context context) - public Object dispatch(String httpMethod, String uri) - { - // TODO: Add `getServletPath` as a method in Context so that this can - // only match the URI relative to where the servlet is hosted. - // `getServletPath` is a method of HttpServletRequest. For - // non-servlet containers this can just default to "" or "/". Find - // out what the default is for Resin and use that. + public DispatchBestMatchInfo getBestMatchInfo(DispatchMatchSource dispatchMatchSource) { + String uri = dispatchMatchSource.uri; + String httpMethod = dispatchMatchSource.httpMethod; int uriStart = uri.startsWith("/") ? 1 : 0; int uriEnd = uri.length() - (uri.endsWith("/") ? 1 : 0); String normalizedUri = uri.substring(uriStart, uriEnd); DispatchMatch dispatchMatch = getDispatchMatches(httpMethod, normalizedUri); - + DispatchBestMatchInfo matchInfo = new DispatchBestMatchInfo(); // TODO: Need to filter the dispatch match down to only the correct one so // the path params can be extracted. There's a TO-DO...somewhere in this // file (or maybe the test) for how to do that naturally and only come out // with the right match per the sorting rules, and not have to deal with // the tree of matches. - - DispatchBestMatchInfo matchInfo = new DispatchBestMatchInfo(); dispatchMatch.getBestMatch(matchInfo); + return matchInfo; + } + + public DispatchBestMatchInfo getBestMatchInfo(String httpMethod, String uri) { + DispatchMatchSource matchSource = new DispatchMatchSource(httpMethod, uri); + if (cacheEnabled) { + if (useResinCache) { + DispatchBestMatchInfo matchInfo = bestMatchesResinCache.get(matchSource); + if (matchInfo == null) { + matchInfo = getBestMatchInfo(matchSource); + bestMatchesResinCache.put(matchSource, matchInfo); + } + return matchInfo; + } else if (useCaffCache) { + return bestMatchesCaff.get(matchSource); + } else { + try + { + return bestMatches.get(matchSource); + } + catch (ExecutionException e) + { + throw new RuntimeException(e); + } + } + } + return getBestMatchInfo(matchSource); + } + + // TODO: Not use Gemini's Context eventually, probably. + //public void dispatch(Context context) + public Object dispatch(String httpMethod, String uri) + { + // TODO: Add `getServletPath` as a method in Context so that this can + // only match the URI relative to where the servlet is hosted. + // `getServletPath` is a method of HttpServletRequest. For + // non-servlet containers this can just default to "" or "/". Find + // out what the default is for Resin and use that. + DispatchBestMatchInfo matchInfo = getBestMatchInfo(httpMethod, uri); + Method method = matchInfo.endpoint.method; // TODO: For now I'm just gonna support valueOf for simplicity, // and assume it's static. diff --git a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java index 0199d755..58ddf783 100644 --- a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java +++ b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java @@ -332,12 +332,38 @@ static class Foo { public static void main(String... args) { JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); jaxRsDispatcher.register(new FooResource()); + JaxRsDispatcher jaxRsDispatcherCached = new JaxRsDispatcher(); + jaxRsDispatcherCached.setCacheEnabled(true); + jaxRsDispatcherCached.register(new FooResource()); + JaxRsDispatcher jaxRsDispatcherResinCached = new JaxRsDispatcher(); + jaxRsDispatcherResinCached.setCacheEnabled(true); + jaxRsDispatcherResinCached.setUseResinCache(true); + jaxRsDispatcherResinCached.register(new FooResource()); + JaxRsDispatcher jaxRsDispatcherCaffCached = new JaxRsDispatcher(); + jaxRsDispatcherCaffCached.setCacheEnabled(true); + jaxRsDispatcherCaffCached.setUseCaffCache(true); + jaxRsDispatcherCaffCached.register(new FooResource()); JaxRsDispatcherTest test = new JaxRsDispatcherTest(); test.warmUpJaxRs(jaxRsDispatcher); + test.warmUpJaxRs(jaxRsDispatcherCached); + test.warmUpJaxRs(jaxRsDispatcherResinCached); + test.warmUpJaxRs(jaxRsDispatcherCaffCached); long start; start = System.currentTimeMillis(); test.justJaxRs(jaxRsDispatcher); - System.out.println("Total time dispatchBlocks: " + System.out.println("Total time dispatchBlocks w/o cache: " + + (System.currentTimeMillis() - start) + "ms"); + start = System.currentTimeMillis(); + test.justJaxRsCached(jaxRsDispatcherCached); + System.out.println("Total time dispatchBlocks w/ google cache: " + + (System.currentTimeMillis() - start) + "ms"); + start = System.currentTimeMillis(); + test.justJaxRsResinCached(jaxRsDispatcherResinCached); + System.out.println("Total time dispatchBlocks w/ resin cache: " + + (System.currentTimeMillis() - start) + "ms"); + start = System.currentTimeMillis(); + test.justJaxRsCaffCached(jaxRsDispatcherCaffCached); + System.out.println("Total time dispatchBlocks w/ caffeine cache: " + (System.currentTimeMillis() - start) + "ms"); } } @@ -347,13 +373,13 @@ public void warmUpJaxRs(JaxRsDispatcher jaxRsDispatcher) String uriNoTrailingSlash = "foo/bar"; for (int i = 0; i < ITERATIONS; i++) { - jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uriNoTrailingSlash); if (i == 0) { - JaxRsDispatcher.DispatchMatch match = jaxRsDispatcher - .getDispatchMatches(HttpMethod.GET, uriNoTrailingSlash); - System.out.println("Found " + match.getLeafValues()); + JaxRsDispatcher.DispatchBestMatchInfo match = jaxRsDispatcher + .getBestMatchInfo(HttpMethod.GET, uriNoTrailingSlash); + System.out.println("Found " + match.getValues()); } } } @@ -363,7 +389,37 @@ public void justJaxRs(JaxRsDispatcher jaxRsDispatcher) String uriNoTrailingSlash = "foo/bar"; for (int i = 0; i < ITERATIONS; i++) { - jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, + uriNoTrailingSlash); + } + } + + public void justJaxRsCached(JaxRsDispatcher jaxRsDispatcher) + { + String uriNoTrailingSlash = "foo/bar"; + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, + uriNoTrailingSlash); + } + } + + public void justJaxRsResinCached(JaxRsDispatcher jaxRsDispatcher) + { + String uriNoTrailingSlash = "foo/bar"; + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, + uriNoTrailingSlash); + } + } + + public void justJaxRsCaffCached(JaxRsDispatcher jaxRsDispatcher) + { + String uriNoTrailingSlash = "foo/bar"; + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uriNoTrailingSlash); } } @@ -376,20 +432,20 @@ public void perfTestJaxRs() jaxRsDispatcher.register(new FooResource()); for (int i = 0; i < ITERATIONS; i++) { - jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uriNoTrailingSlash); if (i == 0) { - JaxRsDispatcher.DispatchMatch match = jaxRsDispatcher - .getDispatchMatches(HttpMethod.GET, uriNoTrailingSlash); - System.out.println("Found " + match.getLeafValues()); + JaxRsDispatcher.DispatchBestMatchInfo match = jaxRsDispatcher + .getBestMatchInfo(HttpMethod.GET, uriNoTrailingSlash); + System.out.println("Found " + match.getValues()); } } long start; start = System.currentTimeMillis(); for (int i = 0; i < ITERATIONS; i++) { - jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uriNoTrailingSlash); } System.out.println("Total time dispatchBlocks: " @@ -504,13 +560,12 @@ public void perfTestAll() start = System.currentTimeMillis(); for (int i = 0; i < ITERATIONS; i++) { - jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, - uriNoTrailingSlash); + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); if (i == 0) { - JaxRsDispatcher.DispatchMatch match = jaxRsDispatcher - .getDispatchMatches(HttpMethod.GET, uri); - System.out.println("Found " + match.getLeafValues()); + JaxRsDispatcher.DispatchBestMatchInfo match = jaxRsDispatcher + .getBestMatchInfo(HttpMethod.GET, uri); + System.out.println("Found " + match.getValues()); } } System.out.println("Total time dispatchBlocks: " @@ -537,12 +592,13 @@ public void perfTestAll() } System.out.println("Total time regex fast: " + (System.currentTimeMillis() - start) + "ms"); + String path = "foo/bar"; Map>> foo = Map.of("foo", Map.of("bar", Map.of(HttpMethod.GET, new Object()))); start = System.currentTimeMillis(); for (int i = 0; i < ITERATIONS; i++) { - String[] uriSegments = "foo/bar".split("/"); + String[] uriSegments = path.split("/"); if (foo.containsKey(uriSegments[0])) { Map> inner = foo.get(uriSegments[0]); @@ -554,84 +610,116 @@ public void perfTestAll() } } } - System.out.println("Total time map: " + System.out.println("Total time tree-map approach (w/ URI split): " + + (System.currentTimeMillis() - start) + "ms"); + Map> foo2 = Map.of("foo/bar", Map.of(HttpMethod.GET, new Object())); + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + String[] uriSegments = path.split("/"); + if (foo2.containsKey(path)) + { + Map endpointsByHttpMethod = foo2.get(path); + Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + } + } + System.out.println("Total time flat-map approach w/ URI split: " + + (System.currentTimeMillis() - start) + "ms"); + Map> foo3 = Map.of("foo/bar", Map.of(HttpMethod.GET, new Object())); + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + //String[] uriSegments = path.split("/"); + if (foo3.containsKey(path)) + { + Map endpointsByHttpMethod = foo3.get(path); + Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + } + } + System.out.println("Total time flat-map approach w/o URI split: " + (System.currentTimeMillis() - start) + "ms"); { String uri = "foo/bar/"; - // This is better for the map approach - String uriNoTrailingSlash = "foo/bar"; JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); jaxRsDispatcher.register(new FooResource()); start = System.currentTimeMillis(); for (int i = 0; i < ITERATIONS; i++) { - jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, - uriNoTrailingSlash); + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); } - System.out.println("Total time dispatchBlocks: " + System.out.println("Total time dispatchBlocks w/o LRU cache: " + (System.currentTimeMillis() - start) + "ms"); } { String uri = "foo/bar/"; - // This is better for the map approach - String uriNoTrailingSlash = "foo/bar"; JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.setCacheEnabled(true); jaxRsDispatcher.register(new FooResource()); start = System.currentTimeMillis(); for (int i = 0; i < ITERATIONS; i++) { - // TODO: Can possibly optimize so that it just naturally checks for - // empty strings for the final block to see if it should end early, - // thus avoiding the need to manipulate the string - int uriStart = uri.startsWith("/") ? 1 : 0; - int uriEnd = uri.length() - (uri.endsWith("/") ? 1 : 0); - String normalizedUri = uri.substring(uriStart, uriEnd); - jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, normalizedUri); + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); } - System.out.println("Total time dispatchBlocks: " + System.out.println("Total time dispatchBlocks w/ LRU cache: " + (System.currentTimeMillis() - start) + "ms"); } { String uri = "foo/bar/"; - // This is better for the map approach - String uriNoTrailingSlash = "foo/bar"; JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.setCacheEnabled(true); + jaxRsDispatcher.setUseResinCache(true); jaxRsDispatcher.register(new FooResource()); start = System.currentTimeMillis(); for (int i = 0; i < ITERATIONS; i++) { - // TODO: Can possibly optimize so that it just naturally checks for - // empty strings for the final block to see if it should end early, - // thus avoiding the need to manipulate the string - int uriStart = 0; - int uriEnd = 7; - String normalizedUri = uri.substring(uriStart, uriEnd); - jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, normalizedUri); + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); } - System.out.println("Total time dispatchBlocks: " + System.out.println("Total time dispatchBlocks w/ resin LRU cache: " + + (System.currentTimeMillis() - start) + "ms"); + } + { + String uri = "foo/bar/"; + JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.setCacheEnabled(true); + jaxRsDispatcher.setUseCaffCache(true); + jaxRsDispatcher.register(new FooResource()); + + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); + } + System.out.println("Total time dispatchBlocks w/ caffeine cache: " + (System.currentTimeMillis() - start) + "ms"); } } } - public static void main(String... args) - { - JaxRsDispatcherTest test = new JaxRsDispatcherTest(); - //test.perfTestAllComplicated(); - Map>> foo = Map.of("foo", - Map.of("bar", Map.of(HttpMethod.GET, new Object()))); - JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); - jaxRsDispatcher.register(new FooResource()); - test.combinedWarmUp(foo, jaxRsDispatcher); - test.mapApproach(foo); - test.dispatcherBlocksApproach(jaxRsDispatcher); + public static class Bar { + public static void main(String... args) + { + JaxRsDispatcherTest test = new JaxRsDispatcherTest(); + //test.perfTestAllComplicated(); + Map>> foo = Map.of("foo", + Map.of("bar", Map.of(HttpMethod.GET, new Object()))); + JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.register(new FooResource()); + JaxRsDispatcher jaxRsDispatcherCached = new JaxRsDispatcher(); + jaxRsDispatcherCached.setCacheEnabled(true); + jaxRsDispatcherCached.register(new FooResource()); + test.combinedWarmUp(foo, jaxRsDispatcher, jaxRsDispatcherCached); + test.mapApproach(foo); + test.dispatcherBlocksApproach(jaxRsDispatcher); + test.dispatcherBlocksCachedApproach(jaxRsDispatcherCached); + } } public void combinedWarmUp(Map>> foo, - JaxRsDispatcher jaxRsDispatcher) + JaxRsDispatcher jaxRsDispatcher, + JaxRsDispatcher jaxRsDispatcherCached) { String uriNoTrailingSlash = "foo/bar"; for (int i = 0; i < ITERATIONS; i++) @@ -649,7 +737,12 @@ public void combinedWarmUp(Map>> foo, } for (int i = 0; i < ITERATIONS; i++) { - jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, uriNoTrailingSlash); + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uriNoTrailingSlash); + } + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcherCached.getBestMatchInfo(HttpMethod.GET, + uriNoTrailingSlash); } } @@ -682,9 +775,22 @@ public void dispatcherBlocksApproach(JaxRsDispatcher jaxRsDispatcher) long start = System.currentTimeMillis(); for (int i = 0; i < ITERATIONS; i++) { - jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, uriNoTrailingSlash); + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uriNoTrailingSlash); } - System.out.println("Total time dispatchBlocks: " + System.out.println("Total time dispatchBlocks w/o cache: " + + (System.currentTimeMillis() - start) + "ms"); + } + + public void dispatcherBlocksCachedApproach(JaxRsDispatcher jaxRsDispatcher) + { + // This is better for the blocks approach + String uriNoTrailingSlash = "foo/bar"; + long start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uriNoTrailingSlash); + } + System.out.println("Total time dispatchBlocks w/ cache: " + (System.currentTimeMillis() - start) + "ms"); } @@ -696,8 +802,6 @@ public void perfTestAllComplicated() var uuidGroupNameStr = "g" + UUID.randomUUID().toString().replaceAll("-", ""); var uuidStr = UUID.randomUUID().toString(); String uri = "foo/" + uuidStr + "/"; - // This is better for the map approach - String uriNoTrailingSlash = "foo/" + uuidStr; long start; start = System.currentTimeMillis(); for (int i = 0; i < ITERATIONS; i++) @@ -728,12 +832,56 @@ public void perfTestAllComplicated() start = System.currentTimeMillis(); for (int i = 0; i < ITERATIONS; i++) { - jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, uri); + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); if (i == 0) { - JaxRsDispatcher.DispatchMatch match = jaxRsDispatcher - .getDispatchMatches(HttpMethod.GET, uri); - System.out.println("Found " + match.getLeafValues()); + JaxRsDispatcher.DispatchBestMatchInfo match = jaxRsDispatcher + .getBestMatchInfo(HttpMethod.GET, uri); + System.out.println("Found " + match.getValues()); + } + } + JaxRsDispatcher jaxRsDispatcherCached = new JaxRsDispatcher(); + jaxRsDispatcherCached.setCacheEnabled(true); + jaxRsDispatcherCached.register(new FooResourceVar()); + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcherCached.getBestMatchInfo(HttpMethod.GET, uri); + if (i == 5) + { + JaxRsDispatcher.DispatchBestMatchInfo match = jaxRsDispatcherCached + .getBestMatchInfo(HttpMethod.GET, uri); + System.out.println("Found " + match.getValues()); + } + } + JaxRsDispatcher jaxRsDispatcherCachedResin = new JaxRsDispatcher(); + jaxRsDispatcherCachedResin.setCacheEnabled(true); + jaxRsDispatcherCachedResin.setUseResinCache(true); + jaxRsDispatcherCachedResin.register(new FooResourceVar()); + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcherCachedResin.getBestMatchInfo(HttpMethod.GET, uri); + if (i == 5) + { + JaxRsDispatcher.DispatchBestMatchInfo match = jaxRsDispatcherCachedResin + .getBestMatchInfo(HttpMethod.GET, uri); + System.out.println("Found " + match.getValues()); + } + } + JaxRsDispatcher jaxRsDispatcherCachedCaff = new JaxRsDispatcher(); + jaxRsDispatcherCachedCaff.setCacheEnabled(true); + jaxRsDispatcherCachedCaff.setUseCaffCache(true); + jaxRsDispatcherCachedCaff.register(new FooResourceVar()); + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcherCachedCaff.getBestMatchInfo(HttpMethod.GET, uri); + if (i == 5) + { + JaxRsDispatcher.DispatchBestMatchInfo match = jaxRsDispatcherCachedCaff + .getBestMatchInfo(HttpMethod.GET, uri); + System.out.println("Found " + match.getValues()); } } } @@ -742,7 +890,6 @@ public void perfTestAllComplicated() var uuidStr = UUID.randomUUID().toString(); String uri = "foo/" + uuidStr + "/"; // This is better for the map approach - String uriNoTrailingSlash = "foo/" + uuidStr; long start; start = System.currentTimeMillis(); for (int i = 0; i < ITERATIONS; i++) @@ -770,25 +917,50 @@ public void perfTestAllComplicated() start = System.currentTimeMillis(); for (int i = 0; i < ITERATIONS; i++) { - jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, - uriNoTrailingSlash); + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); } - System.out.println("Total time dispatchBlocks: " + System.out.println("Total time dispatchBlocks w/o LRU cache: " + (System.currentTimeMillis() - start) + "ms"); } { JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.setCacheEnabled(true); jaxRsDispatcher.register(new FooResourceVar()); start = System.currentTimeMillis(); for (int i = 0; i < ITERATIONS; i++) { - int uriStart = uri.startsWith("/") ? 1 : 0; - int uriEnd = uri.length() - (uri.endsWith("/") ? 1 : 0); - String normalizedUri = uri.substring(uriStart, uriEnd); - jaxRsDispatcher.getDispatchMatches(HttpMethod.GET, normalizedUri); + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); } - System.out.println("Total time dispatchBlocks: " + System.out.println("Total time dispatchBlocks w/ LRU cache: " + + (System.currentTimeMillis() - start) + "ms"); + } + { + JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.setCacheEnabled(true); + jaxRsDispatcher.setUseResinCache(true); + jaxRsDispatcher.register(new FooResourceVar()); + + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); + } + System.out.println("Total time dispatchBlocks w/ resin LRU cache: " + + (System.currentTimeMillis() - start) + "ms"); + } + { + JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.setCacheEnabled(true); + jaxRsDispatcher.setUseCaffCache(true); + jaxRsDispatcher.register(new FooResourceVar()); + + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); + } + System.out.println("Total time dispatchBlocks w/ caffeine cache: " + (System.currentTimeMillis() - start) + "ms"); } { diff --git a/pom.xml b/pom.xml index 89e5b7ba..26a532ec 100755 --- a/pom.xml +++ b/pom.xml @@ -93,6 +93,7 @@ 2.13.0 1.3.0-alpha2 2.1.6 + 2.8.1 1.10.19 @@ -314,6 +315,11 @@ flyway-core ${flyway.version} + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + org.mockito mockito-all From 0c14c0489d5f44e7f87ca24c038fc41f8b6785a4 Mon Sep 17 00:00:00 2001 From: ajohnston Date: Tue, 5 May 2020 21:54:38 -0700 Subject: [PATCH 08/12] Update performance tests to avoid bias --- .../jaxrs/core/CharBufferSplitTest.java | 384 +++++++ .../gemini/jaxrs/core/JavaProcess.java | 53 + .../jaxrs/core/JaxRsDispatcherTest.java | 1011 ++++++----------- .../gemini/jaxrs/core/Performance.java | 110 ++ .../gemini/path/AnnotationDispatcherTest.java | 11 +- 5 files changed, 918 insertions(+), 651 deletions(-) create mode 100644 gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/CharBufferSplitTest.java create mode 100644 gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JavaProcess.java create mode 100644 gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/Performance.java diff --git a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/CharBufferSplitTest.java b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/CharBufferSplitTest.java new file mode 100644 index 00000000..d59d6895 --- /dev/null +++ b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/CharBufferSplitTest.java @@ -0,0 +1,384 @@ +package com.techempower.gemini.jaxrs.core; + +import com.caucho.util.CharSegment; +import org.junit.Test; + +import java.nio.CharBuffer; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class CharBufferSplitTest +{ + static class StringSplitPerformanceTest + { + public static class Runner + { + public static void main(String... args) + { + Performance.test(StringSplitPerformanceTest.class, + ITERATIONS, + List.of( + List.of(CHAR_BUFFER_SPLIT_LINKED_LIST), + List.of(CHAR_BUFFER_SPLIT_ARRAY_LIST), + List.of(CHAR_SEGMENTS_SPLIT_ARRAY_LIST), + List.of(CHAR_SPAN_SPLIT_ARRAY_LIST), + List.of(CHAR_BUFFER_SPLIT_ARRAY), + List.of(STRING_SPLIT) + )); + } + } + + static final int ITERATIONS = 5_400_000; + static final String CHAR_BUFFER_SPLIT_LINKED_LIST = "CharBuffer split LinkedList"; + static final String CHAR_BUFFER_SPLIT_ARRAY_LIST = "CharBuffer split ArrayList"; + static final String CHAR_SEGMENTS_SPLIT_ARRAY_LIST = "Resin CharSegment split ArrayList"; + static final String CHAR_SPAN_SPLIT_ARRAY_LIST = "CharSpan split ArrayList"; + static final String CHAR_BUFFER_SPLIT_ARRAY = "CharBuffer split array (unrealistic)"; + static final String STRING_SPLIT = "String split"; + + public static void main(String... args) + throws Exception + { + String uri = "foo/bar"; + char[] chars = uri.toCharArray(); + if (args.length > 0) + { + switch (args[0]) + { + case CHAR_BUFFER_SPLIT_LINKED_LIST: + perfTestCharBufferSplitLinkedList(uri); + break; + case CHAR_BUFFER_SPLIT_ARRAY_LIST: + perfTestCharBufferSplitArrayList(uri); + break; + case CHAR_SEGMENTS_SPLIT_ARRAY_LIST: + perfTestCharSegmentsSplitArrayList(uri, chars); + break; + case CHAR_SPAN_SPLIT_ARRAY_LIST: + perfTestCharSpanSplitArrayList(uri); + break; + case CHAR_BUFFER_SPLIT_ARRAY: + perfTestCharBufferSplitArray(uri); + break; + case STRING_SPLIT: + perfTestStringSplit(uri); + break; + } + } + } + + public static void perfTestCharBufferSplitLinkedList(String uri) + { + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + List charBuffers = splitCharBufferLinkedList(uri); + } + }); + } + + public static void perfTestCharBufferSplitArrayList(String uri) + { + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + List charBuffers = splitCharBufferArrayList(uri); + } + }); + } + + private static void perfTestCharSegmentsSplitArrayList(String uri, char[] chars) + { + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + List segments = splitCharSegmentArrayList(uri, chars); + } + }); + } + + private static void perfTestCharSpanSplitArrayList(String uri) + { + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + List charSpans = splitCharSpanArrayList(uri); + } + }); + } + + private static void perfTestCharBufferSplitArray(String uri) + { + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + CharBuffer[] charBuffers = splitCharBufferArray(uri); + } + }); + } + + private static void perfTestStringSplit(String uri) + { + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + String[] strings = splitStringArray(uri); + } + }); + } + } + + private static List splitCharBufferLinkedList(String str) + { + int length = str.length(); + if (length == 0) + { + return List.of(); + } + LinkedList buffers = new LinkedList<>(); + int current = 0; + int next; + while ((next = str.indexOf("/", current)) != -1) + { + buffers.add(CharBuffer.wrap(str, current, next)); + current = next + 1; + } + if (current < length - 1) + { + buffers.add(CharBuffer.wrap(str, current, length)); + } + return buffers; + } + + private static List splitCharBufferArrayList(String str) + { + int length = str.length(); + if (length == 0) + { + return List.of(); + } + ArrayList buffers = new ArrayList<>(8); + int current = 0; + int next; + while ((next = str.indexOf("/", current)) != -1) + { + buffers.add(CharBuffer.wrap(str, current, next)); + current = next + 1; + } + if (current < length - 1) + { + buffers.add(CharBuffer.wrap(str, current, length)); + } + return buffers; + } + + private static List splitCharSegmentArrayList(String str, char[] bytes) + { + int length = bytes.length; + if (length == 0) + { + return List.of(); + } + ArrayList segments = new ArrayList<>(8); + int current = 0; + int next; + while ((next = str.indexOf("/", current)) != -1) + { + segments.add(new CharSegment(bytes, current, next - current)); + current = next + 1; + } + if (current < length - 1) + { + segments.add(new CharSegment(bytes, current, length - current)); + } + return segments; + } + + private static List splitCharSpanArrayList(String str) + { + int length = str.length(); + if (length == 0) + { + return List.of(); + } + ArrayList segments = new ArrayList<>(8); + int current = 0; + int next; + while ((next = str.indexOf("/", current)) != -1) + { + segments.add(new CharSpan(str, current, next)); + current = next + 1; + } + if (current < length - 1) + { + segments.add(new CharSpan(str, current, length)); + } + return segments; + } + + private static CharBuffer[] splitCharBufferArray(String str) + { + int length = str.length(); + if (length == 0) + { + return new CharBuffer[0]; + } + CharBuffer[] buffers = new CharBuffer[2]; + int current = 0; + int next; + int index = 0; + while ((next = str.indexOf("/", current)) != -1) + { + buffers[index++] = CharBuffer.wrap(str, current, next); + current = next + 1; + } + if (current < length - 1) + { + buffers[index] = CharBuffer.wrap(str, current, length); + } + return buffers; + } + + @Test + public void doTest() + { + assertEquals( + List.of(CharBuffer.wrap("foo"), CharBuffer.wrap("bar")), + splitCharBufferLinkedList("foo/bar")); + assertEquals( + List.of(CharBuffer.wrap(""), CharBuffer.wrap("foo"), CharBuffer.wrap("bar")), + splitCharBufferLinkedList("/foo/bar")); + assertEquals( + List.of(CharBuffer.wrap(""), CharBuffer.wrap("foo"), CharBuffer.wrap("bar")), + splitCharBufferLinkedList("/foo/bar/")); + assertEquals( + List.of(CharBuffer.wrap("foo"), CharBuffer.wrap("bar")), + splitCharBufferLinkedList("foo/bar/")); + assertEquals( + List.of(CharBuffer.wrap("foo")), + splitCharBufferLinkedList("foo")); + assertEquals( + List.of(CharBuffer.wrap(""), CharBuffer.wrap("foo")), + splitCharBufferLinkedList("/foo")); + assertEquals( + List.of(CharBuffer.wrap(""), CharBuffer.wrap("foo")), + splitCharBufferLinkedList("/foo/")); + assertEquals( + List.of(CharBuffer.wrap("foo")), + splitCharBufferLinkedList("foo/")); + } + + @Test + public void doTest2() + { + assertArrayEquals( + new String[]{"foo", "bar"}, + splitStringArray("foo/bar")); + assertArrayEquals( + new String[]{"", "foo", "bar"}, + splitStringArray("/foo/bar")); + assertArrayEquals( + new String[]{"", "foo", "bar"}, + splitStringArray("/foo/bar/")); + assertArrayEquals( + new String[]{"foo", "bar"}, + splitStringArray("foo/bar/")); + assertArrayEquals( + new String[]{"foo"}, + splitStringArray("foo")); + assertArrayEquals( + new String[]{"", "foo"}, + splitStringArray("/foo")); + assertArrayEquals( + new String[]{"", "foo"}, + splitStringArray("/foo/")); + assertArrayEquals( + new String[]{"foo"}, + splitStringArray("foo/")); + } + + private static String[] splitStringArray(String str) + { + return str.split("/"); + } + + private static class CharSpan implements CharSequence + { + private final CharSequence charSequence; + private final int start; + private final int end; + + private CharSpan(CharSequence charSequence, int start, int end) + { + this.charSequence = charSequence; + this.start = start; + this.end = end; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + CharSpan charSpan = (CharSpan) o; + int length = length(); + if (length != charSpan.length()) + { + return false; + } + for (int i = 0; i < length; i++) + { + if (charAt(i) != charSpan.charAt(i)) + { + return false; + } + } + return true; + } + + @Override + public int hashCode() + { + int h = 1; + for (int i = end - 1; i >= start; i--) + { + h = 31 * h + charAt(i); + } + return h; + } + + @Override + public int length() + { + return end - start; + } + + @Override + public char charAt(int index) + { + return charSequence.charAt(start + index); + } + + @Override + public CharSequence subSequence(int start, int end) + { + return charSequence.subSequence(this.start + start, this.start + end); + } + + @Override + public String toString() + { + return charSequence.subSequence(start, end).toString(); + } + } +} diff --git a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JavaProcess.java b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JavaProcess.java new file mode 100644 index 00000000..de76b17a --- /dev/null +++ b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JavaProcess.java @@ -0,0 +1,53 @@ +package com.techempower.gemini.jaxrs.core; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +// Modified from https://stackoverflow.com/a/35275894 and https://stackoverflow.com/a/58259581 +public final class JavaProcess +{ + private JavaProcess() + { + } + + public static String exec(Class klass, String... args) + throws IOException, InterruptedException + { + String javaHome = System.getProperty("java.home"); + String javaBin = javaHome + + File.separator + "bin" + + File.separator + "java"; + String classpath = System.getProperty("java.class.path"); + String className = klass.getName(); + + List command = new LinkedList<>(); + command.add(javaBin); + command.add("-cp"); + command.add(classpath); + command.add(className); + if (args != null) + { + command.addAll(Arrays.asList(args)); + } + + ProcessBuilder builder = new ProcessBuilder(command); + String result = ""; + Process process; + try + { + process = builder.start(); + result = new String(process.getInputStream().readAllBytes()); + + process.waitFor(); + process.destroy(); + } + catch (Exception e) + { + e.printStackTrace(); + } + return result; + } +} diff --git a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java index 58ddf783..f5ef0a49 100644 --- a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java +++ b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java @@ -1,5 +1,6 @@ package com.techempower.gemini.jaxrs.core; +import com.esotericsoftware.reflectasm.MethodAccess; import org.junit.Test; import javax.ws.rs.GET; @@ -7,11 +8,13 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; +import java.lang.reflect.Method; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; public class JaxRsDispatcherTest { @@ -115,6 +118,7 @@ public interface TestCase6 String doIt(); } + @Path("/foo") public static class TestCase6Impl implements TestCase6 { @@ -125,6 +129,16 @@ public String doIt() } } + public static class TestCase6Impl2 + implements TestCase6 + { + @Override + public String doIt() + { + return "did-it-test-case-6"; + } + } + @Path("/foo") public static class TestCase7 { @@ -236,7 +250,7 @@ public void pathParamsAtClassLevelShouldBeSupported() } @Test - public void inheritanceShouldBeCaptured() + public void methodAnnotationInheritanceShouldBeCaptured() { // TODO: Not yet implemented var dispatcher = new JaxRsDispatcher(); @@ -245,762 +259,465 @@ public void inheritanceShouldBeCaptured() dispatcher.dispatch(HttpMethod.GET, "/foo/bar")); } - static final int ITERATIONS = 2_700_000; - - @Test - public void perfTestRegexSlow() - { - long start; - for (int i = 0; i < ITERATIONS; i++) - { - Pattern pattern = Pattern.compile("/foo/bar"); - Matcher matcher = pattern.matcher("/foo/bar"); - boolean found = matcher.find(); - } - start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) - { - Pattern pattern = Pattern.compile("/foo/bar"); - Matcher matcher = pattern.matcher("/foo/bar"); - boolean found = matcher.find(); - } - System.out.println("Total time regex slow: " - + (System.currentTimeMillis() - start) + "ms"); - } - @Test - public void perfTestRegexFast() + public void classAnnotationInheritanceShouldNotBeCaptured() { - long start; - Pattern pattern = Pattern.compile("/foo/bar"); - for (int i = 0; i < ITERATIONS; i++) - { - Matcher matcher = pattern.matcher("/foo/bar"); - boolean found = matcher.find(); - } - start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) - { - Matcher matcher = pattern.matcher("/foo/bar"); - boolean found = matcher.find(); - } - System.out.println("Total time regex fast: " - + (System.currentTimeMillis() - start) + "ms"); - } + // TODO: Not yet implemented + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestCase6Impl2()); + // TODO: Null isn't really a great indicator here, since a given + // method could very well have returned null. + assertNull(dispatcher.dispatch(HttpMethod.GET, "/foo/bar")); + } + + static class SimpleUriPerformanceTest + { + public static class Runner + { + public static void main(String... args) + { + Performance.test(SimpleUriPerformanceTest.class, + ITERATIONS, + List.of( + List.of(REGEX_SLOW), + List.of(REGEX_FAST), + List.of(MAP_TREE), + List.of(MAP_FLAT), + List.of(MAP_FLAT_WITH_URI_SPLIT), + // TODO: Make a builder for generating these combinations. + // Something like: + // Performance.combinations() + // .next(JAX_RS) + // .next(GUAVA_LOADING_CACHE, RESIN_LRU_CACHE, + // CAFFEINE_LOADING_CACHE) + // .next(/* etc */) + // .build() + List.of(JAX_RS, NO_CACHE), + List.of(JAX_RS, GUAVA_LOADING_CACHE), + List.of(JAX_RS, RESIN_LRU_CACHE), + List.of(JAX_RS, CAFFEINE_LOADING_CACHE) + )); + } + } + + static final int ITERATIONS = 2_700_000; + // approaches + static final String REGEX_SLOW = "regex slow"; + static final String REGEX_FAST = "regex fast"; + static final String MAP_TREE = "tree of maps"; + static final String MAP_FLAT = "single flat map"; + static final String MAP_FLAT_WITH_URI_SPLIT = "single flat map with URI split"; + static final String JAX_RS = "jax-rs"; + // jax-rs customizations + static final String NO_CACHE = "no cache"; + static final String GUAVA_LOADING_CACHE = "Guava LoadingCache"; + static final String RESIN_LRU_CACHE = "Resin LruCache"; + static final String CAFFEINE_LOADING_CACHE = "Caffeine LoadingCache"; - @Test - public void perfTestMap() - { - Map>> foo = Map.of("foo", - Map.of("bar", Map.of(HttpMethod.GET, new Object()))); - String uriNoTrailingSlash = "foo/bar"; - long start; - for (int i = 0; i < ITERATIONS; i++) - { - String[] uriSegments = uriNoTrailingSlash.split("/"); - if (foo.containsKey(uriSegments[0])) - { - Map> inner = foo.get(uriSegments[0]); - if (inner.containsKey(uriSegments[1])) - { - Map endpointsByHttpMethod = inner.get( - uriSegments[1]); - Object resource = endpointsByHttpMethod.get(HttpMethod.GET); - } - } - } - start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) + public static void main(String... args) + throws Exception { - String[] uriSegments = uriNoTrailingSlash.split("/"); - if (foo.containsKey(uriSegments[0])) + if (args.length > 0) { - Map> inner = foo.get(uriSegments[0]); - if (inner.containsKey(uriSegments[1])) + switch (args[0]) { - Map endpointsByHttpMethod = inner.get( - uriSegments[1]); - Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + case REGEX_SLOW: + perfTestRegexSlow(); + break; + case REGEX_FAST: + perfTestRegexFast(); + break; + case MAP_TREE: + perfTestMapTree(); + break; + case MAP_FLAT: + perfTestMapFlat(); + break; + case MAP_FLAT_WITH_URI_SPLIT: + perfTestMapFlatWithUriSplit(); + break; + case JAX_RS: + { + JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.register(new FooResource()); + switch (args[1]) + { + case NO_CACHE: + break; + case GUAVA_LOADING_CACHE: + jaxRsDispatcher.setCacheEnabled(true); + break; + case RESIN_LRU_CACHE: + jaxRsDispatcher.setCacheEnabled(true); + jaxRsDispatcher.setUseResinCache(true); + break; + case CAFFEINE_LOADING_CACHE: + jaxRsDispatcher.setCacheEnabled(true); + jaxRsDispatcher.setUseCaffCache(true); + break; + default: + throw new RuntimeException(); + } + perfTestJaxRs(jaxRsDispatcher); + break; + } + default: + throw new RuntimeException(); } } } - System.out.println("Total time map: " - + (System.currentTimeMillis() - start) + "ms"); - } - - static class Foo { - public static void main(String... args) { - JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); - jaxRsDispatcher.register(new FooResource()); - JaxRsDispatcher jaxRsDispatcherCached = new JaxRsDispatcher(); - jaxRsDispatcherCached.setCacheEnabled(true); - jaxRsDispatcherCached.register(new FooResource()); - JaxRsDispatcher jaxRsDispatcherResinCached = new JaxRsDispatcher(); - jaxRsDispatcherResinCached.setCacheEnabled(true); - jaxRsDispatcherResinCached.setUseResinCache(true); - jaxRsDispatcherResinCached.register(new FooResource()); - JaxRsDispatcher jaxRsDispatcherCaffCached = new JaxRsDispatcher(); - jaxRsDispatcherCaffCached.setCacheEnabled(true); - jaxRsDispatcherCaffCached.setUseCaffCache(true); - jaxRsDispatcherCaffCached.register(new FooResource()); - JaxRsDispatcherTest test = new JaxRsDispatcherTest(); - test.warmUpJaxRs(jaxRsDispatcher); - test.warmUpJaxRs(jaxRsDispatcherCached); - test.warmUpJaxRs(jaxRsDispatcherResinCached); - test.warmUpJaxRs(jaxRsDispatcherCaffCached); - long start; - start = System.currentTimeMillis(); - test.justJaxRs(jaxRsDispatcher); - System.out.println("Total time dispatchBlocks w/o cache: " - + (System.currentTimeMillis() - start) + "ms"); - start = System.currentTimeMillis(); - test.justJaxRsCached(jaxRsDispatcherCached); - System.out.println("Total time dispatchBlocks w/ google cache: " - + (System.currentTimeMillis() - start) + "ms"); - start = System.currentTimeMillis(); - test.justJaxRsResinCached(jaxRsDispatcherResinCached); - System.out.println("Total time dispatchBlocks w/ resin cache: " - + (System.currentTimeMillis() - start) + "ms"); - start = System.currentTimeMillis(); - test.justJaxRsCaffCached(jaxRsDispatcherCaffCached); - System.out.println("Total time dispatchBlocks w/ caffeine cache: " - + (System.currentTimeMillis() - start) + "ms"); - } - } - - public void warmUpJaxRs(JaxRsDispatcher jaxRsDispatcher) - { - String uriNoTrailingSlash = "foo/bar"; - for (int i = 0; i < ITERATIONS; i++) - { - jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, - uriNoTrailingSlash); - if (i == 0) - { - JaxRsDispatcher.DispatchBestMatchInfo match = jaxRsDispatcher - .getBestMatchInfo(HttpMethod.GET, uriNoTrailingSlash); - System.out.println("Found " + match.getValues()); - } - } - } - public void justJaxRs(JaxRsDispatcher jaxRsDispatcher) - { - String uriNoTrailingSlash = "foo/bar"; - for (int i = 0; i < ITERATIONS; i++) - { - jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, - uriNoTrailingSlash); - } - } - - public void justJaxRsCached(JaxRsDispatcher jaxRsDispatcher) - { - String uriNoTrailingSlash = "foo/bar"; - for (int i = 0; i < ITERATIONS; i++) - { - jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, - uriNoTrailingSlash); - } - } - - public void justJaxRsResinCached(JaxRsDispatcher jaxRsDispatcher) - { - String uriNoTrailingSlash = "foo/bar"; - for (int i = 0; i < ITERATIONS; i++) - { - jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, - uriNoTrailingSlash); - } - } - - public void justJaxRsCaffCached(JaxRsDispatcher jaxRsDispatcher) - { - String uriNoTrailingSlash = "foo/bar"; - for (int i = 0; i < ITERATIONS; i++) - { - jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, - uriNoTrailingSlash); - } - } - - @Test - public void perfTestJaxRs() - { - String uriNoTrailingSlash = "foo/bar"; - JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); - jaxRsDispatcher.register(new FooResource()); - for (int i = 0; i < ITERATIONS; i++) - { - jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, - uriNoTrailingSlash); - if (i == 0) - { - JaxRsDispatcher.DispatchBestMatchInfo match = jaxRsDispatcher - .getBestMatchInfo(HttpMethod.GET, uriNoTrailingSlash); - System.out.println("Found " + match.getValues()); - } - } - long start; - start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) + public static void perfTestRegexSlow() { - jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, - uriNoTrailingSlash); - } - System.out.println("Total time dispatchBlocks: " - + (System.currentTimeMillis() - start) + "ms"); - } - - @Test - public void perfTestMaps() - { - Map>> foo = Map.of("foo", - Map.of("bar", Map.of(HttpMethod.GET, new Object()))); - String uriNoTrailingSlash = "foo/bar"; - long start; - for (int i = 0; i < ITERATIONS; i++) - { - String[] uriSegments = uriNoTrailingSlash.split("/"); - if (foo.containsKey(uriSegments[0])) - { - Map> inner = foo.get(uriSegments[0]); - if (inner.containsKey(uriSegments[1])) + String uriNoTrailingSlash = "foo/bar"; + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) { - Map endpointsByHttpMethod = inner.get( - uriSegments[1]); - Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + Pattern pattern = Pattern.compile("foo/bar"); + Matcher matcher = pattern.matcher(uriNoTrailingSlash); + boolean found = matcher.find(); } - } + }); } - start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) + + public static void perfTestRegexFast() { - String[] uriSegments = uriNoTrailingSlash.split("/"); - if (foo.containsKey(uriSegments[0])) - { - Map> inner = foo.get(uriSegments[0]); - if (inner.containsKey(uriSegments[1])) + String uriNoTrailingSlash = "foo/bar"; + Pattern pattern = Pattern.compile("foo/bar"); + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) { - Map endpointsByHttpMethod = inner.get( - uriSegments[1]); - Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + Matcher matcher = pattern.matcher(uriNoTrailingSlash); + boolean found = matcher.find(); } - } - } - System.out.println("Total time map a: " - + (System.currentTimeMillis() - start) + "ms"); - Map> fooB = Map.of("foo/bar", - Map.of(HttpMethod.GET, new Object())); - for (int i = 0; i < ITERATIONS; i++) - { - if (fooB.containsKey(uriNoTrailingSlash)) - { - Map endpointsByHttpMethod = fooB.get(uriNoTrailingSlash); - Object resource = endpointsByHttpMethod.get(HttpMethod.GET); - } + }); } - start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) - { - if (fooB.containsKey(uriNoTrailingSlash)) - { - Map endpointsByHttpMethod = fooB.get(uriNoTrailingSlash); - Object resource = endpointsByHttpMethod.get(HttpMethod.GET); - } - } - System.out.println("Total time map b: " - + (System.currentTimeMillis() - start) + "ms"); - } - @Test - public void perfTestAll() - { - // Warm-up + public static void perfTestMapTree() { - long start; - start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) - { - Pattern pattern = Pattern.compile("/foo/bar"); - Matcher matcher = pattern.matcher("/foo/bar"); - boolean found = matcher.find(); - } - start = System.currentTimeMillis(); - Pattern pattern = Pattern.compile("/foo/bar"); - for (int i = 0; i < ITERATIONS; i++) - { - Matcher matcher = pattern.matcher("/foo/bar"); - boolean found = matcher.find(); - } Map>> foo = Map.of("foo", Map.of("bar", Map.of(HttpMethod.GET, new Object()))); - // This is better for the map approach String uriNoTrailingSlash = "foo/bar"; - start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) - { - String[] uriSegments = uriNoTrailingSlash.split("/"); - if (foo.containsKey(uriSegments[0])) + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) { - Map> inner = foo.get(uriSegments[0]); - if (inner.containsKey(uriSegments[1])) + String[] uriSegments = uriNoTrailingSlash.split("/"); + if (foo.containsKey(uriSegments[0])) { - Map endpointsByHttpMethod = inner.get( - uriSegments[1]); - Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + Map> inner = foo.get(uriSegments[0]); + if (inner.containsKey(uriSegments[1])) + { + Map endpointsByHttpMethod = inner.get( + uriSegments[1]); + Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + } } } - } - { - String uri = "foo/bar/"; - JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); - jaxRsDispatcher.register(new FooResource()); + }); + } - start = System.currentTimeMillis(); + public static void perfTestMapFlat() + { + Map> fooB = Map.of("foo/bar", + Map.of(HttpMethod.GET, new Object())); + String uriNoTrailingSlash = "foo/bar"; + Performance.time(() -> { for (int i = 0; i < ITERATIONS; i++) { - jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); - if (i == 0) + if (fooB.containsKey(uriNoTrailingSlash)) { - JaxRsDispatcher.DispatchBestMatchInfo match = jaxRsDispatcher - .getBestMatchInfo(HttpMethod.GET, uri); - System.out.println("Found " + match.getValues()); + Map endpointsByHttpMethod = fooB.get(uriNoTrailingSlash); + Object resource = endpointsByHttpMethod.get(HttpMethod.GET); } } - System.out.println("Total time dispatchBlocks: " - + (System.currentTimeMillis() - start) + "ms"); - } + }); } + + public static void perfTestMapFlatWithUriSplit() { - long start; - start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) - { - Pattern pattern = Pattern.compile("/foo/bar"); - Matcher matcher = pattern.matcher("/foo/bar"); - boolean found = matcher.find(); - } - System.out.println("Total time regex slow: " - + (System.currentTimeMillis() - start) + "ms"); - start = System.currentTimeMillis(); - Pattern pattern = Pattern.compile("/foo/bar"); - for (int i = 0; i < ITERATIONS; i++) - { - Matcher matcher = pattern.matcher("/foo/bar"); - boolean found = matcher.find(); - } - System.out.println("Total time regex fast: " - + (System.currentTimeMillis() - start) + "ms"); - String path = "foo/bar"; - Map>> foo = Map.of("foo", - Map.of("bar", Map.of(HttpMethod.GET, new Object()))); - start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) - { - String[] uriSegments = path.split("/"); - if (foo.containsKey(uriSegments[0])) + Map> fooB = Map.of("foo/bar", + Map.of(HttpMethod.GET, new Object())); + String uriNoTrailingSlash = "foo/bar"; + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) { - Map> inner = foo.get(uriSegments[0]); - if (inner.containsKey(uriSegments[1])) + String[] uriSegments = uriNoTrailingSlash.split("/"); + if (fooB.containsKey(uriNoTrailingSlash)) { - Map endpointsByHttpMethod = inner.get( - uriSegments[1]); + Map endpointsByHttpMethod = fooB.get(uriNoTrailingSlash); Object resource = endpointsByHttpMethod.get(HttpMethod.GET); } } - } - System.out.println("Total time tree-map approach (w/ URI split): " - + (System.currentTimeMillis() - start) + "ms"); - Map> foo2 = Map.of("foo/bar", Map.of(HttpMethod.GET, new Object())); - start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) - { - String[] uriSegments = path.split("/"); - if (foo2.containsKey(path)) + }); + } + + public static void perfTestJaxRs(JaxRsDispatcher jaxRsDispatcher) + { + String uriNoTrailingSlash = "foo/bar"; + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) { - Map endpointsByHttpMethod = foo2.get(path); - Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, + uriNoTrailingSlash); } - } - System.out.println("Total time flat-map approach w/ URI split: " - + (System.currentTimeMillis() - start) + "ms"); - Map> foo3 = Map.of("foo/bar", Map.of(HttpMethod.GET, new Object())); - start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) + }); + } + } + + static class ComplexUriPerformanceTest + { + public static class Runner + { + public static void main(String... args) { - //String[] uriSegments = path.split("/"); - if (foo3.containsKey(path)) - { - Map endpointsByHttpMethod = foo3.get(path); - Object resource = endpointsByHttpMethod.get(HttpMethod.GET); - } + Performance.test(ComplexUriPerformanceTest.class, + ITERATIONS, + List.of( + List.of(REGEX_SLOW), + List.of(REGEX_FAST), + List.of(JAX_RS, NO_CACHE), + List.of(JAX_RS, GUAVA_LOADING_CACHE), + List.of(JAX_RS, RESIN_LRU_CACHE), + List.of(JAX_RS, CAFFEINE_LOADING_CACHE), + List.of(CREATE_OBJECTS) + )); } - System.out.println("Total time flat-map approach w/o URI split: " - + (System.currentTimeMillis() - start) + "ms"); - { - String uri = "foo/bar/"; - JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); - jaxRsDispatcher.register(new FooResource()); + } - start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) + static final int ITERATIONS = 2_700_000; + // approaches + static final String REGEX_SLOW = "regex slow"; + static final String REGEX_FAST = "regex fast"; + static final String JAX_RS = "jax-rs"; + static final String CREATE_OBJECTS = "create objects (subset of jax-rs)"; + // jax-rs customizations + static final String NO_CACHE = "no cache"; + static final String GUAVA_LOADING_CACHE = "Guava LoadingCache"; + static final String RESIN_LRU_CACHE = "Resin LruCache"; + static final String CAFFEINE_LOADING_CACHE = "Caffeine LoadingCache"; + + public static void main(String... args) + throws Exception + { + if (args.length > 0) + { + switch (args[0]) { - jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); + case REGEX_SLOW: + perfTestRegexSlow(); + break; + case REGEX_FAST: + perfTestRegexFast(); + break; + case JAX_RS: + { + JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.register(new FooResourceVar()); + switch (args[1]) + { + case NO_CACHE: + break; + case GUAVA_LOADING_CACHE: + jaxRsDispatcher.setCacheEnabled(true); + break; + case RESIN_LRU_CACHE: + jaxRsDispatcher.setCacheEnabled(true); + jaxRsDispatcher.setUseResinCache(true); + break; + case CAFFEINE_LOADING_CACHE: + jaxRsDispatcher.setCacheEnabled(true); + jaxRsDispatcher.setUseCaffCache(true); + break; + default: + throw new RuntimeException(); + } + perfTestJaxRs(jaxRsDispatcher); + break; + } + case CREATE_OBJECTS: + perfTestCreateObjects(); + break; + default: + throw new RuntimeException(); } - System.out.println("Total time dispatchBlocks w/o LRU cache: " - + (System.currentTimeMillis() - start) + "ms"); } - { - String uri = "foo/bar/"; - JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); - jaxRsDispatcher.setCacheEnabled(true); - jaxRsDispatcher.register(new FooResource()); + } - start = System.currentTimeMillis(); + public static void perfTestRegexSlow() + { + var uuidGroupNameStr = "g" + UUID.randomUUID().toString().replaceAll("-", ""); + var uuidStr = UUID.randomUUID().toString(); + String uri = "foo/" + uuidStr + "/"; + Performance.time(() -> { for (int i = 0; i < ITERATIONS; i++) { - jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); + Pattern pattern = Pattern.compile("foo/(?<" + uuidGroupNameStr + ">[^/]+?)/.*"); + Matcher matcher = pattern.matcher(uri); + boolean found = matcher.find(); + String matchFound = matcher.group(uuidGroupNameStr); } - System.out.println("Total time dispatchBlocks w/ LRU cache: " - + (System.currentTimeMillis() - start) + "ms"); - } - { - String uri = "foo/bar/"; - JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); - jaxRsDispatcher.setCacheEnabled(true); - jaxRsDispatcher.setUseResinCache(true); - jaxRsDispatcher.register(new FooResource()); + }); + } - start = System.currentTimeMillis(); + public static void perfTestRegexFast() + { + var uuidGroupNameStr = "g" + UUID.randomUUID().toString().replaceAll("-", ""); + var uuidStr = UUID.randomUUID().toString(); + String uri = "foo/" + uuidStr + "/"; + Pattern pattern = Pattern.compile("foo/(?<" + uuidGroupNameStr + ">[^/]+?)/.*"); + Performance.time(() -> { for (int i = 0; i < ITERATIONS; i++) { - jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); + Matcher matcher = pattern.matcher(uri); + boolean found = matcher.find(); + String matchFound = matcher.group(uuidGroupNameStr); } - System.out.println("Total time dispatchBlocks w/ resin LRU cache: " - + (System.currentTimeMillis() - start) + "ms"); - } - { - String uri = "foo/bar/"; - JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); - jaxRsDispatcher.setCacheEnabled(true); - jaxRsDispatcher.setUseCaffCache(true); - jaxRsDispatcher.register(new FooResource()); + }); + } - start = System.currentTimeMillis(); + public static void perfTestJaxRs(JaxRsDispatcher jaxRsDispatcher) + { + var uuidStr = UUID.randomUUID().toString(); + String uri = "foo/" + uuidStr + "/"; + Performance.time(() -> { for (int i = 0; i < ITERATIONS; i++) { jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); } - System.out.println("Total time dispatchBlocks w/ caffeine cache: " - + (System.currentTimeMillis() - start) + "ms"); - } - } - } - - public static class Bar { - public static void main(String... args) - { - JaxRsDispatcherTest test = new JaxRsDispatcherTest(); - //test.perfTestAllComplicated(); - Map>> foo = Map.of("foo", - Map.of("bar", Map.of(HttpMethod.GET, new Object()))); - JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); - jaxRsDispatcher.register(new FooResource()); - JaxRsDispatcher jaxRsDispatcherCached = new JaxRsDispatcher(); - jaxRsDispatcherCached.setCacheEnabled(true); - jaxRsDispatcherCached.register(new FooResource()); - test.combinedWarmUp(foo, jaxRsDispatcher, jaxRsDispatcherCached); - test.mapApproach(foo); - test.dispatcherBlocksApproach(jaxRsDispatcher); - test.dispatcherBlocksCachedApproach(jaxRsDispatcherCached); + }); } - } - public void combinedWarmUp(Map>> foo, - JaxRsDispatcher jaxRsDispatcher, - JaxRsDispatcher jaxRsDispatcherCached) - { - String uriNoTrailingSlash = "foo/bar"; - for (int i = 0; i < ITERATIONS; i++) + public static void perfTestCreateObjects() { - String[] uriSegments = uriNoTrailingSlash.split("/"); - if (foo.containsKey(uriSegments[0])) - { - Map> inner = foo.get(uriSegments[0]); - if (inner.containsKey(uriSegments[1])) + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) { - Map endpointsByHttpMethod = inner.get(uriSegments[1]); - Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + //noinspection MismatchedQueryAndUpdateOfCollection + List matches = new ArrayList<>(1); + matches.add( + new JaxRsDispatcher.DispatchMatch(null, null, null, null)); } - } - } - for (int i = 0; i < ITERATIONS; i++) - { - jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uriNoTrailingSlash); - } - for (int i = 0; i < ITERATIONS; i++) - { - jaxRsDispatcherCached.getBestMatchInfo(HttpMethod.GET, - uriNoTrailingSlash); + }); } } - public void mapApproach(Map>> foo) + static class MethodCallPerformanceTest { - long start = System.currentTimeMillis(); - String uriNoTrailingSlash = "foo/bar"; - String httpMethod = HttpMethod.GET; - for (int i = 0; i < ITERATIONS; i++) + public static class Runner { - String[] uriSegments = uriNoTrailingSlash.split("/"); - if (foo.containsKey(uriSegments[0])) + public static void main(String... args) { - Map> inner = foo.get(uriSegments[0]); - if (inner.containsKey(uriSegments[1])) - { - Map endpointsByHttpMethod = inner.get(uriSegments[1]); - Object resource = endpointsByHttpMethod.get(HttpMethod.GET); - } + Performance.test(MethodCallPerformanceTest.class, + ITERATIONS, + List.of( + List.of(DIRECT_METHOD_CALL), + List.of(REFLECTION_LIBRARY), + List.of(METHOD_REFERENCE), + List.of(STANDARD_REFLECTION) + )); } } - System.out.println("Total time map: " - + (System.currentTimeMillis() - start) + "ms"); - } - public void dispatcherBlocksApproach(JaxRsDispatcher jaxRsDispatcher) - { - // This is better for the blocks approach - String uriNoTrailingSlash = "foo/bar"; - long start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) - { - jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uriNoTrailingSlash); - } - System.out.println("Total time dispatchBlocks w/o cache: " - + (System.currentTimeMillis() - start) + "ms"); - } + static final int ITERATIONS = 1_000_000_000; + static final String DIRECT_METHOD_CALL = "direct method call"; + static final String REFLECTION_LIBRARY = "reflection library"; + static final String METHOD_REFERENCE = "method reference"; + static final String STANDARD_REFLECTION = "standard reflection"; - public void dispatcherBlocksCachedApproach(JaxRsDispatcher jaxRsDispatcher) - { - // This is better for the blocks approach - String uriNoTrailingSlash = "foo/bar"; - long start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) + static class Cow { - jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uriNoTrailingSlash); + public void moo() + { + } } - System.out.println("Total time dispatchBlocks w/ cache: " - + (System.currentTimeMillis() - start) + "ms"); - } - @Test - public void perfTestAllComplicated() - { - // Warm-up + public static void main(String... args) + throws Exception { - var uuidGroupNameStr = "g" + UUID.randomUUID().toString().replaceAll("-", ""); - var uuidStr = UUID.randomUUID().toString(); - String uri = "foo/" + uuidStr + "/"; - long start; - start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) - { - Pattern pattern = Pattern.compile("foo/(?<" + uuidGroupNameStr + ">[^/]+?)/.*"); - Matcher matcher = pattern.matcher(uri); - boolean found = matcher.find(); - String matchFound = matcher.group(uuidGroupNameStr); - if (i == 0) - { - System.out.println("Found " + matchFound); - } - } - start = System.currentTimeMillis(); - Pattern pattern = Pattern.compile("foo/(?<" + uuidGroupNameStr + ">[^/]+?)/.*"); - for (int i = 0; i < ITERATIONS; i++) + if (args.length > 0) { - Matcher matcher = pattern.matcher(uri); - boolean found = matcher.find(); - String matchFound = matcher.group(uuidGroupNameStr); - if (i == 0) + switch (args[0]) { - System.out.println("Found " + matchFound); + case DIRECT_METHOD_CALL: + perfTestDirect(); + break; + case REFLECTION_LIBRARY: + perfTestMethodAccess(); + break; + case METHOD_REFERENCE: + perfTestReference(); + break; + case STANDARD_REFLECTION: + perfTestReflection(); + break; } } - JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); - jaxRsDispatcher.register(new FooResourceVar()); - start = System.currentTimeMillis(); + } + + public static void perfTestDirect() + { + Cow cow = new Cow(); for (int i = 0; i < ITERATIONS; i++) { - jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); - if (i == 0) - { - JaxRsDispatcher.DispatchBestMatchInfo match = jaxRsDispatcher - .getBestMatchInfo(HttpMethod.GET, uri); - System.out.println("Found " + match.getValues()); - } + cow.moo(); } - JaxRsDispatcher jaxRsDispatcherCached = new JaxRsDispatcher(); - jaxRsDispatcherCached.setCacheEnabled(true); - jaxRsDispatcherCached.register(new FooResourceVar()); - start = System.currentTimeMillis(); + long start = System.nanoTime(); for (int i = 0; i < ITERATIONS; i++) { - jaxRsDispatcherCached.getBestMatchInfo(HttpMethod.GET, uri); - if (i == 5) - { - JaxRsDispatcher.DispatchBestMatchInfo match = jaxRsDispatcherCached - .getBestMatchInfo(HttpMethod.GET, uri); - System.out.println("Found " + match.getValues()); - } + cow.moo(); } - JaxRsDispatcher jaxRsDispatcherCachedResin = new JaxRsDispatcher(); - jaxRsDispatcherCachedResin.setCacheEnabled(true); - jaxRsDispatcherCachedResin.setUseResinCache(true); - jaxRsDispatcherCachedResin.register(new FooResourceVar()); - start = System.currentTimeMillis(); + System.out.print(System.nanoTime() - start); + } + + public static void perfTestReference() + { + Cow cow = new Cow(); + Runnable moo = cow::moo; for (int i = 0; i < ITERATIONS; i++) { - jaxRsDispatcherCachedResin.getBestMatchInfo(HttpMethod.GET, uri); - if (i == 5) - { - JaxRsDispatcher.DispatchBestMatchInfo match = jaxRsDispatcherCachedResin - .getBestMatchInfo(HttpMethod.GET, uri); - System.out.println("Found " + match.getValues()); - } + moo.run(); } - JaxRsDispatcher jaxRsDispatcherCachedCaff = new JaxRsDispatcher(); - jaxRsDispatcherCachedCaff.setCacheEnabled(true); - jaxRsDispatcherCachedCaff.setUseCaffCache(true); - jaxRsDispatcherCachedCaff.register(new FooResourceVar()); - start = System.currentTimeMillis(); + long start = System.nanoTime(); for (int i = 0; i < ITERATIONS; i++) { - jaxRsDispatcherCachedCaff.getBestMatchInfo(HttpMethod.GET, uri); - if (i == 5) - { - JaxRsDispatcher.DispatchBestMatchInfo match = jaxRsDispatcherCachedCaff - .getBestMatchInfo(HttpMethod.GET, uri); - System.out.println("Found " + match.getValues()); - } + moo.run(); } + System.out.print(System.nanoTime() - start); } + + public static void perfTestReflection() + throws Exception { - var uuidGroupNameStr = "g" + UUID.randomUUID().toString().replaceAll("-", ""); - var uuidStr = UUID.randomUUID().toString(); - String uri = "foo/" + uuidStr + "/"; - // This is better for the map approach - long start; - start = System.currentTimeMillis(); + Cow cow = new Cow(); + Method method = cow.getClass().getMethod("moo"); + method.setAccessible(true); for (int i = 0; i < ITERATIONS; i++) { - Pattern pattern = Pattern.compile("foo/(?<" + uuidGroupNameStr + ">[^/]+?)/.*"); - Matcher matcher = pattern.matcher(uri); - boolean found = matcher.find(); - String matchFound = matcher.group(uuidGroupNameStr); + method.invoke(cow); } - System.out.println("Total time regex slow: " - + (System.currentTimeMillis() - start) + "ms"); - start = System.currentTimeMillis(); - Pattern pattern = Pattern.compile("foo/(?<" + uuidGroupNameStr + ">[^/]+?)/.*"); + long start = System.nanoTime(); for (int i = 0; i < ITERATIONS; i++) { - Matcher matcher = pattern.matcher(uri); - boolean found = matcher.find(); - String matchFound = matcher.group(uuidGroupNameStr); - } - System.out.println("Total time regex fast: " - + (System.currentTimeMillis() - start) + "ms"); - { - JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); - jaxRsDispatcher.register(new FooResourceVar()); - start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) - { - jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); - } - System.out.println("Total time dispatchBlocks w/o LRU cache: " - + (System.currentTimeMillis() - start) + "ms"); + method.invoke(cow); } - { - JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); - jaxRsDispatcher.setCacheEnabled(true); - jaxRsDispatcher.register(new FooResourceVar()); - - start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) - { - jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); - } - System.out.println("Total time dispatchBlocks w/ LRU cache: " - + (System.currentTimeMillis() - start) + "ms"); - } - { - JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); - jaxRsDispatcher.setCacheEnabled(true); - jaxRsDispatcher.setUseResinCache(true); - jaxRsDispatcher.register(new FooResourceVar()); - - start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) - { - jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); - } - System.out.println("Total time dispatchBlocks w/ resin LRU cache: " - + (System.currentTimeMillis() - start) + "ms"); - } - { - JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); - jaxRsDispatcher.setCacheEnabled(true); - jaxRsDispatcher.setUseCaffCache(true); - jaxRsDispatcher.register(new FooResourceVar()); + System.out.print(System.nanoTime() - start); + } - start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) - { - jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); - } - System.out.println("Total time dispatchBlocks w/ caffeine cache: " - + (System.currentTimeMillis() - start) + "ms"); - } + public static void perfTestMethodAccess() + { + Cow cow = new Cow(); + MethodAccess methodAccess = MethodAccess.get(Cow.class); + int index = methodAccess.getIndex("moo"); + for (int i = 0; i < ITERATIONS; i++) { - start = System.currentTimeMillis(); - for (int i = 0; i < ITERATIONS; i++) - { - List matches = new ArrayList<>(1); - matches.add( - new JaxRsDispatcher.DispatchMatch(null, null, null, null)); - List matches2 = new ArrayList<>(1); - matches2.add( - new JaxRsDispatcher.DispatchMatch(null, null, null, null)); - } - System.out.println("Total time create objects: " - + (System.currentTimeMillis() - start) + "ms"); + methodAccess.invoke(cow, index); } - /*JaxRsDispatcher.DispatchBlock top = new JaxRsDispatcher.DispatchBlock(null); - top.fullSegmentChildren = new JaxRsDispatcher.ChildDispatchBlockGroup(); - JaxRsDispatcher.DispatchBlock foo = new JaxRsDispatcher.DispatchBlock(null); - top.fullSegmentChildren.addChildWordBlock("foo", foo); - - foo.fullSegmentChildren = new JaxRsDispatcher.ChildDispatchBlockGroup(); - JaxRsDispatcher.DispatchBlock variableBlock = new JaxRsDispatcher.DispatchBlock(null); - foo.fullSegmentChildren.setChildPureVariableBlock(variableBlock); - - start = System.currentTimeMillis(); + long start = System.nanoTime(); for (int i = 0; i < ITERATIONS; i++) { - String[] uriSegments = uriNoTrailingSlash.split("/"); - if (top.fullSegmentChildren != null) { - JaxRsDispatcher.DispatchBlock fooInner = top.fullSegmentChildren.getChildWordBlock(uriSegments[0]); - if (fooInner.fullSegmentChildren != null) { - JaxRsDispatcher.DispatchBlock pureVariableBlock = fooInner.fullSegmentChildren.getChildPureVariableBlock(); - if (pureVariableBlock != null) { - Map endpointsByHttpMethod = pureVariableBlock.endpointsByHttpMethod; - } - } - } + methodAccess.invoke(cow, index); } - System.out.println("Total time dispatchBlocks: " + (System.currentTimeMillis() - start) + "ms");*/ + System.out.print(System.nanoTime() - start); } } } \ No newline at end of file diff --git a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/Performance.java b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/Performance.java new file mode 100644 index 00000000..fdedac42 --- /dev/null +++ b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/Performance.java @@ -0,0 +1,110 @@ +package com.techempower.gemini.jaxrs.core; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.text.DecimalFormat; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class Performance +{ + private Performance() + { + } + + public static void test(Class type, + int iterationsMetadata, + List> options) + { + test(true, type, iterationsMetadata, options); + } + + /** Set fork to false to get better error reporting. Otherwise always have + * it as true for the best/most fair performance reporting. */ + public static void test(boolean fork, + Class type, + int iterationsMetadata, + List> options) + { + try + { + System.out.println(String.format("Running performance test `%s`...", + type.getSimpleName())); + long start = System.currentTimeMillis(); + Map, Long> totalNanosecondsByOption = new LinkedHashMap<>(); + long COUNT = 10; + for (int i = 0; i < COUNT; i++) + { + for (List args : options) + { + String output; + if (fork) + { + output = JavaProcess.exec(type, args.toArray(String[]::new)); + } + else + { + PrintStream originalOut = System.out; + try + { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final String utf8 = StandardCharsets.UTF_8.name(); + try (PrintStream ps = new PrintStream(baos, true, utf8)) + { + System.setOut(ps); + type.getDeclaredMethod("main", String[].class) + .invoke(null, (Object) args.toArray(String[]::new)); + output = baos.toString(utf8); + } + } + finally + { + System.setOut(originalOut); + } + + } + if (output.isEmpty()) + { + throw new RuntimeException("Output was empty for args: " + + String.join("::", args)); + } + long milli = Long.parseLong(output); + totalNanosecondsByOption.compute(args, (key, value) -> + (value == null ? 0 : value) + milli); + } + } + System.out.println(String.format(" Total time overall: %sms", + System.currentTimeMillis() - start)); + DecimalFormat commas = new DecimalFormat("#,###"); + DecimalFormat decimal = new DecimalFormat("#.0"); + System.out.println(String.format("The following are the milliseconds" + + " required for each approach to run %s times, averaged over %s" + + " separate runs:", commas.format(iterationsMetadata), COUNT)); + totalNanosecondsByOption.forEach((args, totalNano) -> + System.out.println(String.format(" %s: %sms", + String.join("::", args), + decimal.format((double) totalNano / 1e6 / COUNT)))); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + public static void time(ThrowingRunnable runnable) + throws R + { + runnable.run(); + long start = System.nanoTime(); + runnable.run(); + System.out.print(System.nanoTime() - start); + } + + @FunctionalInterface + public interface ThrowingRunnable + { + void run() throws R; + } +} diff --git a/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationDispatcherTest.java b/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationDispatcherTest.java index c76d68e4..1a9d4b3c 100644 --- a/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationDispatcherTest.java +++ b/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationDispatcherTest.java @@ -69,16 +69,18 @@ public static void main(String...args) { // I know these are identical, but it's easier to distinguish the warm up // from the "real" in the profiler this way. public void warmUpBlah(AnnotationDispatcher dispatcher) { + String uri = "foo/bar"; for (int i = 0; i < ITERATIONS; i++) { - dispatcher.dispatch(Request.HttpMethod.GET, "foo/bar"); + dispatcher.dispatch(Request.HttpMethod.GET, uri); } } public void doBlah(AnnotationDispatcher dispatcher) { + String uri = "foo/bar"; for (int i = 0; i < ITERATIONS; i++) { - dispatcher.dispatch(Request.HttpMethod.GET, "foo/bar"); + dispatcher.dispatch(Request.HttpMethod.GET, uri); } } @@ -86,9 +88,10 @@ public void doBlah(AnnotationDispatcher dispatcher) { public void blah() { AnnotationDispatcher dispatcher = new AnnotationDispatcher<>(); dispatcher.register(new FooResource()); + String uri = "foo/bar"; for (int i = 0; i < ITERATIONS; i++) { - dispatcher.dispatch(Request.HttpMethod.GET, "foo/bar"); + dispatcher.dispatch(Request.HttpMethod.GET, uri); } long start; /*Context context = mock(Context.class); @@ -96,7 +99,7 @@ public void blah() { start = System.currentTimeMillis(); for (int i = 0; i < ITERATIONS; i++) { - dispatcher.dispatch(Request.HttpMethod.GET, "foo/bar"); + dispatcher.dispatch(Request.HttpMethod.GET, uri); } System.out.println("Total time kain-approach: " + (System.currentTimeMillis() - start) + "ms"); From 33ed4030464e002f5d319f37cec10d98fb4bb32d Mon Sep 17 00:00:00 2001 From: ajohnston Date: Wed, 27 May 2020 02:57:01 -0700 Subject: [PATCH 09/12] Implement media type parsing, plus tests --- .../gemini/jaxrs/core/JaxRsDispatcher.java | 13 ++ .../gemini/jaxrs/core/MediaTypeData.java | 60 ++++++++ .../gemini/jaxrs/core/MediaTypeDataGroup.java | 42 ++++++ .../gemini/jaxrs/core/MediaTypeParser.java | 18 +++ .../jaxrs/core/MediaTypeParserImpl.java | 132 ++++++++++++++++++ .../jaxrs/core/ProcessingException.java | 16 +++ .../jaxrs/core/MediaTypeParserTest.java | 93 ++++++++++++ 7 files changed, 374 insertions(+) create mode 100644 gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeData.java create mode 100644 gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeDataGroup.java create mode 100644 gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParser.java create mode 100644 gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParserImpl.java create mode 100644 gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/ProcessingException.java create mode 100644 gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/MediaTypeParserTest.java diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java index 2819bee1..f89c2cb9 100644 --- a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java @@ -794,11 +794,20 @@ static class Endpoint { private final Resource resource; private final Method method; + private final MediaTypeDataGroup mediaTypeConsumes; + private final MediaTypeDataGroup mediaTypeProduces; + // TODO: Change method to "callable" or some such. Endpoint(Resource resource, Method method) { this.resource = resource; this.method = method; + // TODO: Obtain from the method, likely outside then passed in so that + // Method can become "callable" instead. Also, the media type can come + // from the class. + // TODO: Look up "entity provider", section 4.2.3 + mediaTypeConsumes = null; + mediaTypeProduces = null; } } @@ -809,6 +818,10 @@ DispatchMatch getDispatchMatches(String httpMethod, String uri) /*int uriStart = uri.startsWith("/") ? 1 : 0; int uriEnd = uri.length() - (uri.endsWith("/") ? 1 : 0); String normalizedUri = uri.substring(uriStart, uriEnd);*/ + // TODO: Switch to CharSpan. Can also use a modified CharSpan to avoid the + // need to create a normalized uri, since the split can just be told to + // ignore the unnecessary characters (leading/trailing slash, though a + // trailing slash is already ignored by String.split). String[] segments = uri.split("/"); return rootBlock.getDispatchMatch(httpMethod, segments, 0); } diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeData.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeData.java new file mode 100644 index 00000000..1c14a9c0 --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeData.java @@ -0,0 +1,60 @@ +package com.techempower.gemini.jaxrs.core; + +import java.util.Objects; + +class MediaTypeData +{ + // TODO: CharSpan + private final String type; + private final String subtype; + private final double qValue; + + public MediaTypeData(String type, String subtype, double qValue) + { + this.type = Objects.requireNonNull(type); + this.subtype = Objects.requireNonNull(subtype); + this.qValue = qValue; + } + + public String getType() + { + return type; + } + + public String getSubtype() + { + return subtype; + } + + public double getQValue() + { + return qValue; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MediaTypeData that = (MediaTypeData) o; + return Double.compare(that.qValue, qValue) == 0 && + type.equals(that.type) && + subtype.equals(that.subtype); + } + + @Override + public int hashCode() + { + return Objects.hash(type, subtype, qValue); + } + + @Override + public String toString() + { + return "MediaTypeData{" + + "type='" + type + '\'' + + ", subtype='" + subtype + '\'' + + ", qValue=" + qValue + + '}'; + } +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeDataGroup.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeDataGroup.java new file mode 100644 index 00000000..324ee69c --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeDataGroup.java @@ -0,0 +1,42 @@ +package com.techempower.gemini.jaxrs.core; + +import java.util.List; +import java.util.Objects; + +class MediaTypeDataGroup +{ + private final List mediaTypeDataList; + + public MediaTypeDataGroup(List mediaTypeDataList) + { + this.mediaTypeDataList = Objects.requireNonNull(mediaTypeDataList); + } + + public List getMediaTypeDataList() + { + return mediaTypeDataList; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MediaTypeDataGroup that = (MediaTypeDataGroup) o; + return mediaTypeDataList.equals(that.mediaTypeDataList); + } + + @Override + public int hashCode() + { + return Objects.hash(mediaTypeDataList); + } + + @Override + public String toString() + { + return "MediaTypeDataGroup{" + + "mediaTypeDataList=" + mediaTypeDataList + + '}'; + } +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParser.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParser.java new file mode 100644 index 00000000..72cc2908 --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParser.java @@ -0,0 +1,18 @@ +package com.techempower.gemini.jaxrs.core; + +/** + * Specifications: + * + */ +public interface MediaTypeParser +{ + MediaTypeDataGroup parse(String mediaType); +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParserImpl.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParserImpl.java new file mode 100644 index 00000000..c41a01d4 --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParserImpl.java @@ -0,0 +1,132 @@ +package com.techempower.gemini.jaxrs.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class MediaTypeParserImpl + implements MediaTypeParser +{ + private final String qValueKey; + + private static final String WILDCARD = "*"; + private static final Pattern mediaTypePattern; + private static final Pattern parametersPattern; + + static + { + var vCharRange = "-!#%&'*+.^`|~\\w$"; + var token = "[" + vCharRange + "]+"; + var obsTextRange = "\\x80-\\xFF"; + var qdText = "[\t \\x21\\x23-\\x5B\\x5D-\\x7E" + obsTextRange + "]"; + var quotedPair = "\\\\[\t " + vCharRange + obsTextRange + "]"; + var quotedStr = "\"((?:" + qdText + "|" + quotedPair + ")*)\""; + var ows = "[ \t]*"; + + // Group 1: key + // Group 2: unquoted value + // Group 3: quoted value + parametersPattern = Pattern.compile( + ows + ";" + ows + "(" + token + ")=(?:(" + token + ")|(" + quotedStr + "))"); + + // Group 1: type + // Group 2: subtype + // Group 3: parameters + mediaTypePattern = Pattern.compile( + ",?(" + token + ")/(" + token + ")((" + parametersPattern.pattern() + ")*)"); + } + + MediaTypeParserImpl(String qValueKey) + { + this.qValueKey = qValueKey; + } + + @Override + public MediaTypeDataGroup parse(String mediaType) + { + // Immediately fail if a leading comma is present. For simplicity with + // capturing multiple groups, the regex allows a leading comma, but this is + // invalid for the first group. + if (mediaType.charAt(0) == ',') + { + return new MediaTypeDataGroup(List.of()); + } + Matcher mediaTypeMatcher = mediaTypePattern.matcher(mediaType); + List dataList = new ArrayList<>(1); + int mediaTypeEnd = 0; + while (mediaTypeMatcher.find()) + { + if (mediaTypeEnd != mediaTypeMatcher.start()) + { + throw new ProcessingException(String.format( + "Could not fully parse media type \"%s\"," + + " parsed up to position %s.", + mediaType, mediaTypeEnd)); + } + mediaTypeEnd = mediaTypeMatcher.end(); + String type = mediaTypeMatcher.group(1); + String subtype = mediaTypeMatcher.group(2); + if (type.equals(WILDCARD) && !subtype.equals(WILDCARD)) { + throw new ProcessingException(String.format( + "Invalid type/subtype combination \"%s/%s\" in media" + + " type \"%s\", type must be concrete if subtype is concrete.", + type, subtype, mediaType)); + } + double qValue = 1d; + String parameters = mediaTypeMatcher.group(3); + if (parameters != null) + { + Matcher parametersMatcher = parametersPattern.matcher(parameters); + while (parametersMatcher.find()) + { + String key = parametersMatcher.group(1); + String unquotedValue = parametersMatcher.group(2); + // quotedValue will be needed later for lazily-evaluated methods + String quotedValue = parametersMatcher.group(3); + if (unquotedValue == null && quotedValue == null) + { + break; + } + if (key.equalsIgnoreCase(qValueKey) && unquotedValue != null) + { + try + { + qValue = Double.parseDouble(unquotedValue); + } + catch (NumberFormatException e) + { + throw new ProcessingException(String.format( + "Invalid q-value \"%s\" in media type \"%s\"," + + " failed to parse number.", qValue, mediaType), e); + } + if (qValue * 1e4 % 10 != 0) + { + throw new ProcessingException(String.format( + "Invalid q-value \"%s\" in media type \"%s\"," + + " more than 3 decimal places were specified.", + qValue, mediaType)); + } + if (qValue < 0 || qValue > 1) + { + throw new ProcessingException(String.format( + "Invalid q-value \"%s\" in media type \"%s\"," + + " q-value must be between 0 and 1, inclusive.", + qValue, mediaType)); + } + break; + } + } + } + dataList.add(new MediaTypeData(type, subtype, qValue)); + } + if (mediaTypeEnd != mediaType.length()) + { + throw new ProcessingException(String.format( + "Could not fully parse media type \"%s\"," + + " parsed up to position %s.", + mediaType, mediaTypeEnd)); + } + return new MediaTypeDataGroup(dataList); + } +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/ProcessingException.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/ProcessingException.java new file mode 100644 index 00000000..58fa3345 --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/ProcessingException.java @@ -0,0 +1,16 @@ +package com.techempower.gemini.jaxrs.core; + +public class ProcessingException extends RuntimeException +{ + private static final long serialVersionUID = 8411477705739130341L; + + public ProcessingException(String message) + { + super(message); + } + + public ProcessingException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/MediaTypeParserTest.java b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/MediaTypeParserTest.java new file mode 100644 index 00000000..01cfd89c --- /dev/null +++ b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/MediaTypeParserTest.java @@ -0,0 +1,93 @@ +package com.techempower.gemini.jaxrs.core; + +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.runners.Parameterized.Parameters; +import static org.junit.runners.Parameterized.Parameter; + +@RunWith(Enclosed.class) +public class MediaTypeParserTest +{ + @RunWith(Parameterized.class) + public static class ShouldParseSuccessfully + { + @Parameters(name = "q-value key: \"{1}\", input: \"{2}\"") + public static Object[][] params() + { + return new Object[][]{ + // Should support basic captures + {group(new MediaTypeData("text", "html", 1.0d)), "q", "text/html"}, + // Should support q-value + {group(new MediaTypeData("text", "html", 0.9d)), "q", "text/html;q=0.9"}, + // Should support other q-value keys + {group(new MediaTypeData("text", "html", 0.9d)), "qs", "text/html;qs=0.9"}, + {group(new MediaTypeData("text", "html", 0.8d)), "qs", "text/html;q=0.9;qs=0.8"}, + // Should support multi-type captures + {group(new MediaTypeData("text", "html", 1.0d), new MediaTypeData("text", "xml", 0.9d)), "q", "text/html,text/xml;q=0.9"}, + // Should support non q-value parameters + {group(new MediaTypeData("text", "html", 0.8d), new MediaTypeData("text", "xml", 0.9d)), "q", "text/html;f=d;q=0.8,text/xml;q=0.9"}, + // Should respect quote rules (these are a few random variations) + {group(new MediaTypeData("text", "html", 1.0d), new MediaTypeData("text", "xml", 0.9d)), "q", "text/html;f=\"dog\",text/xml;q=0.9"}, + {group(new MediaTypeData("text", "html", 1.0d), new MediaTypeData("text", "xml", 0.9d)), "q", "text/html;f=\"d,og\",text/xml;q=0.9"}, + {group(new MediaTypeData("text", "html", 0.8d), new MediaTypeData("text", "xml", 0.9d)), "q", "text/html;f=\"d,o\";q=0.8,text/xml;q=0.9"}, + // Should respect proper whitespace rules + {group(new MediaTypeData("text", "html", 1.0d), new MediaTypeData("text", "xml", 0.9d)), "q", "text/html,text/xml; \tq=0.9"}, + }; + } + + @Parameter + public MediaTypeDataGroup expected; + + @Parameter(1) + public String qValueKey; + + @Parameter(2) + public String mediaType; + + @Test + public void parse() + { + assertEquals(expected, new MediaTypeParserImpl(qValueKey).parse(mediaType)); + } + + private static MediaTypeDataGroup group(MediaTypeData... mediaTypeData) + { + return new MediaTypeDataGroup(List.of(mediaTypeData)); + } + } + + @RunWith(Parameterized.class) + public static class ShouldFailToParse + { + @Parameters(name = "q-value key: \"{0}\", input: \"{1}\"") + public static Object[][] params() + { + return new Object[][]{ + // Should fail to parse improper placement of quotes + {"q", "text/html;f=\\\"d,og\\\",text/xml;q=0.9"}, + {"q", "text/html;f=\\\"d\";q=0.8,text/xml;q=0.9"}, + {"q", "text/html;f=\\\"d\\\";q=0.8,text/xml;q=0.9"}, + // Should fail to parse improper placement of whitespace + {"q", "text/html,text/xml;q= \t0.9"}, + }; + } + + @Parameter + public String qValueKey; + + @Parameter(1) + public String mediaType; + + @Test(expected = ProcessingException.class) + public void parse() + { + new MediaTypeParserImpl(qValueKey).parse(mediaType); + } + } +} \ No newline at end of file From 2c39980378cf8e07588068f0c618cdd6cdf99053 Mon Sep 17 00:00:00 2001 From: ajohnston Date: Wed, 27 May 2020 03:00:57 -0700 Subject: [PATCH 10/12] Reformat code --- .../techempower/gemini/jaxrs/core/MediaTypeParserImpl.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParserImpl.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParserImpl.java index c41a01d4..26d064e2 100644 --- a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParserImpl.java +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParserImpl.java @@ -10,7 +10,7 @@ class MediaTypeParserImpl { private final String qValueKey; - private static final String WILDCARD = "*"; + private static final String WILDCARD = "*"; private static final Pattern mediaTypePattern; private static final Pattern parametersPattern; @@ -67,7 +67,8 @@ public MediaTypeDataGroup parse(String mediaType) mediaTypeEnd = mediaTypeMatcher.end(); String type = mediaTypeMatcher.group(1); String subtype = mediaTypeMatcher.group(2); - if (type.equals(WILDCARD) && !subtype.equals(WILDCARD)) { + if (type.equals(WILDCARD) && !subtype.equals(WILDCARD)) + { throw new ProcessingException(String.format( "Invalid type/subtype combination \"%s/%s\" in media" + " type \"%s\", type must be concrete if subtype is concrete.", From b1835d8c8e09dde88e13a32f255b49169605402e Mon Sep 17 00:00:00 2001 From: ajohnston Date: Thu, 28 May 2020 03:33:55 -0700 Subject: [PATCH 11/12] Refactor MediaTypeData/MediaTypeDataGroup to capture all parameters Also MediaTypeData now works much more like MediaType, except lazily in one implementation. --- .../gemini/jaxrs/core/CharSpan.java | 107 ++++++++++ .../gemini/jaxrs/core/JaxRsDispatcher.java | 6 +- .../gemini/jaxrs/core/LazyQMediaType.java | 60 ++++++ .../gemini/jaxrs/core/MediaTypeDataGroup.java | 42 ---- .../gemini/jaxrs/core/MediaTypeParser.java | 2 +- .../jaxrs/core/MediaTypeParserImpl.java | 77 ++++--- .../gemini/jaxrs/core/QMediaType.java | 67 ++++++ .../gemini/jaxrs/core/QMediaTypeGroup.java | 42 ++++ .../jaxrs/core/StringStringCharSpanMap.java | 191 ++++++++++++++++++ .../gemini/jaxrs/core/WrappedQMediaType.java | 47 +++++ .../jaxrs/core/CharBufferSplitTest.java | 75 ------- .../jaxrs/core/MediaTypeParserTest.java | 44 ++-- 12 files changed, 595 insertions(+), 165 deletions(-) create mode 100644 gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/CharSpan.java create mode 100644 gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/LazyQMediaType.java delete mode 100644 gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeDataGroup.java create mode 100644 gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/QMediaType.java create mode 100644 gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/QMediaTypeGroup.java create mode 100644 gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/StringStringCharSpanMap.java create mode 100644 gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/WrappedQMediaType.java diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/CharSpan.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/CharSpan.java new file mode 100644 index 00000000..d225e4a4 --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/CharSpan.java @@ -0,0 +1,107 @@ +package com.techempower.gemini.jaxrs.core; + +public class CharSpan implements CharSequence +{ + private final CharSequence charSequence; + private final int start; + private final int end; + private int hash = 1; + private String toString; + + CharSpan(CharSequence charSequence, int start, int end) + { + this.charSequence = charSequence; + this.start = start; + this.end = end; + } + + CharSpan(String string) + { + this(string, 0, string.length()); + this.toString = string; + } + + public int getStart() + { + return start; + } + + public int getEnd() + { + return end; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + CharSpan charSpan = (CharSpan) o; + int length = length(); + if (length != charSpan.length()) + { + return false; + } + for (int i = 0; i < length; i++) + { + if (charAt(i) != charSpan.charAt(i)) + { + return false; + } + } + return true; + } + + @Override + public int hashCode() + { + if (hash == 1 && length() > 0) + { + int h = 1; + for (int i = end - 1; i >= start; i--) + { + h = 31 * h + charAt(i - start); + } + hash = h; + } + return hash; + } + + @Override + public int length() + { + return end - start; + } + + @Override + public char charAt(int index) + { + return charSequence.charAt(start + index); + } + + @Override + public CharSequence subSequence(int start, int end) + { + if (start == 0 && end == length()) + { + return this; + } + return charSequence.subSequence(this.start + start, this.start + end); + } + + @Override + public String toString() + { + if (toString == null) + { + toString = charSequence.subSequence(start, end).toString(); + } + return toString; + } +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java index f89c2cb9..eabb98bb 100644 --- a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java @@ -793,9 +793,9 @@ public Class getInstanceClass() static class Endpoint { private final Resource resource; - private final Method method; - private final MediaTypeDataGroup mediaTypeConsumes; - private final MediaTypeDataGroup mediaTypeProduces; + private final Method method; + private final QMediaTypeGroup mediaTypeConsumes; + private final QMediaTypeGroup mediaTypeProduces; // TODO: Change method to "callable" or some such. Endpoint(Resource resource, Method method) diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/LazyQMediaType.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/LazyQMediaType.java new file mode 100644 index 00000000..7bba2c96 --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/LazyQMediaType.java @@ -0,0 +1,60 @@ +package com.techempower.gemini.jaxrs.core; + +import javax.ws.rs.core.MediaType; +import java.util.Collections; +import java.util.Map; + +public class LazyQMediaType extends QMediaType +{ + private final CharSpan type; + private final CharSpan subtype; + private final double qValue; + private final Map unmodifiableParameters; + private MediaType mediaType; + + public LazyQMediaType(CharSpan type, + CharSpan subtype, + double qValue, + Map parameters) + { + this.type = type; + this.subtype = subtype; + this.qValue = qValue; + this.unmodifiableParameters = Collections.unmodifiableMap( + new StringStringCharSpanMap(parameters)); + } + + @Override + public String getType() + { + return type.toString(); + } + + @Override + public String getSubtype() + { + return subtype.toString(); + } + + @Override + public double getQValue() + { + return qValue; + } + + @Override + public Map getParameters() + { + return unmodifiableParameters; + } + + @Override + public MediaType getMediaType() + { + if (mediaType == null) + { + mediaType = new MediaType(getType(), getSubtype(), getParameters()); + } + return mediaType; + } +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeDataGroup.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeDataGroup.java deleted file mode 100644 index 324ee69c..00000000 --- a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeDataGroup.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.techempower.gemini.jaxrs.core; - -import java.util.List; -import java.util.Objects; - -class MediaTypeDataGroup -{ - private final List mediaTypeDataList; - - public MediaTypeDataGroup(List mediaTypeDataList) - { - this.mediaTypeDataList = Objects.requireNonNull(mediaTypeDataList); - } - - public List getMediaTypeDataList() - { - return mediaTypeDataList; - } - - @Override - public boolean equals(Object o) - { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - MediaTypeDataGroup that = (MediaTypeDataGroup) o; - return mediaTypeDataList.equals(that.mediaTypeDataList); - } - - @Override - public int hashCode() - { - return Objects.hash(mediaTypeDataList); - } - - @Override - public String toString() - { - return "MediaTypeDataGroup{" + - "mediaTypeDataList=" + mediaTypeDataList + - '}'; - } -} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParser.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParser.java index 72cc2908..3c32bccb 100644 --- a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParser.java +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParser.java @@ -14,5 +14,5 @@ */ public interface MediaTypeParser { - MediaTypeDataGroup parse(String mediaType); + QMediaTypeGroup parse(String mediaType); } diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParserImpl.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParserImpl.java index 26d064e2..925c0b12 100644 --- a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParserImpl.java +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParserImpl.java @@ -1,7 +1,9 @@ package com.techempower.gemini.jaxrs.core; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -10,9 +12,9 @@ class MediaTypeParserImpl { private final String qValueKey; - private static final String WILDCARD = "*"; - private static final Pattern mediaTypePattern; - private static final Pattern parametersPattern; + private static final CharSpan WILDCARD = new CharSpan("*"); + private static final Pattern MEDIA_TYPE_PATTERN; + private static final Pattern PARAMETERS_PATTERN; static { @@ -27,14 +29,14 @@ class MediaTypeParserImpl // Group 1: key // Group 2: unquoted value // Group 3: quoted value - parametersPattern = Pattern.compile( - ows + ";" + ows + "(" + token + ")=(?:(" + token + ")|(" + quotedStr + "))"); + PARAMETERS_PATTERN = Pattern.compile( + ows + ";" + ows + "(" + token + ")=(?:(" + token + ")|(?:" + quotedStr + "))"); // Group 1: type // Group 2: subtype // Group 3: parameters - mediaTypePattern = Pattern.compile( - ",?(" + token + ")/(" + token + ")((" + parametersPattern.pattern() + ")*)"); + MEDIA_TYPE_PATTERN = Pattern.compile( + ",?(" + token + ")/(" + token + ")((" + PARAMETERS_PATTERN.pattern() + ")*)"); } MediaTypeParserImpl(String qValueKey) @@ -43,17 +45,19 @@ class MediaTypeParserImpl } @Override - public MediaTypeDataGroup parse(String mediaType) + public QMediaTypeGroup parse(String mediaType) { - // Immediately fail if a leading comma is present. For simplicity with - // capturing multiple groups, the regex allows a leading comma, but this is - // invalid for the first group. + // Immediately fail if a leading comma is present. The regex allows a + // leading comma for simplicity with capturing multiple matches, but this + // is invalid for the first match. if (mediaType.charAt(0) == ',') { - return new MediaTypeDataGroup(List.of()); + throw new ProcessingException(String.format( + "Could not fully parse media type \"%s\"," + + " parsed up to position 0.", mediaType)); } - Matcher mediaTypeMatcher = mediaTypePattern.matcher(mediaType); - List dataList = new ArrayList<>(1); + Matcher mediaTypeMatcher = MEDIA_TYPE_PATTERN.matcher(mediaType); + List mediaTypes = new ArrayList<>(1); int mediaTypeEnd = 0; while (mediaTypeMatcher.find()) { @@ -65,8 +69,10 @@ public MediaTypeDataGroup parse(String mediaType) mediaType, mediaTypeEnd)); } mediaTypeEnd = mediaTypeMatcher.end(); - String type = mediaTypeMatcher.group(1); - String subtype = mediaTypeMatcher.group(2); + CharSpan type = new CharSpan(mediaType, mediaTypeMatcher.start(1), + mediaTypeMatcher.end(1)); + CharSpan subtype = new CharSpan(mediaType, mediaTypeMatcher.start(2), + mediaTypeMatcher.end(2)); if (type.equals(WILDCARD) && !subtype.equals(WILDCARD)) { throw new ProcessingException(String.format( @@ -75,25 +81,30 @@ public MediaTypeDataGroup parse(String mediaType) type, subtype, mediaType)); } double qValue = 1d; - String parameters = mediaTypeMatcher.group(3); - if (parameters != null) + int mediaTypeGroup3Start = mediaTypeMatcher.start(3); + Map parametersMap; + if (mediaTypeGroup3Start != -1) { - Matcher parametersMatcher = parametersPattern.matcher(parameters); + CharSpan parameters = new CharSpan(mediaType, mediaTypeGroup3Start, + mediaTypeMatcher.end(3)); + Matcher parametersMatcher = PARAMETERS_PATTERN.matcher(parameters); + parametersMap = new HashMap<>(0); while (parametersMatcher.find()) { - String key = parametersMatcher.group(1); - String unquotedValue = parametersMatcher.group(2); - // quotedValue will be needed later for lazily-evaluated methods - String quotedValue = parametersMatcher.group(3); - if (unquotedValue == null && quotedValue == null) - { - break; - } - if (key.equalsIgnoreCase(qValueKey) && unquotedValue != null) + CharSpan key = new CharSpan(parameters, + parametersMatcher.start(1), parametersMatcher.end(1)); + CharSpan unquotedValue = new CharSpan(parameters, + parametersMatcher.start(2), parametersMatcher.end(2)); + CharSpan quotedValue = new CharSpan(parameters, + parametersMatcher.start(3), parametersMatcher.end(3)); + CharSpan value = unquotedValue.getStart() != -1 + ? unquotedValue : quotedValue; + parametersMap.put(key, value); + if (key.toString().equalsIgnoreCase(qValueKey)) { try { - qValue = Double.parseDouble(unquotedValue); + qValue = Double.parseDouble(value.toString()); } catch (NumberFormatException e) { @@ -119,7 +130,11 @@ public MediaTypeDataGroup parse(String mediaType) } } } - dataList.add(new MediaTypeData(type, subtype, qValue)); + else + { + parametersMap = Map.of(); + } + mediaTypes.add(new LazyQMediaType(type, subtype, qValue, parametersMap)); } if (mediaTypeEnd != mediaType.length()) { @@ -128,6 +143,6 @@ public MediaTypeDataGroup parse(String mediaType) " parsed up to position %s.", mediaType, mediaTypeEnd)); } - return new MediaTypeDataGroup(dataList); + return new QMediaTypeGroup(mediaTypes); } } diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/QMediaType.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/QMediaType.java new file mode 100644 index 00000000..1ddc3dc8 --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/QMediaType.java @@ -0,0 +1,67 @@ +package com.techempower.gemini.jaxrs.core; + +import javax.ws.rs.core.MediaType; +import java.util.Map; +import java.util.Objects; + +public abstract class QMediaType +{ + static final String ONE = "1"; + + private boolean hashFound; + private int hash; + + public abstract double getQValue(); + + public abstract String getType(); + + public abstract String getSubtype(); + + public abstract Map getParameters(); + + public abstract MediaType getMediaType(); + + public boolean isCompatible(MediaType other) + { + return other != null && // return false if other is null, else + (getType().equals(MediaType.MEDIA_TYPE_WILDCARD) || other.getType().equals(MediaType.MEDIA_TYPE_WILDCARD) || // both are wildcard types, or + (getType().equalsIgnoreCase(other.getType()) && (getSubtype().equals(MediaType.MEDIA_TYPE_WILDCARD) + || other.getSubtype().equals(MediaType.MEDIA_TYPE_WILDCARD))) || // same types, wildcard sub-types, or + (getType().equalsIgnoreCase(other.getType()) && getSubtype().equalsIgnoreCase(other.getSubtype()))); // same types & sub-types + } + + @Override + public final boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof QMediaType)) return false; + QMediaType that = (QMediaType) o; + return getQValue() == that.getQValue() + && getType().equalsIgnoreCase(that.getType()) + && getSubtype().equalsIgnoreCase(that.getSubtype()) + && getParameters().equals(that.getParameters()); + } + + @Override + public int hashCode() + { + if (!hashFound) + { + hash = Objects.hash(getQValue(), getType().toLowerCase(), + getSubtype().toLowerCase(), getParameters()); + hashFound = true; + } + return hash; + } + + @Override + public String toString() + { + return "QMediaType{" + + "type='" + getType() + '\'' + + ", subtype='" + getSubtype() + '\'' + + ", parameters=" + getParameters() + + ", qValue=" + getQValue() + + '}'; + } +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/QMediaTypeGroup.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/QMediaTypeGroup.java new file mode 100644 index 00000000..4c592164 --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/QMediaTypeGroup.java @@ -0,0 +1,42 @@ +package com.techempower.gemini.jaxrs.core; + +import java.util.List; +import java.util.Objects; + +class QMediaTypeGroup +{ + private final List mediaTypes; + + public QMediaTypeGroup(List mediaTypes) + { + this.mediaTypes = Objects.requireNonNull(mediaTypes); + } + + public List getMediaTypes() + { + return mediaTypes; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + QMediaTypeGroup that = (QMediaTypeGroup) o; + return mediaTypes.equals(that.mediaTypes); + } + + @Override + public int hashCode() + { + return Objects.hash(mediaTypes); + } + + @Override + public String toString() + { + return "MediaTypeDataGroup{" + + "mediaTypes=[" + mediaTypes + + "]}"; + } +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/StringStringCharSpanMap.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/StringStringCharSpanMap.java new file mode 100644 index 00000000..a16a164c --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/StringStringCharSpanMap.java @@ -0,0 +1,191 @@ +package com.techempower.gemini.jaxrs.core; + +import org.checkerframework.checker.units.qual.K; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +class StringStringCharSpanMap + implements Map +{ + private final Map backingMap; + private Map stringMap; + + StringStringCharSpanMap(StringStringCharSpanMap sourceMap) + { + this.backingMap = new HashMap<>(sourceMap.backingMap); + } + + public StringStringCharSpanMap(Map backingMap) + { + this.backingMap = backingMap; + } + + boolean isEvaluated() + { + return backingMap != null; + } + + protected Map getStringMap() + { + if (stringMap == null) + { + Map map = new HashMap<>(backingMap.size()); + backingMap.forEach((key, value) -> + map.put(key.toString(), value.toString())); + stringMap = map; + } + return stringMap; + } + + @Override + public int size() + { + if (stringMap == null) + { + return backingMap.size(); + } + return stringMap.size(); + } + + @Override + public boolean isEmpty() + { + if (stringMap == null) + { + return backingMap.isEmpty(); + } + return stringMap.isEmpty(); + } + + @Override + public boolean containsKey(Object key) + { + return getStringMap().containsKey(key); + } + + @Override + public boolean containsValue(Object value) + { + return getStringMap().containsValue(value); + } + + @Override + public String get(Object key) + { + return getStringMap().get(key); + } + + @Override + public String put(String key, String value) + { + return getStringMap().put(key, value); + } + + /** + * Updates the internal CharSpan CharSpan backing map. Does not update the + * String String map if it has been evaluated. + */ + public void putInternal(CharSpan key, CharSpan value) + { + backingMap.put(key, value); + } + + @Override + public String remove(Object key) + { + return getStringMap().remove(key); + } + + @Override + public void putAll(Map m) + { + getStringMap().putAll(m); + } + + @Override + public void clear() + { + getStringMap().clear(); + } + + @Override + public Set keySet() + { + return getStringMap().keySet(); + } + + @Override + public Collection values() + { + return getStringMap().values(); + } + + @Override + public Set> entrySet() + { + return getStringMap().entrySet(); + } + + @Override + public int hashCode() + { + return getStringMap().hashCode(); + } + + @Override + public boolean equals(Object o) + { + if (o == this) + { + return true; + } + + if (!(o instanceof Map)) + { + return false; + } + Map m = (Map) o; + if (m.size() != size()) + { + return false; + } + + try + { + for (Entry e : entrySet()) + { + String key = e.getKey(); + String value = e.getValue(); + if (value == null) + { + if (!(m.get(key) == null && m.containsKey(key))) + { + return false; + } + } + else + { + if (!value.equals(m.get(key))) + { + return false; + } + } + } + } + catch (ClassCastException | NullPointerException unused) + { + return false; + } + + return true; + } + + @Override + public String toString() + { + return getStringMap().toString(); + } +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/WrappedQMediaType.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/WrappedQMediaType.java new file mode 100644 index 00000000..fca80fe8 --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/WrappedQMediaType.java @@ -0,0 +1,47 @@ +package com.techempower.gemini.jaxrs.core; + +import javax.ws.rs.core.MediaType; +import java.util.Map; + +public class WrappedQMediaType extends QMediaType +{ + private final MediaType mediaType; + + /** + * Creates a QMediaType wrapping the given media type, associating it with + * the default q value 1. + */ + public WrappedQMediaType(MediaType mediaType) + { + this.mediaType = mediaType; + } + + @Override + public double getQValue() + { + return 1; + } + + @Override + public String getType() + { + return mediaType.getType(); + } + + @Override + public String getSubtype() + { + return mediaType.getSubtype(); + } + + @Override + public Map getParameters() + { + return mediaType.getParameters(); + } + + @Override + public MediaType getMediaType() { + return mediaType; + } +} diff --git a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/CharBufferSplitTest.java b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/CharBufferSplitTest.java index d59d6895..f3e039c3 100644 --- a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/CharBufferSplitTest.java +++ b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/CharBufferSplitTest.java @@ -306,79 +306,4 @@ private static String[] splitStringArray(String str) return str.split("/"); } - private static class CharSpan implements CharSequence - { - private final CharSequence charSequence; - private final int start; - private final int end; - - private CharSpan(CharSequence charSequence, int start, int end) - { - this.charSequence = charSequence; - this.start = start; - this.end = end; - } - - @Override - public boolean equals(Object o) - { - if (this == o) - { - return true; - } - if (o == null || getClass() != o.getClass()) - { - return false; - } - CharSpan charSpan = (CharSpan) o; - int length = length(); - if (length != charSpan.length()) - { - return false; - } - for (int i = 0; i < length; i++) - { - if (charAt(i) != charSpan.charAt(i)) - { - return false; - } - } - return true; - } - - @Override - public int hashCode() - { - int h = 1; - for (int i = end - 1; i >= start; i--) - { - h = 31 * h + charAt(i); - } - return h; - } - - @Override - public int length() - { - return end - start; - } - - @Override - public char charAt(int index) - { - return charSequence.charAt(start + index); - } - - @Override - public CharSequence subSequence(int start, int end) - { - return charSequence.subSequence(this.start + start, this.start + end); - } - - @Override - public String toString() - { - return charSequence.subSequence(start, end).toString(); - } - } } diff --git a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/MediaTypeParserTest.java b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/MediaTypeParserTest.java index 01cfd89c..1b9aa3d2 100644 --- a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/MediaTypeParserTest.java +++ b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/MediaTypeParserTest.java @@ -5,7 +5,9 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import javax.ws.rs.core.MediaType; import java.util.List; +import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.runners.Parameterized.Parameters; @@ -22,27 +24,29 @@ public static Object[][] params() { return new Object[][]{ // Should support basic captures - {group(new MediaTypeData("text", "html", 1.0d)), "q", "text/html"}, + {group(mediaType("text", "html", 1.0d, Map.of())), "q", "text/html"}, // Should support q-value - {group(new MediaTypeData("text", "html", 0.9d)), "q", "text/html;q=0.9"}, + {group(mediaType("text", "html", 0.9d, Map.of("q", "0.9"))), "q", "text/html;q=0.9"}, + {group(mediaType("text", "html", 1.0d, Map.of("q", "1.0"))), "q", "text/html;q=1.0"}, + {group(mediaType("text", "html", 0.999d, Map.of("q", "0.999"))), "q", "text/html;q=0.999"}, // Should support other q-value keys - {group(new MediaTypeData("text", "html", 0.9d)), "qs", "text/html;qs=0.9"}, - {group(new MediaTypeData("text", "html", 0.8d)), "qs", "text/html;q=0.9;qs=0.8"}, + {group(mediaType("text", "html", 0.9d, Map.of("qs", "0.9"))), "qs", "text/html;qs=0.9"}, + {group(mediaType("text", "html", 0.8d, Map.of("q", "0.9", "qs", "0.8"))), "qs", "text/html;q=0.9;qs=0.8"}, // Should support multi-type captures - {group(new MediaTypeData("text", "html", 1.0d), new MediaTypeData("text", "xml", 0.9d)), "q", "text/html,text/xml;q=0.9"}, + {group(mediaType("text", "html", 1.0d, Map.of()), mediaType("text", "xml", 0.9d, Map.of("q", "0.9"))), "q", "text/html,text/xml;q=0.9"}, // Should support non q-value parameters - {group(new MediaTypeData("text", "html", 0.8d), new MediaTypeData("text", "xml", 0.9d)), "q", "text/html;f=d;q=0.8,text/xml;q=0.9"}, + {group(mediaType("text", "html", 0.8d, Map.of("f", "d", "q", "0.8")), mediaType("text", "xml", 0.9d, Map.of("q", "0.9"))), "q", "text/html;f=d;q=0.8,text/xml;q=0.9"}, // Should respect quote rules (these are a few random variations) - {group(new MediaTypeData("text", "html", 1.0d), new MediaTypeData("text", "xml", 0.9d)), "q", "text/html;f=\"dog\",text/xml;q=0.9"}, - {group(new MediaTypeData("text", "html", 1.0d), new MediaTypeData("text", "xml", 0.9d)), "q", "text/html;f=\"d,og\",text/xml;q=0.9"}, - {group(new MediaTypeData("text", "html", 0.8d), new MediaTypeData("text", "xml", 0.9d)), "q", "text/html;f=\"d,o\";q=0.8,text/xml;q=0.9"}, + {group(mediaType("text", "html", 1.0d, Map.of("f", "dog")), mediaType("text", "xml", 0.9d, Map.of("q", "0.9"))), "q", "text/html;f=\"dog\",text/xml;q=0.9"}, + {group(mediaType("text", "html", 1.0d, Map.of("f", "d,og")), mediaType("text", "xml", 0.9d, Map.of("q", "0.9"))), "q", "text/html;f=\"d,og\",text/xml;q=0.9"}, + {group(mediaType("text", "html", 0.8d, Map.of("f", "d,o", "q", "0.8")), mediaType("text", "xml", 0.9d, Map.of("q", "0.9"))), "q", "text/html;f=\"d,o\";q=0.8,text/xml;q=0.9"}, // Should respect proper whitespace rules - {group(new MediaTypeData("text", "html", 1.0d), new MediaTypeData("text", "xml", 0.9d)), "q", "text/html,text/xml; \tq=0.9"}, + {group(mediaType("text", "html", 1.0d, Map.of()), mediaType("text", "xml", 0.9d, Map.of("q", "0.9"))), "q", "text/html,text/xml; \tq=0.9"}, }; } @Parameter - public MediaTypeDataGroup expected; + public QMediaTypeGroup expected; @Parameter(1) public String qValueKey; @@ -56,9 +60,21 @@ public void parse() assertEquals(expected, new MediaTypeParserImpl(qValueKey).parse(mediaType)); } - private static MediaTypeDataGroup group(MediaTypeData... mediaTypeData) + private static QMediaType mediaType(String type, String subtype, double qValue, Map parameters) { - return new MediaTypeDataGroup(List.of(mediaTypeData)); + return new WrappedQMediaType(new MediaType(type, subtype, parameters)) + { + @Override + public double getQValue() + { + return qValue; + } + }; + } + + private static QMediaTypeGroup group(QMediaType... mediaTypes) + { + return new QMediaTypeGroup(List.of(mediaTypes)); } } @@ -75,6 +91,8 @@ public static Object[][] params() {"q", "text/html;f=\\\"d\\\";q=0.8,text/xml;q=0.9"}, // Should fail to parse improper placement of whitespace {"q", "text/html,text/xml;q= \t0.9"}, + // Should fail to parse excessive numbers after decimal point + {"q", "text/html,text/xml;q=0.9999"}, }; } From 24b68e5ee81f012fc1f9097ec9418db0ba450095 Mon Sep 17 00:00:00 2001 From: ajohnston Date: Wed, 5 Aug 2020 02:45:21 -0700 Subject: [PATCH 12/12] Add a functional, non-performant endpoint registry, plus incomplete (but passing) tests This should help with designing more complicated tests and work as a reference implementation for the performant version. There's also a dispatching test that is added because I was feeling optimistic, but really it's too high level. The endpoint registry test does a better job of testing the target functionality more precisely. I'll likely remove the dispatching test at some point. --- gemini-jax-rs/pom.xml | 6 +- .../gemini/jaxrs/core/Endpoint.java | 16 ++ .../gemini/jaxrs/core/EndpointMetadata.java | 42 +++ .../gemini/jaxrs/core/EndpointRegistry.java | 14 + .../gemini/jaxrs/core/JaxRsDispatcher.java | 8 + .../jaxrs/core/MediaTypeParserImpl.java | 2 +- .../gemini/jaxrs/core/QMediaTypeGroup.java | 2 + .../jaxrs/core/SimpleCombinedQMediaType.java | 223 +++++++++++++++ .../jaxrs/core/SimpleEndpointRegistry.java | 177 ++++++++++++ .../gemini/jaxrs/core/WrappedQMediaType.java | 14 +- .../jaxrs/core/EndpointRegistryTest.java | 126 +++++++++ .../core/JaxRsMediaTypeDispatchingTest.java | 53 ++++ .../jaxrs/core/MediaTypeParserTest.java | 10 +- .../core/SimpleCombinedQMediaTypeTest.java | 258 ++++++++++++++++++ pom.xml | 5 + 15 files changed, 945 insertions(+), 11 deletions(-) create mode 100644 gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/Endpoint.java create mode 100644 gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/EndpointMetadata.java create mode 100644 gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/EndpointRegistry.java create mode 100644 gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/SimpleCombinedQMediaType.java create mode 100644 gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/SimpleEndpointRegistry.java create mode 100644 gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/EndpointRegistryTest.java create mode 100644 gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsMediaTypeDispatchingTest.java create mode 100644 gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/SimpleCombinedQMediaTypeTest.java diff --git a/gemini-jax-rs/pom.xml b/gemini-jax-rs/pom.xml index 7beeda8b..6a4aa66c 100644 --- a/gemini-jax-rs/pom.xml +++ b/gemini-jax-rs/pom.xml @@ -23,7 +23,6 @@ 3.1.0-SNAPSHOT - com.techempower gemini-jax-rs An extension for Gemini that provides dispatching implemented as a subset of JAX-RS. @@ -48,6 +47,11 @@ mockito-all test + + org.slf4j + slf4j-simple + test +