C getCookie(String name) {
+ final String cookieString = frame.getRequestHeader(HttpHeader.Cookie);
+ // fixme
+ return null;
+ }
+
+ @Override
+ public void setCookie(String name, String value, String domain, String path, int age, boolean secure) {
+ // fixme
+ }
+
+ @Override
+ public void deleteCookie(String name, String path) {
+ //fixme
+ }
+
+ @Override
+ public String getClientId() {
+ return channel.getRemoteAddr();
+ }
+
+ @Override
+ public HttpMethod getRequestMethod() {
+ return HttpMethod.valueOf(frame.getMethod().getValue());
+ }
+
+ @Override
+ @Deprecated
+ /**
+ * @deprecated
+ */
+ public InputStream getInputStream() throws IOException {
+ return null;
+ }
+
+ @Override
+ public boolean redirect(String redirectDestinationUrl) {
+ // fixme
+ return false;
+ }
+
+ @Override
+ public boolean redirectPermanent(String redirectDestinationUrl) {
+ // fixme
+ return false;
+ }
+
+ @Override
+ public void setResponseHeader(String headerName, String value) {
+ frame.setResponseHeader(HttpHeader.valueOf(headerName), value.getBytes());
+ }
+
+ @Override
+ @Deprecated
+ /**
+ * @deprecated Output stream was how blocking servers handled IO, but
+ * Firenio uses the NIO message model, so by the time an HttpRequest is
+ * constructed and available, the entire HttpMessage has been read.
+ * @see #getChannel()
+ * @see #getFrame()
+ */
+ public OutputStream getOutputStream() throws IOException {
+ return null;
+ }
+
+ /**
+ * Returns the underlying reference to HttpChannel
+ */
+ public Channel getChannel() {
+ return channel;
+ }
+
+ /**
+ * Returns the underlying reference to the HttpFrame
+ */
+ public Frame getFrame() {
+ return frame;
+ }
+
+ @Override
+ public String getRequestContentType() {
+ return frame.getRequestHeader(HttpHeader.Content_Type);
+ }
+
+ @Override
+ public void setContentType(String contentType) {
+ frame.setResponseHeader(HttpHeader.Content_Type, contentType.getBytes());
+ }
+
+ @Override
+ public void setExpiration(int secondsFromNow) {
+ frame.setResponseHeader(HttpHeader.Expires,
+ (System.currentTimeMillis() + (secondsFromNow * UtilityConstants.SECOND) + "").getBytes());
+ }
+
+ @Override
+ public String getCurrentURI() {
+ // fixme
+ return null;
+ }
+
+ @Deprecated
+ @Override
+ /**
+ * @deprecated
+ */
+ public boolean isSecure() {
+ return false;
+ }
+
+ @Override
+ public boolean isCommitted() {
+ return !channel.isOpen();
+ }
+
+ @Override
+ public String getQueryString() {
+ // fixme
+ return null;
+ }
+
+ @Override
+ public Session getSession(boolean create) {
+ // fixme
+ return null;
+ }
+
+ @Override
+ public void setAttribute(String name, Object o) {
+ // fixme
+ }
+
+ @Override
+ public Object getAttribute(String name) {
+ // fixme
+ return null;
+ }
+
+ @Override
+ public Infrastructure getInfrastructure() {
+ return application.getInfrastructure();
+ }
+
+ @Override
+ public boolean isHead() {
+ // fixme
+ return false;
+// return frame.isHead();
+ }
+
+ @Override
+ public boolean isGet() {
+ return frame.isGet();
+ }
+
+ @Override
+ public boolean isPost() {
+ // fixme
+ return false;
+// return frame.isPost();
+ }
+
+ @Override
+ public boolean isPut() {
+ // fixme
+ return false;
+// return frame.isPut();
+ }
+
+ @Override
+ public boolean isDelete() {
+ // fixme
+ return false;
+// return frame.isDelete();
+ }
+
+ @Override
+ public boolean isTrace() {
+ // fixme
+ return false;
+// return frame.isTrace();
+ }
+
+ @Override
+ public boolean isOptions() {
+ // fixme
+ return false;
+// return frame.isOptions();
+ }
+
+ @Override
+ public boolean isConnect() {
+ // fixme
+ return false;
+ }
+
+ @Override
+ public boolean isPatch() {
+ // fixme
+ return false;
+// return frame.isPatch();
+ }
+
+ @Override
+ public void setStatus(int status) {
+ frame.setStatus(HttpStatus.get(status));
+ }
+}
diff --git a/gemini-firenio/src/main/java/com/techempower/gemini/firenio/lifecycle/InitAnnotationDispatcher.java b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/lifecycle/InitAnnotationDispatcher.java
new file mode 100644
index 00000000..5229b411
--- /dev/null
+++ b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/lifecycle/InitAnnotationDispatcher.java
@@ -0,0 +1,22 @@
+package com.techempower.gemini.firenio.lifecycle;
+
+import com.techempower.gemini.Dispatcher;
+import com.techempower.gemini.GeminiApplication;
+import com.techempower.gemini.lifecycle.InitializationTask;
+import com.techempower.gemini.firenio.path.AnnotationDispatcher;
+
+/**
+ * Initializes the AnnotationDispatcher, if one is enabled within the
+ * application.
+ */
+public class InitAnnotationDispatcher implements InitializationTask {
+ @Override
+ public void taskInitialize(GeminiApplication application)
+ {
+ final Dispatcher dispatcher = application.getDispatcher();
+ if (dispatcher != null && dispatcher instanceof AnnotationDispatcher)
+ {
+ ((AnnotationDispatcher)dispatcher).initialize();
+ }
+ }
+}
diff --git a/gemini-firenio/src/main/java/com/techempower/gemini/firenio/monitor/FirenioMonitor.java b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/monitor/FirenioMonitor.java
new file mode 100644
index 00000000..5751764c
--- /dev/null
+++ b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/monitor/FirenioMonitor.java
@@ -0,0 +1,114 @@
+/*******************************************************************************
+ * Copyright (c) 2018, 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.firenio.monitor;
+
+import com.techempower.gemini.*;
+import com.techempower.gemini.monitor.GeminiMonitor;
+import com.techempower.gemini.monitor.session.*;
+
+/**
+ * The main class for Gemini application-monitoring functionality.
+ * Applications should instantiate an instance of Monitor and then attach
+ * the provided MonitorListener as a DatabaseConnectionListener and Dispatch
+ * Listener.
+ *
+ * The Monitor has four sub-components:
+ *
+ * - Performance monitoring, the main component, observes the execution
+ * of requests to trend the performance of each type of request over
+ * time.
+ * - Health monitoring, an optional component, observes the total amount
+ * of memory used, the number of threads, and other macro-level concerns
+ * to evaluate the health of the application.
+ * - CPU Usage Percentage monitoring, an optional component, uses JMX to
+ * observe the CPU time of Java threads and provide a rough usage
+ * percentage per thread in 1-second real-time samples.
+ * - Web session monitoring, an optional component, that counts and
+ * optionally maintains a set of active web sessions.
+ *
+ * Configurable options:
+ *
+ * - Feature.monitor - Is the Gemini Monitoring component enabled as a
+ * whole? Defaults to yes.
+ * - Feature.monitor.health - Is the Health Monitoring sub-component
+ * enabled? Defaults to yes.
+ * - Feature.monitor.cpu - Is the CPU Usage Percentage sub-component
+ * enabled? Defaults to yes.
+ * - Feature.monitor.session - Is the Session Monitoring sub-component
+ * enabled?
+ * - GeminiMonitor.HealthSnapshotCount - The number of health snapshots to
+ * retain in memory. The default is 120. Cannot be lower than 2 or
+ * greater than 30000.
+ * - GeminiMonitor.HealthSnapshotInterval - The number of milliseconds
+ * between snapshots. The default is 300000 (5 minutes). Cannot be set
+ * below 500ms or greater than 1 year.
+ * - GeminiMonitor.SessionSnapshotCount - The number of session snapshots to
+ * retain in memory. The defaults are the same as Health snapshots.
+ * - GeminiMonitor.SessionSnapshotInterval - The number of milliseconds
+ * between snapshots. Defaults same as for health.
+ * - GeminiMonitor.SessionTracking - If true, active sessions will be
+ * tracked by the session monitor to allow for listing active sessions.
+ *
+ *
+ * Note that some of the operations executed by the health snapshot are non
+ * trivial (e.g., 10-20 milliseconds). Setting a very low snapshot interval
+ * such as 500ms would mean that every 500ms, you may be consuming about
+ * 25ms of CPU time to take a snapshot. An interval of 1 minute should be
+ * suitable for most applications.
+ */
+public class FirenioMonitor
+ extends GeminiMonitor
+{
+
+ /**
+ * Constructor.
+ */
+ public FirenioMonitor(GeminiApplication app)
+ {
+ super(app);
+ }
+
+ @Override
+ public SessionState getSessionState()
+ {
+ // fixme
+ return new SessionState(this) {
+ @Override
+ public int getSessionCount() {
+ return super.getSessionCount();
+ }
+ };
+ }
+
+ @Override
+ protected void addSessionListener()
+ {
+ // fixme
+ }
+
+}
diff --git a/gemini-firenio/src/main/java/com/techempower/gemini/firenio/mustache/FirenioMustacheManager.java b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/mustache/FirenioMustacheManager.java
new file mode 100644
index 00000000..a4d8c79e
--- /dev/null
+++ b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/mustache/FirenioMustacheManager.java
@@ -0,0 +1,99 @@
+/*******************************************************************************
+ * Copyright (c) 2018, 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.firenio.mustache;
+
+import java.io.*;
+
+import com.github.mustachejava.*;
+import com.techempower.gemini.*;
+import com.techempower.gemini.configuration.*;
+import com.techempower.gemini.mustache.MustacheManager;
+import com.techempower.util.*;
+
+/**
+ * The Resin specific implementation of {@link MustacheManager},
+ * which compiles and renders Mustache templates
+ */
+public class FirenioMustacheManager
+ extends MustacheManager
+{
+ public FirenioMustacheManager(GeminiApplication app)
+ {
+ super(app);
+ }
+
+ @Override
+ public void configure(EnhancedProperties props)
+ {
+ super.configure(props);
+ final EnhancedProperties.Focus focus = props.focus("Mustache.");
+ this.mustacheDirectory = focus.get("Directory", "${Servlet.WebInf}/mustache/");
+ if (super.enabled)
+ {
+ validateMustacheDirectory();
+ setupTemplateCache();
+ }
+ }
+
+ /**
+ * Returns a mustache factory. In the development environment, this method
+ * returns a new factory on each invocation so that compiled templates are
+ * not cached. In production, this returns the same factory every time,
+ * which caches templates.
+ */
+ @Override
+ public MustacheFactory getMustacheFactory()
+ {
+ return (useTemplateCache && this.mustacheFactory != null
+ ? this.mustacheFactory
+ : new DefaultMustacheFactory(new File(this.mustacheDirectory)));
+ }
+
+ @Override
+ public void resetTemplateCache()
+ {
+ mustacheFactory = new DefaultMustacheFactory(new File(mustacheDirectory));
+ }
+
+ /**
+ * Confirm that a valid directory has been provided by the configuration.
+ */
+ protected void validateMustacheDirectory()
+ {
+ if (this.enabled)
+ {
+ // Confirm directory exists.
+ final File directory = new File(this.mustacheDirectory);
+ if (!directory.isDirectory())
+ {
+ throw new ConfigurationError("Mustache.Directory " + this.mustacheDirectory + " does not exist.");
+ }
+ }
+ }
+}
+
\ No newline at end of file
diff --git a/gemini-firenio/src/main/java/com/techempower/gemini/firenio/path/AnnotationDispatcher.java b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/path/AnnotationDispatcher.java
new file mode 100644
index 00000000..4a16f16a
--- /dev/null
+++ b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/path/AnnotationDispatcher.java
@@ -0,0 +1,640 @@
+/*******************************************************************************
+ * 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.firenio.path;
+
+import com.esotericsoftware.reflectasm.MethodAccess;
+import com.firenio.codec.http11.HttpMethod;
+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.firenio.FirenioContext;
+import com.techempower.gemini.firenio.HttpRequest;
+import com.techempower.gemini.path.PathSegments;
+import com.techempower.gemini.path.RequestReferences;
+import com.techempower.gemini.path.annotation.Path;
+import com.techempower.gemini.prehandler.Prehandler;
+import com.techempower.helper.NetworkHelper;
+import com.techempower.helper.ReflectionHelper;
+import com.techempower.helper.StringHelper;
+import org.reflections.Reflections;
+import org.reflections.ReflectionsException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+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.firenio.HttpRequest.*;
+import static com.techempower.gemini.firenio.HttpRequest.HEADER_ACCESS_CONTROL_EXPOSED_HEADERS;
+
+public class AnnotationDispatcher implements Dispatcher {
+
+ //
+ // Member variables.
+ //
+ private final GeminiApplication app;
+ private final Map handlers;
+ private final Map testHandlers;
+ private final ExceptionHandler[] exceptionHandlers;
+ private final Prehandler[] prehandlers;
+ private final DispatchListener[] listeners;
+
+ private final Logger log = LoggerFactory.getLogger(getClass());
+ private ExecutorService preinitializationTasks = Executors.newSingleThreadExecutor();
+ private Reflections reflections = null;
+
+ public AnnotationDispatcher(GeminiApplication application)
+ {
+ app = application;
+ handlers = new HashMap<>();
+ testHandlers = new HashMap<>();
+ exceptionHandlers = new ExceptionHandler[]{};
+ prehandlers = new Prehandler[]{};
+ listeners = new DispatchListener[]{};
+
+ // fixme
+// 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)
+ {
+ log.error("Exception while instantiating Reflections component.", exc);
+ }
+ }
+ });
+ }
+
+ public void initialize() {
+ // Wait for pre-initialization tasks to complete.
+ try
+ {
+ log.info("Completing preinitialization tasks.");
+ preinitializationTasks.shutdown();
+ log.info("Awaiting termination of preinitialization tasks.");
+ preinitializationTasks.awaitTermination(5L, TimeUnit.MINUTES);
+ log.info("Preinitialization tasks complete.");
+ log.info("Reflections component: " + reflections);
+ }
+ catch (InterruptedException iexc)
+ {
+ log.error("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.info("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
+// }
+
+ final Method[] methods = clazz.getMethods();
+ for (Method method : methods) {
+ // Set up references to methods annotated as Paths.
+ final Path path = method.getAnnotation(Path.class);
+ if (path != null) {
+ final String url = annotation.value().replaceAll("/", "").equals("")
+ ? "/" + path.value()
+ : "/" + annotation.value() + "/" + path.value();
+ final MethodAccess methodAccess = MethodAccess.get(clazz);
+ try {
+ testHandlers.put(url, new PathUriMethod(clazz, method, methodAccess));
+ } catch (NoSuchMethodException e) {
+ log.error("This error should be impossible", e);
+ } catch (IllegalAccessException e) {
+ log.error("Handler methods must be public", e);
+ } catch (InvocationTargetException | InstantiationException e) {
+ log.error("Handler constructor must be default and public", e);
+ }
+ }
+ }
+ }
+ }
+ });
+
+ try
+ {
+ service.shutdown();
+ service.awaitTermination(1L, TimeUnit.MINUTES);
+ }
+ catch (InterruptedException iexc)
+ {
+ log.error("Unable to register all annotated handlers in 1 minute!");
+ }
+
+ log.info("Done registering annotated items.");
+ }
+ catch (ReflectionsException e)
+ {
+ throw new RuntimeException("Warn: problem registering class with reflection", 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) {
+ 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(context.getRequestUri());
+
+ // 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);
+
+ // fixme
+// if(((HttpRequest)context.getRequest()).getRequestMethod().toString() == HttpMethod.OPTIONS.getValue())
+ if(((HttpRequest)context.getRequest()).getRequestMethod().toString() == "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));
+ PathUriMethod handler = testHandlers.get(context.getRequestUri());
+
+ // 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 (((HttpRequest)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, context);
+
+
+ final Object value;
+ if (handler.paramCount == 0) {
+ value = handler.methodAccess.invoke(handler.handler, handler.index,
+ ReflectionHelper.NO_VALUES);
+ } else {
+ // fixme
+ value = handler.methodAccess.invoke(handler.handler, handler.index, context);
+ }
+ // fixme
+ try {
+ context.getRequest().print(value.toString());
+ return value != null;
+ } catch (IOException ioe) {
+ return false;
+ }
+
+ }
+ 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)
+ {
+ 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.warn("dispatchException called with a null reference.");
+ 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.error("Exception encountered while processing earlier " + exception, 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);
+ }
+
+ // fixme
+// if(((HttpRequest)context.getRequest()).getRequestMethod().toString() == HttpMethod.OPTIONS.getValue())
+ if(((HttpRequest)context.getRequest()).getRequestMethod().toString() == "OPTIONS")
+ {
+ context.headers().put(HEADER_ACCESS_CONTROL_MAX_AGE,
+ app.getSecurity().getSettings().getAccessControlMaxAge() + "");
+ }
+ }
+
+ protected static class PathUriMethod
+ {
+ public final Object handler;
+ public final Method method;
+ public final MethodAccess methodAccess;
+ public final int index;
+ public final int paramCount;
+
+ public PathUriMethod(Class> clazz, Method method, MethodAccess methodAccess) throws
+ NoSuchMethodException,
+ IllegalAccessException,
+ InvocationTargetException,
+ InstantiationException {
+ this.handler = clazz.getDeclaredConstructor().newInstance();
+ this.method = method;
+ this.methodAccess = methodAccess;
+
+ final Class>[] classes =
+ new Class[method.getGenericParameterTypes().length];
+ this.paramCount = classes.length;
+
+ for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
+ if (Context.class.isAssignableFrom((Class>)method.getGenericParameterTypes()[paramIndex])) {
+ classes[paramIndex] = method.getParameterTypes()[paramIndex];
+ }
+ }
+
+ if (paramCount == 0) {
+ this.index = methodAccess.getIndex(method.getName(),
+ ReflectionHelper.NO_PARAMETERS);
+ } else {
+ this.index = methodAccess.getIndex(method.getName(), classes);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("PathUriMethod[method:{%s},methodAccess:{%s},index:{%d}]", method.toString(), methodAccess.toString(), index);
+ }
+ }
+}
diff --git a/gemini-firenio/src/main/java/com/techempower/gemini/firenio/path/AnnotationHandler.java b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/path/AnnotationHandler.java
new file mode 100644
index 00000000..630e2a06
--- /dev/null
+++ b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/path/AnnotationHandler.java
@@ -0,0 +1,961 @@
+package com.techempower.gemini.firenio.path;
+
+
+import com.esotericsoftware.reflectasm.MethodAccess;
+import com.firenio.codec.http11.HttpMethod;
+import com.techempower.gemini.Context;
+import com.techempower.gemini.firenio.HttpRequest;
+import com.techempower.gemini.Request;
+import com.techempower.gemini.path.BasicPathHandler;
+import com.techempower.gemini.path.PathSegments;
+import com.techempower.gemini.path.RequestBodyAdapter;
+import com.techempower.gemini.path.RequestBodyException;
+import com.techempower.gemini.path.annotation.*;
+import com.techempower.helper.NumberHelper;
+import com.techempower.helper.ReflectionHelper;
+import com.techempower.helper.StringHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+
+import static com.techempower.gemini.firenio.HttpRequest.HEADER_ACCESS_CONTROL_REQUEST_METHOD;
+
+/**
+ * Similar to MethodUriHandler, AnnotationHandler class does the same
+ * strategy of creating `PathUriTree`s for each HttpRequest.Method type
+ * and then inserting handler methods into the trees.
+ * @param
+ */
+class AnnotationHandler {
+ final String rootUri;
+ final Object handler;
+
+ private final Logger log = LoggerFactory.getLogger(getClass());
+ 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,
+ 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, HttpMethod.GET);
+ }
+ // fixme
+// else if (put != null)
+// {
+// psm = analyzeAnnotatedMethod(path, method, HttpMethod.PUT);
+// }
+ else if (post != null)
+ {
+ psm = analyzeAnnotatedMethod(path, method, HttpMethod.POST);
+ }
+ // fixme
+// else if (delete != null)
+// {
+// psm = analyzeAnnotatedMethod(path, method, HttpMethod.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, HttpMethod.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 (((HttpRequest)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);
+ }
+
+ /**
+ * 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) {
+ 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)
+ {
+ Object value = methodAccess.invoke(handler, method.index,
+ ReflectionHelper.NO_VALUES);
+ // fixme
+ try {
+ context.getRequest().print(value.toString());
+ return value != null;
+ } catch (IOException ioe) {
+ return false;
+ }
+ }
+ 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
+ {
+ // fixme
+ Object value = methodAccess.invoke(handler, method.index,
+ getVariableArguments(segments, method, context));
+ context.getRequest().print(value.toString());
+ return value != null;
+ }
+ catch (RequestBodyException | IOException e)
+ {
+ log.error("Got RequestBodyException.", e);
+ // todo
+// 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 ++;
+ }
+ }
+
+ // Injection stuff
+ if (argsIndex < args.length) {
+ // Handle adapting and injecting the request body if configured.
+ if (method.bodyParameter != null)
+ {
+ args[argsIndex] = method.bodyParameter.readBody(context);
+ }
+ else if (Context.class.isAssignableFrom((Class>)method.method.getGenericParameterTypes()[argsIndex]))
+ {
+ args[argsIndex] = 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;
+ }
+ }
+ }
+
+ /**
+ * Represents a parameter that is populated from the request body.
+ * This must be the last parameter to the handler method, and is
+ * created when we detect a {@link Body} annotation (or a custom
+ * annotation that has {@link Body}).
+ *
+ * The {@link #adapter} is an instance of the adapter class
+ * that was specified by the body annotation {@link Body#value()},
+ * created by invoking the class's empty constructor.
+ *
+ * The {@link #type} is the generic parameter type of the last
+ * parameter to the method.
+ */
+ protected static class RequestBodyParameter {
+ public final RequestBodyAdapter> adapter;
+ public final Type type;
+
+ private RequestBodyParameter(Class extends RequestBodyAdapter>> adapterClass,
+ Type type) {
+ try {
+ this.adapter = adapterClass.getDeclaredConstructor().newInstance();
+ this.type = type;
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Unable to construct request body adapter of type "
+ + adapterClass.getName());
+ }
+ }
+
+ /**
+ * Adapt the request body in the specified context using the adapter and type
+ * on this instance.
+ *
+ * @see RequestBodyAdapter#read(Context, Type)
+ */
+ @SuppressWarnings({ "unchecked" })
+ Object readBody(C context) throws RequestBodyException
+ {
+ return ((RequestBodyAdapter) adapter).read(context, type);
+ }
+ }
+
+ /**
+ * A base class representing a single handler method that provides logic
+ * for dealing with the {@link Body} annotation. While subclasses of
+ * {@link BasicPathHandler} make use of their own routing-related
+ * annotations, the {@link Body} annotation should be supported by all
+ * handler implementations. The {@link #bodyParameter} member variable is
+ * non-null if a body annotation was detected on the method. Due to the
+ * flexibility that subclasses have with method parameters and invocation,
+ * it is up to the subclass to verify that the method signature itself
+ * matches what is expected based on the data available. Additionally,
+ * when routing a request to a method, the subclass is responsible for
+ * using the {@link #bodyParameter} to adapt the raw request body in
+ * order to pass to the handler method.
+ */
+ protected static abstract class BasicPathHandlerMethod
+ {
+ private static final Set SUPPORTED_BODY_METHODS = EnumSet.of(
+ Request.HttpMethod.POST, Request.HttpMethod.PUT, Request.HttpMethod.PATCH);
+
+ public final Method method;
+ public final Request.HttpMethod httpMethod;
+ public final RequestBodyParameter bodyParameter;
+
+ BasicPathHandlerMethod(Method method, Request.HttpMethod httpMethod)
+ {
+ this.method = method;
+ this.httpMethod = httpMethod;
+
+ Body body = method.getAnnotation(Body.class);
+ // We allow users to create their own annotations (that must be annotated
+ // with @Body), so scan the method's annotations.
+ if (body == null)
+ {
+ for (Annotation annotation : method.getAnnotations())
+ {
+ body = annotation.annotationType().getAnnotation(Body.class);
+ if (body != null)
+ {
+ break;
+ }
+ }
+ }
+ if (body != null)
+ {
+ if (!SUPPORTED_BODY_METHODS.contains(httpMethod))
+ {
+ throw new IllegalArgumentException("The " + httpMethod.name()
+ + " HTTP method does not support request bodies, but there is "
+ + "a @Body annotation present. See " + getClass().getName() + "#"
+ + method.getName());
+ }
+
+ // A body parameter may be generic, for example Map,
+ // so use the generic parameter type for the body parameter, which
+ // will return a ParameterizedType if necessary.
+ final Type[] genericParameterTypes = method.getGenericParameterTypes();
+
+ if (genericParameterTypes.length == 0)
+ {
+ throw new IllegalArgumentException("Methods annotated with @Body must "
+ + "accept at least 1 parameter, where the last parameter is "
+ + "for the body. See " + getClass().getName() + "#"
+ + method.getName());
+ }
+
+ this.bodyParameter = new RequestBodyParameter(body.value(),
+ genericParameterTypes[genericParameterTypes.length - 1]);
+ }
+ else
+ {
+ this.bodyParameter = null;
+ }
+ }
+ }
+
+ /**
+ * Details of an annotated path segment method.
+ */
+ protected static class PathUriMethod extends 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, HttpMethod httpMethod,
+ MethodAccess methodAccess)
+ {
+ super(method, Request.HttpMethod.valueOf(httpMethod.getValue()));
+
+ 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 ++;
+ }
+ }
+
+ //
+ if (variableCount < classes.length &&
+ Context.class.isAssignableFrom((Class>)method.getGenericParameterTypes()[variableCount]))
+ {
+ classes[variableCount] = method.getParameterTypes()[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 + "}";
+ }
+ }
+ }
+}
diff --git a/gemini-firenio/src/main/java/com/techempower/gemini/firenio/session/HttpSessionManager.java b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/session/HttpSessionManager.java
new file mode 100644
index 00000000..a1e655a0
--- /dev/null
+++ b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/session/HttpSessionManager.java
@@ -0,0 +1,140 @@
+/*******************************************************************************
+ * 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.firenio.session;
+
+import com.techempower.gemini.*;
+import com.techempower.gemini.session.Session;
+import com.techempower.gemini.session.SessionManager;
+import com.techempower.util.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages the creation of user session objects. Initializes new sessions
+ * to the proper timeout, etc.
+ *
+ * The Context class uses SessionManager to create sessions. This allows
+ * for any necessary initialization to happen on all new sessions.
+ *
+ * Reads the following configuration options from the .conf file:
+ *
+ * - SessionTimeout - Timeout for sessions in seconds. Default: 3600.
+ *
- StrictSessions - Attempts to prevent session hijacking by hashing
+ * request headers provided at the start of each session with those
+ * received on each subsequent request; resetting the session in the event
+ * of a mismatch.
+ *
- RefererTracking - Captures the HTTP "referer" (sic) request header
+ * provided when a session is new.
+ *
+ */
+public class HttpSessionManager
+ implements SessionManager
+{
+ //
+ // Constants.
+ //
+
+ public static final int DEFAULT_TIMEOUT = 3600; // One hour
+ public static final String SESSION_HASH = "Gemini-Session-Hash";
+
+ //
+ // Member variables.
+ //
+
+ private int timeoutSeconds = DEFAULT_TIMEOUT;
+ private Logger log = LoggerFactory.getLogger(getClass());
+ private boolean refererTracking = false;
+ private long sessionAccumulator = 0L;
+ private boolean strictSessions = false;
+
+ //
+ // Member methods.
+ //
+
+ /**
+ * Constructor.
+ */
+ public HttpSessionManager(GeminiApplication application)
+ {
+ application.getConfigurator().addConfigurable(this);
+ }
+
+ /**
+ * Configure this component.
+ */
+ @Override
+ public void configure(EnhancedProperties props)
+ {
+ setTimeoutSeconds(props.getInt("SessionTimeout", DEFAULT_TIMEOUT));
+ log.info("Session timeout: {} seconds.", getTimeoutSeconds());
+
+ refererTracking = props.getBoolean("RefererTracking", refererTracking);
+ if (refererTracking)
+ {
+ log.info("Referer tracking enabled.");
+ }
+ strictSessions = props.getBoolean("StrictSessions", strictSessions);
+ if (strictSessions)
+ {
+ log.info("Scrict sessions enabled.");
+ }
+ }
+
+ /**
+ * Sets the session timeout in minutes. Note: only future sessions will be
+ * affected.
+ */
+ public void setTimeoutMinutes(int minutes)
+ {
+ timeoutSeconds = minutes * 60;
+ }
+
+ /**
+ * Sets the session timeout in seconds. Note: only future sessions will be
+ * affected.
+ */
+ public void setTimeoutSeconds(int seconds)
+ {
+ timeoutSeconds = seconds;
+ }
+
+ /**
+ * Gets the session timeout in seconds.
+ */
+ @Override
+ public int getTimeoutSeconds()
+ {
+ return timeoutSeconds;
+ }
+
+ @Override
+ public Session getSession(Request request, boolean create)
+ {
+ // fixme
+ return null;
+ }
+}
diff --git a/gemini-hikaricp/pom.xml b/gemini-hikaricp/pom.xml
index 99c24a2d..6b541e15 100755
--- a/gemini-hikaricp/pom.xml
+++ b/gemini-hikaricp/pom.xml
@@ -21,7 +21,7 @@
gemini-parent
com.techempower
- 3.2.0-SNAPSHOT
+ 4.0.1-SNAPSHOT
gemini-hikaricp
diff --git a/gemini-jdbc/pom.xml b/gemini-jdbc/pom.xml
index e6bd612b..33b3e0c1 100755
--- a/gemini-jdbc/pom.xml
+++ b/gemini-jdbc/pom.xml
@@ -21,7 +21,7 @@
gemini-parent
com.techempower
- 3.2.0-SNAPSHOT
+ 4.0.1-SNAPSHOT
gemini-jdbc
diff --git a/gemini-jndi/pom.xml b/gemini-jndi/pom.xml
index 77a141b4..ec011526 100755
--- a/gemini-jndi/pom.xml
+++ b/gemini-jndi/pom.xml
@@ -21,7 +21,7 @@
gemini-parent
com.techempower
- 3.2.0-SNAPSHOT
+ 4.0.1-SNAPSHOT
gemini-jndi
diff --git a/gemini-log4j12/pom.xml b/gemini-log4j12/pom.xml
index e1b083f9..e01b4cd1 100644
--- a/gemini-log4j12/pom.xml
+++ b/gemini-log4j12/pom.xml
@@ -20,7 +20,7 @@
gemini-parent
com.techempower
- 3.2.0-SNAPSHOT
+ 4.0.1-SNAPSHOT
com.techempower
diff --git a/gemini-log4j2/pom.xml b/gemini-log4j2/pom.xml
index 34922ed0..4123a5e9 100644
--- a/gemini-log4j2/pom.xml
+++ b/gemini-log4j2/pom.xml
@@ -20,7 +20,7 @@
gemini-parent
com.techempower
- 3.2.0-SNAPSHOT
+ 4.0.1-SNAPSHOT
com.techempower
diff --git a/gemini-logback/pom.xml b/gemini-logback/pom.xml
index a38016d8..c685f290 100644
--- a/gemini-logback/pom.xml
+++ b/gemini-logback/pom.xml
@@ -20,7 +20,7 @@
gemini-parent
com.techempower
- 3.2.0-SNAPSHOT
+ 4.0.1-SNAPSHOT
com.techempower
gemini-logback
diff --git a/gemini-resin-archetype/pom.xml b/gemini-resin-archetype/pom.xml
index 37bdfcbd..0578ec1b 100755
--- a/gemini-resin-archetype/pom.xml
+++ b/gemini-resin-archetype/pom.xml
@@ -17,7 +17,7 @@
com.techempower
gemini-parent
- 3.2.0-SNAPSHOT
+ 4.0.1-SNAPSHOT
gemini-resin-archetype
diff --git a/gemini-resin/pom.xml b/gemini-resin/pom.xml
index a1f2b117..d7b80c23 100755
--- a/gemini-resin/pom.xml
+++ b/gemini-resin/pom.xml
@@ -19,7 +19,7 @@
gemini-parent
com.techempower
- 3.2.0-SNAPSHOT
+ 4.0.1-SNAPSHOT
gemini-resin
diff --git a/gemini/pom.xml b/gemini/pom.xml
index e3fba08f..ea1807e0 100755
--- a/gemini/pom.xml
+++ b/gemini/pom.xml
@@ -19,7 +19,7 @@
gemini-parent
com.techempower
- 3.2.0-SNAPSHOT
+ 4.0.1-SNAPSHOT
gemini
diff --git a/gemini/src/main/java/com/techempower/gemini/BasicInfrastructure.java b/gemini/src/main/java/com/techempower/gemini/BasicInfrastructure.java
index 410e2ad2..20c74068 100755
--- a/gemini/src/main/java/com/techempower/gemini/BasicInfrastructure.java
+++ b/gemini/src/main/java/com/techempower/gemini/BasicInfrastructure.java
@@ -235,7 +235,7 @@ public void configure(EnhancedProperties props)
this.log.warn("JSPDirectory should end with a trailing slash.");
this.jspFileDirectory = this.jspFileDirectory + '/';
}
- if (!this.jspPhysicalDirectory.endsWith(File.separator))
+ if (!this.jspPhysicalDirectory.endsWith("/"))
{
this.log.warn("JSPPhysicalDirectory should end with a trailing slash or backslash.");
this.jspPhysicalDirectory = this.jspPhysicalDirectory + File.separator;
diff --git a/gemini/src/main/java/com/techempower/gemini/Dispatcher.java b/gemini/src/main/java/com/techempower/gemini/Dispatcher.java
index 484a671e..ba46e5a5 100755
--- a/gemini/src/main/java/com/techempower/gemini/Dispatcher.java
+++ b/gemini/src/main/java/com/techempower/gemini/Dispatcher.java
@@ -38,6 +38,13 @@
*/
public interface Dispatcher
{
+ /**
+ * Returns all the registered routes associated with this dispatcher.
+ * By default, returns an empty String[].
+ */
+ default String[] getRoutes() {
+ return new String[]{};
+ }
/**
* Dispatch a request (represented as a Context) and ensure that it is
diff --git a/gemini/src/main/java/com/techempower/gemini/mustache/MustacheManager.java b/gemini/src/main/java/com/techempower/gemini/mustache/MustacheManager.java
index aec5b2e3..17cb641f 100755
--- a/gemini/src/main/java/com/techempower/gemini/mustache/MustacheManager.java
+++ b/gemini/src/main/java/com/techempower/gemini/mustache/MustacheManager.java
@@ -105,7 +105,7 @@ protected TemplateAppReferences constructApplicationReferences()
public void configure(EnhancedProperties props)
{
final EnhancedProperties.Focus focus = props.focus("Mustache.");
- this.enabled = focus.getBoolean("Enabled", true);
+ this.enabled = focus.getBoolean("Enabled", false);
this.useTemplateCache = focus.getBoolean("TemplateCacheEnabled", !application.getVersion().isDevelopment());
log.info("Mustache {}using template cache.",
this.useTemplateCache ? "" : "not ");
diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/Path.java b/gemini/src/main/java/com/techempower/gemini/path/annotation/Path.java
index 06bb8896..9bfdfcda 100755
--- a/gemini/src/main/java/com/techempower/gemini/path/annotation/Path.java
+++ b/gemini/src/main/java/com/techempower/gemini/path/annotation/Path.java
@@ -54,7 +54,7 @@
* the root URI of the handler. Example /api/users => UserHandler; {@code @Path}
* will handle `GET /api/users`.
*/
-@Target(ElementType.METHOD)
+@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Path
{
diff --git a/pom.xml b/pom.xml
index cb238a37..940122e1 100755
--- a/pom.xml
+++ b/pom.xml
@@ -22,7 +22,7 @@
https://github.com/TechEmpower/gemini
com.techempower
gemini-parent
- 3.2.0-SNAPSHOT
+ 4.0.1-SNAPSHOT
msmith
@@ -48,6 +48,7 @@
gemini-log4j2
gemini-logback
gemini-log4j12
+ gemini-firenio
@@ -137,6 +138,11 @@
gemini-hikaricp
${project.version}
+
+ ${project.groupId}
+ gemini-firenio
+ ${project.version}
+
com.fasterxml.jackson.core
jackson-core