From 213171d7585b7cb0140218d771cc58db0950ea21 Mon Sep 17 00:00:00 2001 From: Mike Smith Date: Fri, 24 Apr 2020 09:07:27 -0700 Subject: [PATCH 1/4] WIP: Dispatcher+Firenio works --- gemini-firenio/pom.xml | 51 ++ .../techempower/gemini/FirenioContext.java | 17 + .../gemini/FirenioGeminiApplication.java | 136 +++ .../com/techempower/gemini/HttpRequest.java | 342 +++++++ .../lifecycle/InitAnnotationDispatcher.java | 21 + .../gemini/monitor/FirenioMonitor.java | 113 +++ .../mustache/FirenioMustacheManager.java | 98 ++ .../gemini/path/AnnotationDispatcher.java | 550 ++++++++++++ .../gemini/path/AnnotationHandler.java | 834 ++++++++++++++++++ .../gemini/session/HttpSessionManager.java | 138 +++ gemini-hikaricp/pom.xml | 2 +- gemini-jdbc/pom.xml | 2 +- gemini-jndi/pom.xml | 2 +- gemini-log4j12/pom.xml | 2 +- gemini-log4j2/pom.xml | 2 +- gemini-logback/pom.xml | 2 +- gemini-resin-archetype/pom.xml | 2 +- gemini-resin/pom.xml | 2 +- gemini/pom.xml | 2 +- .../gemini/mustache/MustacheManager.java | 2 +- .../gemini/path/annotation/Path.java | 2 +- pom.xml | 8 +- 22 files changed, 2318 insertions(+), 12 deletions(-) create mode 100644 gemini-firenio/pom.xml create mode 100644 gemini-firenio/src/main/java/com/techempower/gemini/FirenioContext.java create mode 100644 gemini-firenio/src/main/java/com/techempower/gemini/FirenioGeminiApplication.java create mode 100644 gemini-firenio/src/main/java/com/techempower/gemini/HttpRequest.java create mode 100644 gemini-firenio/src/main/java/com/techempower/gemini/lifecycle/InitAnnotationDispatcher.java create mode 100644 gemini-firenio/src/main/java/com/techempower/gemini/monitor/FirenioMonitor.java create mode 100644 gemini-firenio/src/main/java/com/techempower/gemini/mustache/FirenioMustacheManager.java create mode 100644 gemini-firenio/src/main/java/com/techempower/gemini/path/AnnotationDispatcher.java create mode 100644 gemini-firenio/src/main/java/com/techempower/gemini/path/AnnotationHandler.java create mode 100644 gemini-firenio/src/main/java/com/techempower/gemini/session/HttpSessionManager.java diff --git a/gemini-firenio/pom.xml b/gemini-firenio/pom.xml new file mode 100644 index 00000000..38420d96 --- /dev/null +++ b/gemini-firenio/pom.xml @@ -0,0 +1,51 @@ + + + + 4.0.0 + jar + + + TechEmpower, Inc. + https://www.techempower.com/ + + + + + Revised BSD License, 3-clause + repo + + + + + gemini-parent + com.techempower + 4.0.0-SNAPSHOT + + + gemini-firenio + gemini-firenio + + An extension for Firenio-specific functionality with the Gemini web framework. + + + + + com.firenio + firenio-all + 1.3.3 + + + org.reflections + reflections + + + org.javassist + javassist + + + com.techempower + gemini + + + + diff --git a/gemini-firenio/src/main/java/com/techempower/gemini/FirenioContext.java b/gemini-firenio/src/main/java/com/techempower/gemini/FirenioContext.java new file mode 100644 index 00000000..bb347a8f --- /dev/null +++ b/gemini-firenio/src/main/java/com/techempower/gemini/FirenioContext.java @@ -0,0 +1,17 @@ +package com.techempower.gemini; + +import com.techempower.gemini.context.Attachments; + +public class FirenioContext extends Context { + + public FirenioContext(Request request, GeminiApplication application) + { + super(application, request); + } + + @Override + public Attachments files() { + // FIXME + return null; + } +} diff --git a/gemini-firenio/src/main/java/com/techempower/gemini/FirenioGeminiApplication.java b/gemini-firenio/src/main/java/com/techempower/gemini/FirenioGeminiApplication.java new file mode 100644 index 00000000..e436187c --- /dev/null +++ b/gemini-firenio/src/main/java/com/techempower/gemini/FirenioGeminiApplication.java @@ -0,0 +1,136 @@ +package com.techempower.gemini; + +import com.firenio.codec.http11.HttpCodec; +import com.firenio.codec.http11.HttpConnection; +import com.firenio.codec.http11.HttpContentType; +import com.firenio.codec.http11.HttpFrame; +import com.firenio.component.*; +import com.techempower.data.ConnectionMonitor; +import com.techempower.data.ConnectorFactory; +import com.techempower.data.DatabaseAffinity; +import com.techempower.gemini.lifecycle.InitAnnotationDispatcher; +import com.techempower.gemini.monitor.FirenioMonitor; +import com.techempower.gemini.mustache.FirenioMustacheManager; +import com.techempower.gemini.mustache.MustacheManager; +import com.techempower.gemini.path.AnnotationDispatcher; +import com.techempower.gemini.session.HttpSessionManager; +import com.techempower.gemini.session.SessionManager; +import com.techempower.util.EnhancedProperties; + +import java.sql.SQLException; + +public abstract class FirenioGeminiApplication + extends GeminiApplication { + + /** + * Overload: Constructs an instance of a subclass of Context, provided the + * parameters used to construct Context objects. Note that it is NO + * LONGER necessary to overload this method if your application is not using + * a special subclass of Context. + */ + @Override + public Context getContext(Request request) + { + return new FirenioContext(request, this); + } + + @Override + protected ConnectorFactory constructConnectorFactory() { + return new ConnectorFactory() { + @Override + public ConnectionMonitor getConnectionMonitor() throws SQLException { + return null; + } + + @Override + public void determineIdentifierQuoteString() { + + } + + @Override + public String getIdentifierQuoteString() { + return null; + } + + @Override + public DatabaseAffinity getDatabaseAffinity() { + return null; + } + + @Override + public boolean isEnabled() { + return false; + } + + @Override + public void configure(EnhancedProperties props) { + + } + }; + } + + @Override + protected Dispatcher constructDispatcher() { + return new AnnotationDispatcher<>(this); + } + + @Override + protected MustacheManager constructMustacheManager() { + return new FirenioMustacheManager(this); + } + + @Override + protected SessionManager constructSessionManager() { + return new HttpSessionManager(this); + } + + @Override + protected FirenioMonitor constructMonitor() { + return new FirenioMonitor(this); + } + + /** + * Starts the FirenioGeminiApplication + * todo + * + * @throws Exception + */ + public final void start() throws Exception { + final FirenioGeminiApplication thiss = this; + this.getLifecycle().addInitializationTask(new InitAnnotationDispatcher()); + // Initialize the application. + this.initialize(null); + + int slept = 0; + int maxSleep = 10_000; + while (!this.isRunning()) { + // Wait until the application is initialized and configured. + slept += 1000; + Thread.sleep(1000); + } + + // TODO: this is the dispatcher + IoEventHandle eventHandleAdaptor = new IoEventHandle() { + + @Override + public void accept(Channel channel, Frame frame) throws Exception { + final HttpFrame httpFrame = (HttpFrame) frame; + final HttpRequest request = new HttpRequest(channel, httpFrame, thiss); + final FirenioContext context = new FirenioContext(request, thiss); + getDispatcher().dispatch(context); +// HttpFrame f = (HttpFrame) frame; +// f.setContent("Hello, World!".getBytes()); + // fixme - content type shouldn't be set here + httpFrame.setContentType(HttpContentType.text_plain); + httpFrame.setConnection(HttpConnection.NONE); + channel.writeAndFlush(httpFrame); + channel.release(httpFrame); + } + }; + ChannelAcceptor context = new ChannelAcceptor(8300); + context.addChannelEventListener(new LoggerChannelOpenListener()); + context.setIoEventHandle(eventHandleAdaptor); + context.addProtocolCodec(new HttpCodec()); + context.bind(); + } +} diff --git a/gemini-firenio/src/main/java/com/techempower/gemini/HttpRequest.java b/gemini-firenio/src/main/java/com/techempower/gemini/HttpRequest.java new file mode 100644 index 00000000..ef300b3f --- /dev/null +++ b/gemini-firenio/src/main/java/com/techempower/gemini/HttpRequest.java @@ -0,0 +1,342 @@ +package com.techempower.gemini; + +import com.firenio.codec.http11.HttpFrame; +import com.firenio.codec.http11.HttpHeader; +import com.firenio.codec.http11.HttpStatus; +import com.firenio.component.Channel; +import com.techempower.gemini.session.Session; +import com.techempower.util.UtilityConstants; + +import java.io.*; +import java.util.Enumeration; +import java.util.Vector; + +public class HttpRequest implements Request { + + // + // Member variables. + // + + private final GeminiApplication application; + private final Channel channel; + private final HttpFrame frame; + + public HttpRequest(Channel channel, HttpFrame frame, GeminiApplication application) + { + this.application = application; + this.channel = channel; + this.frame = frame; + } + + @Override + public void setCharacterEncoding(String encoding) throws UnsupportedEncodingException { + // fixme - this throws an error in Firenio if the service is already + // running. Not sure if this is intended; need to investigate. +// channel.getContext().setCharset(Charset.forName(encoding)); + } + + @Override + public String getRequestCharacterEncoding() { + return channel.getCharset().displayName(); + } + + @Override + public Enumeration getHeaderNames() { + final Vector headerNames = new Vector<>(); + frame.getRequestHeaders().scan(); + while(frame.getRequestHeaders().hasNext()) { + HttpHeader header = HttpHeader.get(frame.getRequestHeaders().key()); + headerNames.add(header.name()); + } + return (Enumeration) headerNames; + } + + @Override + public String getHeader(String name) { + try { + return frame.getRequestHeader(HttpHeader.valueOf(name)); + } catch (IllegalArgumentException iae) { + // fixme - this is thrown when the header isn't there + return null; + } + } + + @Override + public Enumeration getParameterNames() { + final Vector paramNames = new Vector<>(); + for (String param : frame.getRequestParams().values()) { + paramNames.add(param); + } + return (Enumeration) paramNames; + } + + @Override + public String getParameter(String name) { + return frame.getRequestParam(name); + } + + @Override + public void putParameter(String name, String value) { + frame.getRequestParams().put(name, value); + } + + @Override + public void removeParameter(String name) { + frame.getRequestParams().remove(name); + } + + @Override + public void removeAllRequestValues() { + frame.getRequestParams().clear(); + } + + @Override + public String[] getParameterValues(String name) { + // FIXME - not sure how HttpFrame handles multiple param values. + return null; + } + + @Override + @Deprecated + /** + * @deprecated + */ + public String encodeURL(String url) { + return null; + } + + @Override + public void print(String text) throws IOException { + // fixme + frame.setContent(text.getBytes()); + } + + @Override + @Deprecated + /** + * @deprecated + */ + public PrintWriter getWriter() throws IOException { + return null; + } + + @Override + @Deprecated + /** + * @deprecated + */ + public String getRequestSignature() { + return null; + } + + @Override + @Deprecated + /** + * @deprecated + */ + public String getRealPath(String path) { + return null; + } + + @Override + public StringBuffer getRequestURL() { + // fixme + return new StringBuffer(frame.getRequestURL()); + } + + @Override + public String getRequestURI() { + return frame.getRequestURL(); + } + + @Override + public 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 + */ + public OutputStream getOutputStream() throws IOException { + return null; + } + + @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/lifecycle/InitAnnotationDispatcher.java b/gemini-firenio/src/main/java/com/techempower/gemini/lifecycle/InitAnnotationDispatcher.java new file mode 100644 index 00000000..ef1292f6 --- /dev/null +++ b/gemini-firenio/src/main/java/com/techempower/gemini/lifecycle/InitAnnotationDispatcher.java @@ -0,0 +1,21 @@ +package com.techempower.gemini.lifecycle; + +import com.techempower.gemini.Dispatcher; +import com.techempower.gemini.GeminiApplication; +import com.techempower.gemini.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/monitor/FirenioMonitor.java b/gemini-firenio/src/main/java/com/techempower/gemini/monitor/FirenioMonitor.java new file mode 100644 index 00000000..72623cd1 --- /dev/null +++ b/gemini-firenio/src/main/java/com/techempower/gemini/monitor/FirenioMonitor.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * 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.monitor; + +import com.techempower.gemini.*; +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: + *

    + *
  1. Performance monitoring, the main component, observes the execution + * of requests to trend the performance of each type of request over + * time.
  2. + *
  3. 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.
  4. + *
  5. 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.
  6. + *
  7. Web session monitoring, an optional component, that counts and + * optionally maintains a set of active web sessions.
  8. + *
+ * 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/mustache/FirenioMustacheManager.java b/gemini-firenio/src/main/java/com/techempower/gemini/mustache/FirenioMustacheManager.java new file mode 100644 index 00000000..f7c28e96 --- /dev/null +++ b/gemini-firenio/src/main/java/com/techempower/gemini/mustache/FirenioMustacheManager.java @@ -0,0 +1,98 @@ +/******************************************************************************* + * 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.mustache; + +import java.io.*; + +import com.github.mustachejava.*; +import com.techempower.gemini.*; +import com.techempower.gemini.configuration.*; +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/path/AnnotationDispatcher.java b/gemini-firenio/src/main/java/com/techempower/gemini/path/AnnotationDispatcher.java new file mode 100644 index 00000000..14e8f118 --- /dev/null +++ b/gemini-firenio/src/main/java/com/techempower/gemini/path/AnnotationDispatcher.java @@ -0,0 +1,550 @@ +/******************************************************************************* + * 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.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.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.HttpRequest.*; +import static com.techempower.gemini.HttpRequest.HEADER_ACCESS_CONTROL_EXPOSED_HEADERS; + +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[]{}; + + // 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) + { + // 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); + } + } + + /** + * 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)); + + // 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); + } + 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.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); + } + + // 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() + ""); + } + } +} diff --git a/gemini-firenio/src/main/java/com/techempower/gemini/path/AnnotationHandler.java b/gemini-firenio/src/main/java/com/techempower/gemini/path/AnnotationHandler.java new file mode 100644 index 00000000..041995b8 --- /dev/null +++ b/gemini-firenio/src/main/java/com/techempower/gemini/path/AnnotationHandler.java @@ -0,0 +1,834 @@ +package com.techempower.gemini.path; + + +import com.esotericsoftware.reflectasm.MethodAccess; +import com.firenio.codec.http11.HttpMethod; +import com.techempower.gemini.Context; +import com.techempower.gemini.HttpRequest; +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.io.IOException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import static com.techempower.gemini.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 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) + { + // 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 ++; + } + } + + // 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; + } + } + } + + /** + * 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, 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/session/HttpSessionManager.java b/gemini-firenio/src/main/java/com/techempower/gemini/session/HttpSessionManager.java new file mode 100644 index 00000000..fc75a96d --- /dev/null +++ b/gemini-firenio/src/main/java/com/techempower/gemini/session/HttpSessionManager.java @@ -0,0 +1,138 @@ +/******************************************************************************* + * 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.session; + +import com.techempower.gemini.*; +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 0a1989e2..db58af30 100755 --- a/gemini-hikaricp/pom.xml +++ b/gemini-hikaricp/pom.xml @@ -21,7 +21,7 @@ gemini-parent com.techempower - 3.1.0-SNAPSHOT + 4.0.0-SNAPSHOT gemini-hikaricp diff --git a/gemini-jdbc/pom.xml b/gemini-jdbc/pom.xml index 007a9b83..48592102 100755 --- a/gemini-jdbc/pom.xml +++ b/gemini-jdbc/pom.xml @@ -21,7 +21,7 @@ gemini-parent com.techempower - 3.1.0-SNAPSHOT + 4.0.0-SNAPSHOT gemini-jdbc diff --git a/gemini-jndi/pom.xml b/gemini-jndi/pom.xml index 0fdb6111..bc358dec 100755 --- a/gemini-jndi/pom.xml +++ b/gemini-jndi/pom.xml @@ -21,7 +21,7 @@ gemini-parent com.techempower - 3.1.0-SNAPSHOT + 4.0.0-SNAPSHOT gemini-jndi diff --git a/gemini-log4j12/pom.xml b/gemini-log4j12/pom.xml index 700cd580..a26e426e 100644 --- a/gemini-log4j12/pom.xml +++ b/gemini-log4j12/pom.xml @@ -20,7 +20,7 @@ gemini-parent com.techempower - 3.1.0-SNAPSHOT + 4.0.0-SNAPSHOT com.techempower diff --git a/gemini-log4j2/pom.xml b/gemini-log4j2/pom.xml index f65bac1a..b031a4d8 100644 --- a/gemini-log4j2/pom.xml +++ b/gemini-log4j2/pom.xml @@ -20,7 +20,7 @@ gemini-parent com.techempower - 3.1.0-SNAPSHOT + 4.0.0-SNAPSHOT com.techempower diff --git a/gemini-logback/pom.xml b/gemini-logback/pom.xml index 5775b1ae..2d4dca92 100644 --- a/gemini-logback/pom.xml +++ b/gemini-logback/pom.xml @@ -20,7 +20,7 @@ gemini-parent com.techempower - 3.1.0-SNAPSHOT + 4.0.0-SNAPSHOT com.techempower gemini-logback diff --git a/gemini-resin-archetype/pom.xml b/gemini-resin-archetype/pom.xml index 5c8e2758..069eb7ae 100755 --- a/gemini-resin-archetype/pom.xml +++ b/gemini-resin-archetype/pom.xml @@ -17,7 +17,7 @@ com.techempower gemini-parent - 3.1.0-SNAPSHOT + 4.0.0-SNAPSHOT gemini-resin-archetype diff --git a/gemini-resin/pom.xml b/gemini-resin/pom.xml index 12e8c7d3..2ee3e873 100755 --- a/gemini-resin/pom.xml +++ b/gemini-resin/pom.xml @@ -19,7 +19,7 @@ gemini-parent com.techempower - 3.1.0-SNAPSHOT + 4.0.0-SNAPSHOT gemini-resin diff --git a/gemini/pom.xml b/gemini/pom.xml index 983043b7..2d62a3b8 100755 --- a/gemini/pom.xml +++ b/gemini/pom.xml @@ -19,7 +19,7 @@ gemini-parent com.techempower - 3.1.0-SNAPSHOT + 4.0.0-SNAPSHOT gemini 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 454d05d0..81a3fc26 100755 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ https://github.com/TechEmpower/gemini com.techempower gemini-parent - 3.1.0-SNAPSHOT + 4.0.0-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 From 888f84a628a871caca7f888cfb0bf701ed56fca5 Mon Sep 17 00:00:00 2001 From: Mike Smith Date: Fri, 24 Apr 2020 09:51:38 -0700 Subject: [PATCH 2/4] Re-add logging --- .../gemini/FirenioGeminiApplication.java | 16 +++++++--- .../gemini/path/AnnotationDispatcher.java | 31 +++++++++---------- .../gemini/path/AnnotationHandler.java | 5 ++- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/gemini-firenio/src/main/java/com/techempower/gemini/FirenioGeminiApplication.java b/gemini-firenio/src/main/java/com/techempower/gemini/FirenioGeminiApplication.java index e436187c..3e4cb719 100644 --- a/gemini-firenio/src/main/java/com/techempower/gemini/FirenioGeminiApplication.java +++ b/gemini-firenio/src/main/java/com/techempower/gemini/FirenioGeminiApplication.java @@ -16,12 +16,16 @@ import com.techempower.gemini.session.HttpSessionManager; import com.techempower.gemini.session.SessionManager; import com.techempower.util.EnhancedProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.sql.SQLException; public abstract class FirenioGeminiApplication extends GeminiApplication { + protected Logger log = LoggerFactory.getLogger(getClass()); + /** * Overload: Constructs an instance of a subclass of Context, provided the * parameters used to construct Context objects. Note that it is NO @@ -103,14 +107,18 @@ public final void start() throws Exception { int slept = 0; int maxSleep = 10_000; - while (!this.isRunning()) { + while (slept < maxSleep && !this.isRunning()) { // Wait until the application is initialized and configured. slept += 1000; Thread.sleep(1000); } - // TODO: this is the dispatcher - IoEventHandle eventHandleAdaptor = new IoEventHandle() { + if (slept > maxSleep && !this.isRunning()) { + log.error("Failed to start Gemini application after {} seconds", slept); + return; + } + + final IoEventHandle eventHandleAdaptor = new IoEventHandle() { @Override public void accept(Channel channel, Frame frame) throws Exception { @@ -118,8 +126,6 @@ public void accept(Channel channel, Frame frame) throws Exception { final HttpRequest request = new HttpRequest(channel, httpFrame, thiss); final FirenioContext context = new FirenioContext(request, thiss); getDispatcher().dispatch(context); -// HttpFrame f = (HttpFrame) frame; -// f.setContent("Hello, World!".getBytes()); // fixme - content type shouldn't be set here httpFrame.setContentType(HttpContentType.text_plain); httpFrame.setConnection(HttpConnection.NONE); diff --git a/gemini-firenio/src/main/java/com/techempower/gemini/path/AnnotationDispatcher.java b/gemini-firenio/src/main/java/com/techempower/gemini/path/AnnotationDispatcher.java index 14e8f118..c523b81e 100644 --- a/gemini-firenio/src/main/java/com/techempower/gemini/path/AnnotationDispatcher.java +++ b/gemini-firenio/src/main/java/com/techempower/gemini/path/AnnotationDispatcher.java @@ -37,6 +37,8 @@ import com.techempower.helper.StringHelper; import org.reflections.Reflections; import org.reflections.ReflectionsException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; @@ -53,13 +55,13 @@ public class AnnotationDispatcher implements Dispatche // // Member variables. // - private final GeminiApplication app; private final Map handlers; 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; @@ -93,8 +95,7 @@ public void run() { } catch (Exception exc) { - // todo -// log.log("Exception while instantiating Reflections component.", exc); + log.error("Exception while instantiating Reflections component.", exc); } } }); @@ -104,16 +105,16 @@ public void initialize() { // Wait for pre-initialization tasks to complete. try { -// log.log("Completing preinitialization tasks."); + log.info("Completing preinitialization tasks."); preinitializationTasks.shutdown(); -// log.log("Awaiting termination of preinitialization tasks."); + log.info("Awaiting termination of preinitialization tasks."); preinitializationTasks.awaitTermination(5L, TimeUnit.MINUTES); -// log.log("Preinitialization tasks complete."); -// log.log("Reflections component: " + reflections); + log.info("Preinitialization tasks complete."); + log.info("Reflections component: " + reflections); } catch (InterruptedException iexc) { -// log.log("Preinitialization interrupted.", iexc); + log.error("Preinitialization interrupted.", iexc); } // Throw an exception if Reflections is not ready. @@ -126,7 +127,7 @@ public void initialize() { } private void register() { -// log.log("Registering annotated entities, relations, and type adapters."); + log.info("Registering annotated entities, relations, and type adapters."); try { final ExecutorService service = Executors.newFixedThreadPool(1); @@ -155,14 +156,14 @@ public void run() { try { service.shutdown(); - service.awaitTermination(1L, TimeUnit.HOURS); + service.awaitTermination(1L, TimeUnit.MINUTES); } catch (InterruptedException iexc) { -// log.log("Unable to register all entities in 1 hour!", LogLevel.CRITICAL); + log.error("Unable to register all annotated handlers in 1 minute!"); } -// log.log("Done registering annotated items."); + log.info("Done registering annotated items."); } catch (ReflectionsException e) { @@ -384,8 +385,7 @@ public void renderComplete(Context context) { public void dispatchException(Context context, Throwable exception, String description) { if (exception == null) { -// log.log("dispatchException called with a null reference.", -// LogLevel.ALERT); + log.warn("dispatchException called with a null reference."); return; } @@ -409,8 +409,7 @@ public void dispatchException(Context context, Throwable exception, String descr // 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); + log.error("Exception encountered while processing earlier " + exception, exc); } } diff --git a/gemini-firenio/src/main/java/com/techempower/gemini/path/AnnotationHandler.java b/gemini-firenio/src/main/java/com/techempower/gemini/path/AnnotationHandler.java index 041995b8..02fba01e 100644 --- a/gemini-firenio/src/main/java/com/techempower/gemini/path/AnnotationHandler.java +++ b/gemini-firenio/src/main/java/com/techempower/gemini/path/AnnotationHandler.java @@ -10,6 +10,8 @@ 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.reflect.Method; @@ -29,6 +31,7 @@ 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; @@ -294,8 +297,8 @@ protected boolean dispatchToAnnotatedMethod(PathSegments segments, } catch (RequestBodyException | IOException e) { + log.error("Got RequestBodyException.", e); // todo -// log().log("Got RequestBodyException.", LogLevel.DEBUG, e); // return this.error(e.getStatusCode(), e.getMessage()); } } From c87dd1fdee5b9f42617c77c0c381cdb97d29915f Mon Sep 17 00:00:00 2001 From: Mike Smith Date: Fri, 24 Apr 2020 10:22:50 -0700 Subject: [PATCH 3/4] Use Firenio configuration example --- gemini-firenio/pom.xml | 2 +- .../gemini/FirenioGeminiApplication.java | 64 ++++++++++++++++++- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/gemini-firenio/pom.xml b/gemini-firenio/pom.xml index 38420d96..995f6362 100644 --- a/gemini-firenio/pom.xml +++ b/gemini-firenio/pom.xml @@ -32,7 +32,7 @@ com.firenio firenio-all - 1.3.3 + 1.3.2 org.reflections diff --git a/gemini-firenio/src/main/java/com/techempower/gemini/FirenioGeminiApplication.java b/gemini-firenio/src/main/java/com/techempower/gemini/FirenioGeminiApplication.java index 3e4cb719..a906cbb2 100644 --- a/gemini-firenio/src/main/java/com/techempower/gemini/FirenioGeminiApplication.java +++ b/gemini-firenio/src/main/java/com/techempower/gemini/FirenioGeminiApplication.java @@ -1,10 +1,13 @@ package com.techempower.gemini; +import com.firenio.Options; import com.firenio.codec.http11.HttpCodec; import com.firenio.codec.http11.HttpConnection; import com.firenio.codec.http11.HttpContentType; import com.firenio.codec.http11.HttpFrame; +import com.firenio.common.Util; import com.firenio.component.*; +import com.firenio.log.DebugUtil; import com.techempower.data.ConnectionMonitor; import com.techempower.data.ConnectorFactory; import com.techempower.data.DatabaseAffinity; @@ -100,6 +103,37 @@ protected FirenioMonitor constructMonitor() { * @throws Exception */ public final void start() throws Exception { + boolean lite = Util.getBooleanProperty("lite"); + boolean read = Util.getBooleanProperty("read"); + boolean pool = Util.getBooleanProperty("pool"); + boolean epoll = Util.getBooleanProperty("epoll"); + boolean direct = Util.getBooleanProperty("direct"); + boolean inline = Util.getBooleanProperty("inline"); + boolean nodelay = Util.getBooleanProperty("nodelay"); + boolean cachedurl = Util.getBooleanProperty("cachedurl"); + boolean unsafeBuf = Util.getBooleanProperty("unsafeBuf"); + int core = Util.getIntProperty("core", 1); + int frame = Util.getIntProperty("frame", 16); + int level = Util.getIntProperty("level", 1); + int readBuf = Util.getIntProperty("readBuf", 16); + Options.setBufAutoExpansion(false); + Options.setChannelReadFirst(read); + Options.setEnableEpoll(epoll); + Options.setEnableUnsafeBuf(unsafeBuf); + DebugUtil.info("lite: {}", lite); + DebugUtil.info("read: {}", read); + DebugUtil.info("pool: {}", pool); + DebugUtil.info("core: {}", core); + DebugUtil.info("epoll: {}", epoll); + DebugUtil.info("frame: {}", frame); + DebugUtil.info("level: {}", level); + DebugUtil.info("direct: {}", direct); + DebugUtil.info("inline: {}", inline); + DebugUtil.info("readBuf: {}", readBuf); + DebugUtil.info("nodelay: {}", nodelay); + DebugUtil.info("cachedurl: {}", cachedurl); + DebugUtil.info("unsafeBuf: {}", unsafeBuf); + final FirenioGeminiApplication thiss = this; this.getLifecycle().addInitializationTask(new InitAnnotationDispatcher()); // Initialize the application. @@ -133,10 +167,36 @@ public void accept(Channel channel, Frame frame) throws Exception { channel.release(httpFrame); } }; - ChannelAcceptor context = new ChannelAcceptor(8300); + + int pool_cap = 1024 * 128; + int pool_unit = 256; + if (inline) { + pool_cap = 1024 * 8; + pool_unit = 256 * 16; + } + NioEventLoopGroup group = new NioEventLoopGroup(); + ChannelAcceptor context = new ChannelAcceptor(group, 8080); + group.setMemoryPoolCapacity(pool_cap); + group.setEnableMemoryPoolDirect(direct); + group.setEnableMemoryPool(pool); + group.setMemoryPoolUnit(pool_unit); + group.setWriteBuffers(32); + group.setChannelReadBuffer(1024 * readBuf); + group.setEventLoopSize(Util.availableProcessors() * core); + group.setConcurrentFrameStack(false); + if (nodelay) { + context.addChannelEventListener(new ChannelEventListenerAdapter() { + + @Override + public void channelOpened(Channel ch) throws Exception { + ch.setOption(SocketOptions.TCP_NODELAY, 1); + ch.setOption(SocketOptions.SO_KEEPALIVE, 0); + } + }); + } context.addChannelEventListener(new LoggerChannelOpenListener()); context.setIoEventHandle(eventHandleAdaptor); context.addProtocolCodec(new HttpCodec()); - context.bind(); + context.bind(1024 * 8); } } From 930de63a01aedcbfc75f78e2f43ef718fdd00469 Mon Sep 17 00:00:00 2001 From: Mike Smith Date: Thu, 30 Apr 2020 09:20:59 -0700 Subject: [PATCH 4/4] WIP Gemini-Firenio testing --- gemini-firenio/pom.xml | 2 +- .../gemini/{ => firenio}/FirenioContext.java | 37 +- .../FirenioGeminiApplication.java | 413 ++-- .../gemini/{ => firenio}/HttpRequest.java | 688 +++---- .../lifecycle/InitAnnotationDispatcher.java | 43 +- .../{ => firenio}/monitor/FirenioMonitor.java | 227 +-- .../mustache/FirenioMustacheManager.java | 195 +- .../path/AnnotationDispatcher.java | 1189 ++++++----- .../{ => firenio}/path/AnnotationHandler.java | 1798 +++++++++-------- .../session/HttpSessionManager.java | 278 +-- gemini-hikaricp/pom.xml | 2 +- gemini-jdbc/pom.xml | 2 +- gemini-jndi/pom.xml | 2 +- gemini-log4j12/pom.xml | 2 +- gemini-log4j2/pom.xml | 2 +- gemini-logback/pom.xml | 2 +- gemini-resin-archetype/pom.xml | 2 +- gemini-resin/pom.xml | 2 +- gemini/pom.xml | 2 +- pom.xml | 2 +- 20 files changed, 2563 insertions(+), 2327 deletions(-) rename gemini-firenio/src/main/java/com/techempower/gemini/{ => firenio}/FirenioContext.java (65%) rename gemini-firenio/src/main/java/com/techempower/gemini/{ => firenio}/FirenioGeminiApplication.java (88%) rename gemini-firenio/src/main/java/com/techempower/gemini/{ => firenio}/HttpRequest.java (94%) rename gemini-firenio/src/main/java/com/techempower/gemini/{ => firenio}/lifecycle/InitAnnotationDispatcher.java (76%) rename gemini-firenio/src/main/java/com/techempower/gemini/{ => firenio}/monitor/FirenioMonitor.java (97%) rename gemini-firenio/src/main/java/com/techempower/gemini/{ => firenio}/mustache/FirenioMustacheManager.java (96%) rename gemini-firenio/src/main/java/com/techempower/gemini/{ => firenio}/path/AnnotationDispatcher.java (77%) rename gemini-firenio/src/main/java/com/techempower/gemini/{ => firenio}/path/AnnotationHandler.java (85%) rename gemini-firenio/src/main/java/com/techempower/gemini/{ => firenio}/session/HttpSessionManager.java (96%) diff --git a/gemini-firenio/pom.xml b/gemini-firenio/pom.xml index 995f6362..fdfad867 100644 --- a/gemini-firenio/pom.xml +++ b/gemini-firenio/pom.xml @@ -19,7 +19,7 @@ gemini-parent com.techempower - 4.0.0-SNAPSHOT + 4.0.1-SNAPSHOT gemini-firenio diff --git a/gemini-firenio/src/main/java/com/techempower/gemini/FirenioContext.java b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/FirenioContext.java similarity index 65% rename from gemini-firenio/src/main/java/com/techempower/gemini/FirenioContext.java rename to gemini-firenio/src/main/java/com/techempower/gemini/firenio/FirenioContext.java index bb347a8f..7a05bb29 100644 --- a/gemini-firenio/src/main/java/com/techempower/gemini/FirenioContext.java +++ b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/FirenioContext.java @@ -1,17 +1,20 @@ -package com.techempower.gemini; - -import com.techempower.gemini.context.Attachments; - -public class FirenioContext extends Context { - - public FirenioContext(Request request, GeminiApplication application) - { - super(application, request); - } - - @Override - public Attachments files() { - // FIXME - return null; - } -} +package com.techempower.gemini.firenio; + +import com.techempower.gemini.Context; +import com.techempower.gemini.GeminiApplication; +import com.techempower.gemini.Request; +import com.techempower.gemini.context.Attachments; + +public class FirenioContext extends Context { + + public FirenioContext(Request request, GeminiApplication application) + { + super(application, request); + } + + @Override + public Attachments files() { + // FIXME + return null; + } +} diff --git a/gemini-firenio/src/main/java/com/techempower/gemini/FirenioGeminiApplication.java b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/FirenioGeminiApplication.java similarity index 88% rename from gemini-firenio/src/main/java/com/techempower/gemini/FirenioGeminiApplication.java rename to gemini-firenio/src/main/java/com/techempower/gemini/firenio/FirenioGeminiApplication.java index a906cbb2..ab65232f 100644 --- a/gemini-firenio/src/main/java/com/techempower/gemini/FirenioGeminiApplication.java +++ b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/FirenioGeminiApplication.java @@ -1,202 +1,211 @@ -package com.techempower.gemini; - -import com.firenio.Options; -import com.firenio.codec.http11.HttpCodec; -import com.firenio.codec.http11.HttpConnection; -import com.firenio.codec.http11.HttpContentType; -import com.firenio.codec.http11.HttpFrame; -import com.firenio.common.Util; -import com.firenio.component.*; -import com.firenio.log.DebugUtil; -import com.techempower.data.ConnectionMonitor; -import com.techempower.data.ConnectorFactory; -import com.techempower.data.DatabaseAffinity; -import com.techempower.gemini.lifecycle.InitAnnotationDispatcher; -import com.techempower.gemini.monitor.FirenioMonitor; -import com.techempower.gemini.mustache.FirenioMustacheManager; -import com.techempower.gemini.mustache.MustacheManager; -import com.techempower.gemini.path.AnnotationDispatcher; -import com.techempower.gemini.session.HttpSessionManager; -import com.techempower.gemini.session.SessionManager; -import com.techempower.util.EnhancedProperties; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.sql.SQLException; - -public abstract class FirenioGeminiApplication - extends GeminiApplication { - - protected Logger log = LoggerFactory.getLogger(getClass()); - - /** - * Overload: Constructs an instance of a subclass of Context, provided the - * parameters used to construct Context objects. Note that it is NO - * LONGER necessary to overload this method if your application is not using - * a special subclass of Context. - */ - @Override - public Context getContext(Request request) - { - return new FirenioContext(request, this); - } - - @Override - protected ConnectorFactory constructConnectorFactory() { - return new ConnectorFactory() { - @Override - public ConnectionMonitor getConnectionMonitor() throws SQLException { - return null; - } - - @Override - public void determineIdentifierQuoteString() { - - } - - @Override - public String getIdentifierQuoteString() { - return null; - } - - @Override - public DatabaseAffinity getDatabaseAffinity() { - return null; - } - - @Override - public boolean isEnabled() { - return false; - } - - @Override - public void configure(EnhancedProperties props) { - - } - }; - } - - @Override - protected Dispatcher constructDispatcher() { - return new AnnotationDispatcher<>(this); - } - - @Override - protected MustacheManager constructMustacheManager() { - return new FirenioMustacheManager(this); - } - - @Override - protected SessionManager constructSessionManager() { - return new HttpSessionManager(this); - } - - @Override - protected FirenioMonitor constructMonitor() { - return new FirenioMonitor(this); - } - - /** - * Starts the FirenioGeminiApplication - * todo - * - * @throws Exception - */ - public final void start() throws Exception { - boolean lite = Util.getBooleanProperty("lite"); - boolean read = Util.getBooleanProperty("read"); - boolean pool = Util.getBooleanProperty("pool"); - boolean epoll = Util.getBooleanProperty("epoll"); - boolean direct = Util.getBooleanProperty("direct"); - boolean inline = Util.getBooleanProperty("inline"); - boolean nodelay = Util.getBooleanProperty("nodelay"); - boolean cachedurl = Util.getBooleanProperty("cachedurl"); - boolean unsafeBuf = Util.getBooleanProperty("unsafeBuf"); - int core = Util.getIntProperty("core", 1); - int frame = Util.getIntProperty("frame", 16); - int level = Util.getIntProperty("level", 1); - int readBuf = Util.getIntProperty("readBuf", 16); - Options.setBufAutoExpansion(false); - Options.setChannelReadFirst(read); - Options.setEnableEpoll(epoll); - Options.setEnableUnsafeBuf(unsafeBuf); - DebugUtil.info("lite: {}", lite); - DebugUtil.info("read: {}", read); - DebugUtil.info("pool: {}", pool); - DebugUtil.info("core: {}", core); - DebugUtil.info("epoll: {}", epoll); - DebugUtil.info("frame: {}", frame); - DebugUtil.info("level: {}", level); - DebugUtil.info("direct: {}", direct); - DebugUtil.info("inline: {}", inline); - DebugUtil.info("readBuf: {}", readBuf); - DebugUtil.info("nodelay: {}", nodelay); - DebugUtil.info("cachedurl: {}", cachedurl); - DebugUtil.info("unsafeBuf: {}", unsafeBuf); - - final FirenioGeminiApplication thiss = this; - this.getLifecycle().addInitializationTask(new InitAnnotationDispatcher()); - // Initialize the application. - this.initialize(null); - - int slept = 0; - int maxSleep = 10_000; - while (slept < maxSleep && !this.isRunning()) { - // Wait until the application is initialized and configured. - slept += 1000; - Thread.sleep(1000); - } - - if (slept > maxSleep && !this.isRunning()) { - log.error("Failed to start Gemini application after {} seconds", slept); - return; - } - - final IoEventHandle eventHandleAdaptor = new IoEventHandle() { - - @Override - public void accept(Channel channel, Frame frame) throws Exception { - final HttpFrame httpFrame = (HttpFrame) frame; - final HttpRequest request = new HttpRequest(channel, httpFrame, thiss); - final FirenioContext context = new FirenioContext(request, thiss); - getDispatcher().dispatch(context); - // fixme - content type shouldn't be set here - httpFrame.setContentType(HttpContentType.text_plain); - httpFrame.setConnection(HttpConnection.NONE); - channel.writeAndFlush(httpFrame); - channel.release(httpFrame); - } - }; - - int pool_cap = 1024 * 128; - int pool_unit = 256; - if (inline) { - pool_cap = 1024 * 8; - pool_unit = 256 * 16; - } - NioEventLoopGroup group = new NioEventLoopGroup(); - ChannelAcceptor context = new ChannelAcceptor(group, 8080); - group.setMemoryPoolCapacity(pool_cap); - group.setEnableMemoryPoolDirect(direct); - group.setEnableMemoryPool(pool); - group.setMemoryPoolUnit(pool_unit); - group.setWriteBuffers(32); - group.setChannelReadBuffer(1024 * readBuf); - group.setEventLoopSize(Util.availableProcessors() * core); - group.setConcurrentFrameStack(false); - if (nodelay) { - context.addChannelEventListener(new ChannelEventListenerAdapter() { - - @Override - public void channelOpened(Channel ch) throws Exception { - ch.setOption(SocketOptions.TCP_NODELAY, 1); - ch.setOption(SocketOptions.SO_KEEPALIVE, 0); - } - }); - } - context.addChannelEventListener(new LoggerChannelOpenListener()); - context.setIoEventHandle(eventHandleAdaptor); - context.addProtocolCodec(new HttpCodec()); - context.bind(1024 * 8); - } -} +package com.techempower.gemini.firenio; + +import com.firenio.Options; +import com.firenio.codec.http11.HttpCodec; +import com.firenio.codec.http11.HttpConnection; +import com.firenio.codec.http11.HttpContentType; +import com.firenio.codec.http11.HttpFrame; +import com.firenio.common.Util; +import com.firenio.component.*; +import com.firenio.log.DebugUtil; +import com.techempower.data.ConnectionMonitor; +import com.techempower.data.ConnectorFactory; +import com.techempower.data.DatabaseAffinity; +import com.techempower.gemini.Context; +import com.techempower.gemini.Dispatcher; +import com.techempower.gemini.GeminiApplication; +import com.techempower.gemini.Request; +import com.techempower.gemini.firenio.lifecycle.InitAnnotationDispatcher; +import com.techempower.gemini.firenio.monitor.FirenioMonitor; +import com.techempower.gemini.firenio.mustache.FirenioMustacheManager; +import com.techempower.gemini.mustache.MustacheManager; +import com.techempower.gemini.firenio.path.AnnotationDispatcher; +import com.techempower.gemini.firenio.session.HttpSessionManager; +import com.techempower.gemini.session.SessionManager; +import com.techempower.util.EnhancedProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.SQLException; + +public abstract class FirenioGeminiApplication + extends GeminiApplication { + + protected Logger log = LoggerFactory.getLogger(getClass()); + + /** + * Overload: Constructs an instance of a subclass of Context, provided the + * parameters used to construct Context objects. Note that it is NO + * LONGER necessary to overload this method if your application is not using + * a special subclass of Context. + */ + @Override + public Context getContext(Request request) + { + return new FirenioContext(request, this); + } + + @Override + protected ConnectorFactory constructConnectorFactory() { + return new ConnectorFactory() { + @Override + public ConnectionMonitor getConnectionMonitor() throws SQLException { + return null; + } + + @Override + public void determineIdentifierQuoteString() { + + } + + @Override + public String getIdentifierQuoteString() { + return null; + } + + @Override + public DatabaseAffinity getDatabaseAffinity() { + return null; + } + + @Override + public boolean isEnabled() { + return false; + } + + @Override + public void configure(EnhancedProperties props) { + + } + }; + } + + @Override + protected Dispatcher constructDispatcher() { + return new AnnotationDispatcher<>(this); + } + + @Override + protected MustacheManager constructMustacheManager() { + return new FirenioMustacheManager(this); + } + + @Override + protected SessionManager constructSessionManager() { + return new HttpSessionManager(this); + } + + @Override + protected FirenioMonitor constructMonitor() { + return new FirenioMonitor(this); + } + + public final void start() throws Exception { + start("Gemini", 8080, 1024 * 8); + } + + /** + * Starts the FirenioGeminiApplication + * todo + * + * @throws Exception + */ + public final void start(String serverName, int port, int backlog) throws Exception { + boolean lite = Util.getBooleanProperty("lite"); + boolean read = Util.getBooleanProperty("read"); + boolean pool = Util.getBooleanProperty("pool"); + boolean epoll = Util.getBooleanProperty("epoll"); + boolean direct = Util.getBooleanProperty("direct"); + boolean inline = Util.getBooleanProperty("inline"); + boolean nodelay = Util.getBooleanProperty("nodelay"); + boolean cachedurl = Util.getBooleanProperty("cachedurl"); + boolean unsafeBuf = Util.getBooleanProperty("unsafeBuf"); + int core = Util.getIntProperty("core", 1); + int frame = Util.getIntProperty("frame", 16); + int level = Util.getIntProperty("level", 1); + int readBuf = Util.getIntProperty("readBuf", 16); + Options.setBufAutoExpansion(false); + Options.setChannelReadFirst(read); + Options.setEnableEpoll(epoll); + Options.setEnableUnsafeBuf(unsafeBuf); + DebugUtil.info("lite: {}", lite); + DebugUtil.info("read: {}", read); + DebugUtil.info("pool: {}", pool); + DebugUtil.info("core: {}", core); + DebugUtil.info("epoll: {}", epoll); + DebugUtil.info("frame: {}", frame); + DebugUtil.info("level: {}", level); + DebugUtil.info("direct: {}", direct); + DebugUtil.info("inline: {}", inline); + DebugUtil.info("readBuf: {}", readBuf); + DebugUtil.info("nodelay: {}", nodelay); + DebugUtil.info("cachedurl: {}", cachedurl); + DebugUtil.info("unsafeBuf: {}", unsafeBuf); + + final FirenioGeminiApplication thiss = this; + this.getLifecycle().addInitializationTask(new InitAnnotationDispatcher()); + // Initialize the application. + this.initialize(null); + + int slept = 0; + int maxSleep = 10_000; + while (slept < maxSleep && !this.isRunning()) { + // Wait until the application is initialized and configured. + slept += 1000; + Thread.sleep(1000); + } + + if (slept > maxSleep && !this.isRunning()) { + log.error("Failed to start Gemini application after {} seconds", slept); + return; + } + + final IoEventHandle eventHandleAdaptor = new IoEventHandle() { + + @Override + public void accept(Channel channel, Frame frame) throws Exception { + final HttpFrame httpFrame = (HttpFrame) frame; + final HttpRequest request = new HttpRequest(channel, httpFrame, thiss); + final FirenioContext context = new FirenioContext(request, thiss); + getDispatcher().dispatch(context); + // fixme - content type shouldn't be set here + httpFrame.setContentType(HttpContentType.text_plain); + httpFrame.setConnection(HttpConnection.NONE); + channel.writeAndFlush(httpFrame); + channel.release(httpFrame); + } + }; + + int fcache = 1024 * 16; + int pool_cap = 1024 * 128; + int pool_unit = 256; + if (inline) { + pool_cap = 1024 * 8; + pool_unit = 256 * 16; + } + NioEventLoopGroup group = new NioEventLoopGroup(); + ChannelAcceptor context = new ChannelAcceptor(group, port); + group.setMemoryPoolCapacity(pool_cap); + group.setEnableMemoryPoolDirect(direct); + group.setEnableMemoryPool(pool); + group.setMemoryPoolUnit(pool_unit); + group.setWriteBuffers(32); + group.setChannelReadBuffer(1024 * readBuf); + group.setEventLoopSize(Util.availableProcessors() * core); + group.setConcurrentFrameStack(false); + if (nodelay) { + context.addChannelEventListener(new ChannelEventListenerAdapter() { + + @Override + public void channelOpened(Channel ch) throws Exception { + ch.setOption(SocketOptions.TCP_NODELAY, 1); + ch.setOption(SocketOptions.SO_KEEPALIVE, 0); + } + }); + } + context.addChannelEventListener(new LoggerChannelOpenListener()); + context.setIoEventHandle(eventHandleAdaptor); + context.addProtocolCodec(new HttpCodec(serverName, fcache, lite, inline)); + context.bind(backlog); + } +} diff --git a/gemini-firenio/src/main/java/com/techempower/gemini/HttpRequest.java b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/HttpRequest.java similarity index 94% rename from gemini-firenio/src/main/java/com/techempower/gemini/HttpRequest.java rename to gemini-firenio/src/main/java/com/techempower/gemini/firenio/HttpRequest.java index ef300b3f..7ecd99b5 100644 --- a/gemini-firenio/src/main/java/com/techempower/gemini/HttpRequest.java +++ b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/HttpRequest.java @@ -1,342 +1,346 @@ -package com.techempower.gemini; - -import com.firenio.codec.http11.HttpFrame; -import com.firenio.codec.http11.HttpHeader; -import com.firenio.codec.http11.HttpStatus; -import com.firenio.component.Channel; -import com.techempower.gemini.session.Session; -import com.techempower.util.UtilityConstants; - -import java.io.*; -import java.util.Enumeration; -import java.util.Vector; - -public class HttpRequest implements Request { - - // - // Member variables. - // - - private final GeminiApplication application; - private final Channel channel; - private final HttpFrame frame; - - public HttpRequest(Channel channel, HttpFrame frame, GeminiApplication application) - { - this.application = application; - this.channel = channel; - this.frame = frame; - } - - @Override - public void setCharacterEncoding(String encoding) throws UnsupportedEncodingException { - // fixme - this throws an error in Firenio if the service is already - // running. Not sure if this is intended; need to investigate. -// channel.getContext().setCharset(Charset.forName(encoding)); - } - - @Override - public String getRequestCharacterEncoding() { - return channel.getCharset().displayName(); - } - - @Override - public Enumeration getHeaderNames() { - final Vector headerNames = new Vector<>(); - frame.getRequestHeaders().scan(); - while(frame.getRequestHeaders().hasNext()) { - HttpHeader header = HttpHeader.get(frame.getRequestHeaders().key()); - headerNames.add(header.name()); - } - return (Enumeration) headerNames; - } - - @Override - public String getHeader(String name) { - try { - return frame.getRequestHeader(HttpHeader.valueOf(name)); - } catch (IllegalArgumentException iae) { - // fixme - this is thrown when the header isn't there - return null; - } - } - - @Override - public Enumeration getParameterNames() { - final Vector paramNames = new Vector<>(); - for (String param : frame.getRequestParams().values()) { - paramNames.add(param); - } - return (Enumeration) paramNames; - } - - @Override - public String getParameter(String name) { - return frame.getRequestParam(name); - } - - @Override - public void putParameter(String name, String value) { - frame.getRequestParams().put(name, value); - } - - @Override - public void removeParameter(String name) { - frame.getRequestParams().remove(name); - } - - @Override - public void removeAllRequestValues() { - frame.getRequestParams().clear(); - } - - @Override - public String[] getParameterValues(String name) { - // FIXME - not sure how HttpFrame handles multiple param values. - return null; - } - - @Override - @Deprecated - /** - * @deprecated - */ - public String encodeURL(String url) { - return null; - } - - @Override - public void print(String text) throws IOException { - // fixme - frame.setContent(text.getBytes()); - } - - @Override - @Deprecated - /** - * @deprecated - */ - public PrintWriter getWriter() throws IOException { - return null; - } - - @Override - @Deprecated - /** - * @deprecated - */ - public String getRequestSignature() { - return null; - } - - @Override - @Deprecated - /** - * @deprecated - */ - public String getRealPath(String path) { - return null; - } - - @Override - public StringBuffer getRequestURL() { - // fixme - return new StringBuffer(frame.getRequestURL()); - } - - @Override - public String getRequestURI() { - return frame.getRequestURL(); - } - - @Override - public 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 - */ - public OutputStream getOutputStream() throws IOException { - return null; - } - - @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)); - } -} +package com.techempower.gemini.firenio; + +import com.firenio.codec.http11.HttpFrame; +import com.firenio.codec.http11.HttpHeader; +import com.firenio.codec.http11.HttpStatus; +import com.firenio.component.Channel; +import com.techempower.gemini.Cookie; +import com.techempower.gemini.GeminiApplication; +import com.techempower.gemini.Infrastructure; +import com.techempower.gemini.Request; +import com.techempower.gemini.session.Session; +import com.techempower.util.UtilityConstants; + +import java.io.*; +import java.util.Enumeration; +import java.util.Vector; + +public class HttpRequest implements Request { + + // + // Member variables. + // + + private final GeminiApplication application; + private final Channel channel; + private final HttpFrame frame; + + public HttpRequest(Channel channel, HttpFrame frame, GeminiApplication application) + { + this.application = application; + this.channel = channel; + this.frame = frame; + } + + @Override + public void setCharacterEncoding(String encoding) throws UnsupportedEncodingException { + // fixme - this throws an error in Firenio if the service is already + // running. Not sure if this is intended; need to investigate. +// channel.getContext().setCharset(Charset.forName(encoding)); + } + + @Override + public String getRequestCharacterEncoding() { + return channel.getCharset().displayName(); + } + + @Override + public Enumeration getHeaderNames() { + final Vector headerNames = new Vector<>(); + frame.getRequestHeaders().scan(); + while(frame.getRequestHeaders().hasNext()) { + HttpHeader header = HttpHeader.get(frame.getRequestHeaders().key()); + headerNames.add(header.name()); + } + return (Enumeration) headerNames; + } + + @Override + public String getHeader(String name) { + try { + return frame.getRequestHeader(HttpHeader.valueOf(name)); + } catch (IllegalArgumentException iae) { + // fixme - this is thrown when the header isn't there + return null; + } + } + + @Override + public Enumeration getParameterNames() { + final Vector paramNames = new Vector<>(); + for (String param : frame.getRequestParams().values()) { + paramNames.add(param); + } + return (Enumeration) paramNames; + } + + @Override + public String getParameter(String name) { + return frame.getRequestParam(name); + } + + @Override + public void putParameter(String name, String value) { + frame.getRequestParams().put(name, value); + } + + @Override + public void removeParameter(String name) { + frame.getRequestParams().remove(name); + } + + @Override + public void removeAllRequestValues() { + frame.getRequestParams().clear(); + } + + @Override + public String[] getParameterValues(String name) { + // FIXME - not sure how HttpFrame handles multiple param values. + return null; + } + + @Override + @Deprecated + /** + * @deprecated + */ + public String encodeURL(String url) { + return null; + } + + @Override + public void print(String text) throws IOException { + // fixme + frame.setContent(text.getBytes()); + } + + @Override + @Deprecated + /** + * @deprecated + */ + public PrintWriter getWriter() throws IOException { + return null; + } + + @Override + @Deprecated + /** + * @deprecated + */ + public String getRequestSignature() { + return null; + } + + @Override + @Deprecated + /** + * @deprecated + */ + public String getRealPath(String path) { + return null; + } + + @Override + public StringBuffer getRequestURL() { + // fixme + return new StringBuffer(frame.getRequestURL()); + } + + @Override + public String getRequestURI() { + return frame.getRequestURL(); + } + + @Override + public 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 + */ + public OutputStream getOutputStream() throws IOException { + return null; + } + + @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/lifecycle/InitAnnotationDispatcher.java b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/lifecycle/InitAnnotationDispatcher.java similarity index 76% rename from gemini-firenio/src/main/java/com/techempower/gemini/lifecycle/InitAnnotationDispatcher.java rename to gemini-firenio/src/main/java/com/techempower/gemini/firenio/lifecycle/InitAnnotationDispatcher.java index ef1292f6..5229b411 100644 --- a/gemini-firenio/src/main/java/com/techempower/gemini/lifecycle/InitAnnotationDispatcher.java +++ b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/lifecycle/InitAnnotationDispatcher.java @@ -1,21 +1,22 @@ -package com.techempower.gemini.lifecycle; - -import com.techempower.gemini.Dispatcher; -import com.techempower.gemini.GeminiApplication; -import com.techempower.gemini.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(); - } - } -} +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/monitor/FirenioMonitor.java b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/monitor/FirenioMonitor.java similarity index 97% rename from gemini-firenio/src/main/java/com/techempower/gemini/monitor/FirenioMonitor.java rename to gemini-firenio/src/main/java/com/techempower/gemini/firenio/monitor/FirenioMonitor.java index 72623cd1..5751764c 100644 --- a/gemini-firenio/src/main/java/com/techempower/gemini/monitor/FirenioMonitor.java +++ b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/monitor/FirenioMonitor.java @@ -1,113 +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.monitor; - -import com.techempower.gemini.*; -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: - *

    - *
  1. Performance monitoring, the main component, observes the execution - * of requests to trend the performance of each type of request over - * time.
  2. - *
  3. 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.
  4. - *
  5. 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.
  6. - *
  7. Web session monitoring, an optional component, that counts and - * optionally maintains a set of active web sessions.
  8. - *
- * 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 - } - -} +/******************************************************************************* + * 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: + *

    + *
  1. Performance monitoring, the main component, observes the execution + * of requests to trend the performance of each type of request over + * time.
  2. + *
  3. 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.
  4. + *
  5. 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.
  6. + *
  7. Web session monitoring, an optional component, that counts and + * optionally maintains a set of active web sessions.
  8. + *
+ * 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/mustache/FirenioMustacheManager.java b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/mustache/FirenioMustacheManager.java similarity index 96% rename from gemini-firenio/src/main/java/com/techempower/gemini/mustache/FirenioMustacheManager.java rename to gemini-firenio/src/main/java/com/techempower/gemini/firenio/mustache/FirenioMustacheManager.java index f7c28e96..a4d8c79e 100644 --- a/gemini-firenio/src/main/java/com/techempower/gemini/mustache/FirenioMustacheManager.java +++ b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/mustache/FirenioMustacheManager.java @@ -1,98 +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.mustache; - -import java.io.*; - -import com.github.mustachejava.*; -import com.techempower.gemini.*; -import com.techempower.gemini.configuration.*; -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."); - } - } - } -} +/******************************************************************************* + * 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/path/AnnotationDispatcher.java b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/path/AnnotationDispatcher.java similarity index 77% rename from gemini-firenio/src/main/java/com/techempower/gemini/path/AnnotationDispatcher.java rename to gemini-firenio/src/main/java/com/techempower/gemini/firenio/path/AnnotationDispatcher.java index c523b81e..ddfffbd6 100644 --- a/gemini-firenio/src/main/java/com/techempower/gemini/path/AnnotationDispatcher.java +++ b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/path/AnnotationDispatcher.java @@ -1,549 +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.path; - -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.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 org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -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.HttpRequest.*; -import static com.techempower.gemini.HttpRequest.HEADER_ACCESS_CONTROL_EXPOSED_HEADERS; - -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 final Logger log = LoggerFactory.getLogger(getClass()); - 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[]{}; - - // 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 - } - } - } - }); - - 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)); - - // 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); - } - 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() + ""); - } - } -} +/******************************************************************************* + * 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() + "/" + path.value(); + log.info("url: {}", url); + final MethodAccess methodAccess = MethodAccess.get(clazz); + log.info("methodAccess: {}", methodAccess); + final PathUriMethod pathUriMethod; + try { + pathUriMethod = new PathUriMethod(clazz, method, methodAccess); + log.info("pathUriMethod: {}", pathUriMethod); + testHandlers.put(url, pathUriMethod); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (InstantiationException e) { + e.printStackTrace(); + } + } + } + } + } + }); + + 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/path/AnnotationHandler.java b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/path/AnnotationHandler.java similarity index 85% rename from gemini-firenio/src/main/java/com/techempower/gemini/path/AnnotationHandler.java rename to gemini-firenio/src/main/java/com/techempower/gemini/firenio/path/AnnotationHandler.java index 02fba01e..630e2a06 100644 --- a/gemini-firenio/src/main/java/com/techempower/gemini/path/AnnotationHandler.java +++ b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/path/AnnotationHandler.java @@ -1,837 +1,961 @@ -package com.techempower.gemini.path; - - -import com.esotericsoftware.reflectasm.MethodAccess; -import com.firenio.codec.http11.HttpMethod; -import com.techempower.gemini.Context; -import com.techempower.gemini.HttpRequest; -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 org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.List; - -import static com.techempower.gemini.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; - } - } - } - - /** - * 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, 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 + "}"; - } - } - } -} +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> 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/session/HttpSessionManager.java b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/session/HttpSessionManager.java similarity index 96% rename from gemini-firenio/src/main/java/com/techempower/gemini/session/HttpSessionManager.java rename to gemini-firenio/src/main/java/com/techempower/gemini/firenio/session/HttpSessionManager.java index fc75a96d..a1e655a0 100644 --- a/gemini-firenio/src/main/java/com/techempower/gemini/session/HttpSessionManager.java +++ b/gemini-firenio/src/main/java/com/techempower/gemini/firenio/session/HttpSessionManager.java @@ -1,138 +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.session; - -import com.techempower.gemini.*; -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; - } -} +/******************************************************************************* + * 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 db58af30..6b541e15 100755 --- a/gemini-hikaricp/pom.xml +++ b/gemini-hikaricp/pom.xml @@ -21,7 +21,7 @@ gemini-parent com.techempower - 4.0.0-SNAPSHOT + 4.0.1-SNAPSHOT gemini-hikaricp diff --git a/gemini-jdbc/pom.xml b/gemini-jdbc/pom.xml index 48592102..33b3e0c1 100755 --- a/gemini-jdbc/pom.xml +++ b/gemini-jdbc/pom.xml @@ -21,7 +21,7 @@ gemini-parent com.techempower - 4.0.0-SNAPSHOT + 4.0.1-SNAPSHOT gemini-jdbc diff --git a/gemini-jndi/pom.xml b/gemini-jndi/pom.xml index bc358dec..ec011526 100755 --- a/gemini-jndi/pom.xml +++ b/gemini-jndi/pom.xml @@ -21,7 +21,7 @@ gemini-parent com.techempower - 4.0.0-SNAPSHOT + 4.0.1-SNAPSHOT gemini-jndi diff --git a/gemini-log4j12/pom.xml b/gemini-log4j12/pom.xml index a26e426e..e01b4cd1 100644 --- a/gemini-log4j12/pom.xml +++ b/gemini-log4j12/pom.xml @@ -20,7 +20,7 @@ gemini-parent com.techempower - 4.0.0-SNAPSHOT + 4.0.1-SNAPSHOT com.techempower diff --git a/gemini-log4j2/pom.xml b/gemini-log4j2/pom.xml index b031a4d8..4123a5e9 100644 --- a/gemini-log4j2/pom.xml +++ b/gemini-log4j2/pom.xml @@ -20,7 +20,7 @@ gemini-parent com.techempower - 4.0.0-SNAPSHOT + 4.0.1-SNAPSHOT com.techempower diff --git a/gemini-logback/pom.xml b/gemini-logback/pom.xml index 2d4dca92..c685f290 100644 --- a/gemini-logback/pom.xml +++ b/gemini-logback/pom.xml @@ -20,7 +20,7 @@ gemini-parent com.techempower - 4.0.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 069eb7ae..0578ec1b 100755 --- a/gemini-resin-archetype/pom.xml +++ b/gemini-resin-archetype/pom.xml @@ -17,7 +17,7 @@ com.techempower gemini-parent - 4.0.0-SNAPSHOT + 4.0.1-SNAPSHOT gemini-resin-archetype diff --git a/gemini-resin/pom.xml b/gemini-resin/pom.xml index 2ee3e873..d7b80c23 100755 --- a/gemini-resin/pom.xml +++ b/gemini-resin/pom.xml @@ -19,7 +19,7 @@ gemini-parent com.techempower - 4.0.0-SNAPSHOT + 4.0.1-SNAPSHOT gemini-resin diff --git a/gemini/pom.xml b/gemini/pom.xml index 2d62a3b8..ea1807e0 100755 --- a/gemini/pom.xml +++ b/gemini/pom.xml @@ -19,7 +19,7 @@ gemini-parent com.techempower - 4.0.0-SNAPSHOT + 4.0.1-SNAPSHOT gemini diff --git a/pom.xml b/pom.xml index 81a3fc26..940122e1 100755 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ https://github.com/TechEmpower/gemini com.techempower gemini-parent - 4.0.0-SNAPSHOT + 4.0.1-SNAPSHOT msmith