summaryrefslogtreecommitdiffstats
path: root/jdisc_core/src/main/java/com
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /jdisc_core/src/main/java/com
Publish
Diffstat (limited to 'jdisc_core/src/main/java/com')
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/AbstractResource.java205
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/Container.java66
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/HeaderFields.java305
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/Metric.java61
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/NoopSharedResource.java19
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/ProxyRequestHandler.java239
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/ReferencedResource.java58
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/References.java47
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/Request.java411
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/ResourceReference.java33
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/Response.java220
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/SharedResource.java54
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/TimeoutManager.java40
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/Timer.java29
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/AbstractApplication.java108
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/Application.java42
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/ApplicationNotReadyException.java19
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingMatch.java64
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingRepository.java110
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingSet.java84
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingSetSelector.java33
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/BundleInstallationException.java34
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/BundleInstaller.java86
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerActivator.java39
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerBuilder.java133
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerThread.java60
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/DeactivatedContainer.java38
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/GlobPattern.java191
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/GuiceRepository.java127
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricConsumer.java68
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricImpl.java68
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricNullProvider.java15
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricProvider.java24
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/OsgiFramework.java99
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/OsgiHeader.java41
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/ResourcePool.java169
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/ServerRepository.java75
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/UriPattern.java217
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/application/package-info.java152
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/client/AbstractClientApplication.java55
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/client/ClientApplication.java16
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/client/ClientDriver.java133
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/client/package-info.java9
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ActiveContainer.java137
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationConfigModule.java64
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationEnvironmentModule.java37
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationLoader.java261
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapDaemon.java104
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapLoader.java16
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/BundleLocationResolver.java70
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogFormatter.java199
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogListener.java109
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogManager.java54
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerSnapshot.java112
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerTermination.java51
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/DefaultBindingSelector.java18
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ExportPackages.java98
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixFramework.java175
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixParams.java50
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogHandler.java164
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogManager.java102
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogService.java60
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ScheduledQueue.java136
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/SystemTimer.java17
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/TimeoutManagerImpl.java244
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/AbstractContentOutputStream.java68
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/AbstractRequestHandler.java36
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/BindingNotFoundException.java38
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/BlockingContentWriter.java78
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/BufferedContentChannel.java156
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/CallableRequestDispatch.java22
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/CallableResponseDispatch.java34
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/CompletionHandler.java39
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/ContentChannel.java49
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/ContentInputStream.java29
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/FastContentOutputStream.java85
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/FastContentWriter.java156
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureCompletion.java37
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureConjunction.java97
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureResponse.java66
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/NullContent.java42
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/OverloadException.java24
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/ReadableContentChannel.java181
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestDeniedException.java39
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestDispatch.java156
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestHandler.java62
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/ResponseDispatch.java179
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/ResponseHandler.java32
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/ThreadedRequestHandler.java156
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/UnsafeContentInputStream.java82
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/handler/package-info.java68
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/package-info.java54
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/service/AbstractClientProvider.java20
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/service/AbstractServerProvider.java30
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/service/BindingSetNotFoundException.java38
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/service/ClientProvider.java24
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/service/ContainerNotReadyException.java26
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/service/CurrentContainer.java37
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/service/NoBindingSetSelectedException.java39
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/service/ServerProvider.java52
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/service/package-info.java77
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingClientProvider.java29
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingCompletionHandler.java20
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingContentChannel.java23
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingOsgiFramework.java50
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingRequest.java40
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingRequestHandler.java24
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingResponseHandler.java17
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingServerProvider.java21
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/test/ServerProviderConformanceTest.java3143
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/test/TestDriver.java402
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/test/package-info.java8
112 files changed, 12559 insertions, 0 deletions
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/AbstractResource.java b/jdisc_core/src/main/java/com/yahoo/jdisc/AbstractResource.java
new file mode 100644
index 00000000000..9862e574009
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/AbstractResource.java
@@ -0,0 +1,205 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc;
+
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.service.ClientProvider;
+import com.yahoo.jdisc.service.ServerProvider;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * <p>This class provides a thread-safe implementation of the {@link SharedResource} interface, and should be used for
+ * all subclasses of {@link RequestHandler}, {@link ClientProvider} and {@link ServerProvider}. Once the reference count
+ * of this resource reaches zero, the {@link #destroy()} method is called.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public abstract class AbstractResource implements SharedResource {
+
+ private static final Logger log = Logger.getLogger(AbstractResource.class.getName());
+
+ private final boolean debug = SharedResource.DEBUG;
+ private final AtomicInteger refCount;
+ private final Object monitor;
+ private final Set<Throwable> activeReferences;
+ private final ResourceReference initialCreationReference;
+
+ protected AbstractResource() {
+ if (!debug) {
+ this.refCount = new AtomicInteger(1);
+ this.monitor = null;
+ this.activeReferences = null;
+ this.initialCreationReference = new NoDebugResourceReference(this);
+ } else {
+ this.refCount = null;
+ this.monitor = new Object();
+ this.activeReferences = new HashSet<>();
+ final Throwable referenceStack = new Throwable();
+ this.activeReferences.add(referenceStack);
+ this.initialCreationReference = new DebugResourceReference(this, referenceStack);
+ }
+ }
+
+ @Override
+ public final ResourceReference refer() {
+ if (!debug) {
+ addRef(1);
+ return new NoDebugResourceReference(this);
+ }
+
+ final Throwable referenceStack = new Throwable();
+ final String state;
+ synchronized (monitor) {
+ if (activeReferences.isEmpty()) {
+ throw new IllegalStateException("Object is already destroyed, no more new references may be created."
+ + " State={ " + currentStateDebugWithLock() + " }");
+ }
+ activeReferences.add(referenceStack);
+ state = currentStateDebugWithLock();
+ }
+ log.log(Level.WARNING,
+ getClass().getName() + "@" + System.identityHashCode(this) + ".refer(): state={ " + state + " }",
+ referenceStack);
+ return new DebugResourceReference(this, referenceStack);
+ }
+
+ public void release() {
+ initialCreationReference.close();
+ }
+
+ private void removeReferenceStack(final Throwable referenceStack, final Throwable releaseStack) {
+ final boolean doDestroy;
+ final String state;
+ synchronized (monitor) {
+ final boolean wasThere = activeReferences.remove(referenceStack);
+ state = currentStateDebugWithLock();
+ if (!wasThere) {
+ throw new IllegalStateException("Reference is already released and can only be released once."
+ + " reference=" + Arrays.toString(referenceStack.getStackTrace())
+ + ". State={ " + state + "}");
+ }
+ doDestroy = activeReferences.isEmpty();
+ }
+ log.log(Level.WARNING,
+ getClass().getName() + "@" + System.identityHashCode(this) + " release: state={ " + state + " }",
+ releaseStack);
+ if (doDestroy) {
+ destroy();
+ }
+ }
+
+ /**
+ * <p>Returns the reference count of this resource. This typically has no value for other than single-threaded unit-
+ * tests, as it is merely a snapshot of the counter.</p>
+ *
+ * @return The current value of the reference counter.
+ */
+ public final int retainCount() {
+ if (!debug) {
+ return refCount.get();
+ }
+
+ synchronized (monitor) {
+ return activeReferences.size();
+ }
+ }
+
+ /**
+ * <p>This method signals that this AbstractResource can dispose of any internal resources, and commence with shut
+ * down of any internal threads. This will be called once the reference count of this resource reaches zero.</p>
+ */
+ protected void destroy() {
+
+ }
+
+ private int addRef(int value) {
+ while (true) {
+ int prev = refCount.get();
+ if (prev == 0) {
+ throw new IllegalStateException(getClass().getName() + ".addRef(" + value + "):"
+ + " Object is already destroyed."
+ + " Consider toggling the " + SharedResource.SYSTEM_PROPERTY_NAME_DEBUG
+ + " system property to get debugging assistance with reference tracking.");
+ }
+ int next = prev + value;
+ if (refCount.compareAndSet(prev, next)) {
+ return next;
+ }
+ }
+ }
+
+ /**
+ * Returns a string describing the current state of references in human-friendly terms. May be used for debugging.
+ */
+ public String currentState() {
+ if (!debug) {
+ return "Active references: " + refCount.get() + "."
+ + " Resource reference debugging is turned off. Consider toggling the "
+ + SharedResource.SYSTEM_PROPERTY_NAME_DEBUG
+ + " system property to get debugging assistance with reference tracking.";
+ }
+ synchronized (monitor) {
+ return currentStateDebugWithLock();
+ }
+ }
+
+ private String currentStateDebugWithLock() {
+ return "Active references: " + makeListOfActiveReferences();
+ }
+
+ private String makeListOfActiveReferences() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("[");
+ for (final Throwable activeReference : activeReferences) {
+ builder.append(" ");
+ builder.append(Arrays.toString(activeReference.getStackTrace()));
+ }
+ builder.append(" ]");
+ return builder.toString();
+ }
+
+ private static class NoDebugResourceReference implements ResourceReference {
+ private final AbstractResource resource;
+ private final AtomicBoolean isReleased = new AtomicBoolean(false);
+
+ public NoDebugResourceReference(final AbstractResource resource) {
+ this.resource = resource;
+ }
+
+ @Override
+ public final void close() {
+ final boolean wasReleasedBefore = isReleased.getAndSet(true);
+ if (wasReleasedBefore) {
+ final String message = "Reference is already released and can only be released once."
+ + " State={ " + resource.currentState() + " }";
+ throw new IllegalStateException(message);
+ }
+ int refCount = resource.addRef(-1);
+ if (refCount == 0) {
+ resource.destroy();
+ }
+ }
+ }
+
+ private static class DebugResourceReference implements ResourceReference {
+ private final AbstractResource resource;
+ private final Throwable referenceStack;
+
+ public DebugResourceReference(final AbstractResource resource, final Throwable referenceStack) {
+ this.resource = resource;
+ this.referenceStack = referenceStack;
+ }
+
+ @Override
+ public final void close() {
+ final Throwable releaseStack = new Throwable();
+ resource.removeReferenceStack(referenceStack, releaseStack);
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/Container.java b/jdisc_core/src/main/java/com/yahoo/jdisc/Container.java
new file mode 100644
index 00000000000..53e9c76eb61
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/Container.java
@@ -0,0 +1,66 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc;
+
+import com.google.inject.ConfigurationException;
+import com.google.inject.Key;
+import com.google.inject.ProvisionException;
+import com.yahoo.jdisc.application.Application;
+import com.yahoo.jdisc.application.BindingSet;
+import com.yahoo.jdisc.application.ContainerActivator;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.service.CurrentContainer;
+import com.yahoo.jdisc.service.ServerProvider;
+
+import java.net.URI;
+
+/**
+ * <p>This is the immutable Container. An instance of this class is attached to every {@link Request}, and as long as
+ * the {@link Request#release()} method has not been called, that Container instance is actively kept alive to prevent
+ * any race conditions during reconfiguration or shutdown. At any time there is only a single active Container in the
+ * running {@link Application}, and the only way to retrieve a reference to that Container is by calling {@link
+ * CurrentContainer#newReference(URI)}. Instead of holding a local Container object inside a {@link ServerProvider}
+ * (which will eventually become stale), use the {@link Request#Request(CurrentContainer, URI) appropriate Request
+ * constructor} instead.</p>
+ *
+ * <p>The only way to <u>create</u> a new instance of this class is to 1) create and configure a {@link
+ * ContainerBuilder}, and 2) pass that to the {@link ContainerActivator#activateContainer(ContainerBuilder)} method.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface Container extends SharedResource, Timer {
+
+ /**
+ * <p>Attempts to find a {@link RequestHandler} in the current server- (if {@link Request#isServerRequest()} is
+ * <em>true</em>) or client- (if {@link Request#isServerRequest()} is <em>false</em>) {@link BindingSet} that
+ * matches the given {@link URI}. If no match can be found, this method returns null.</p>
+ *
+ * @param request The Request to match against the bound {@link RequestHandler}s.
+ * @return The matching RequestHandler, or null if there is no match.
+ */
+ public RequestHandler resolveHandler(Request request);
+
+ /**
+ * <p>Returns the appropriate instance for the given injection key. When feasible, avoid using this method in favor
+ * of having Guice inject your dependencies ahead of time.</p>
+ *
+ * @param key The key of the instance to return.
+ * @param <T> The class of the instance to return.
+ * @return The appropriate instance of the given class.
+ * @throws ConfigurationException If this injector cannot find or create the provider.
+ * @throws ProvisionException If there was a runtime failure while providing an instance.
+ */
+ public <T> T getInstance(Key<T> key);
+
+ /**
+ * <p>Returns the appropriate instance for the given injection type. When feasible, avoid using this method in
+ * favor of having Guice inject your dependencies ahead of time.</p>
+ *
+ * @param type The class object of the instance to return.
+ * @param <T> The class of the instance to return.
+ * @return The appropriate instance of the given class.
+ * @throws ConfigurationException If this injector cannot find or create the provider.
+ * @throws ProvisionException If there was a runtime failure while providing an instance.
+ */
+ public <T> T getInstance(Class<T> type);
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/HeaderFields.java b/jdisc_core/src/main/java/com/yahoo/jdisc/HeaderFields.java
new file mode 100644
index 00000000000..a81fb3ff152
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/HeaderFields.java
@@ -0,0 +1,305 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.*;
+
+/**
+ * <p>This is an encapsulation of the header fields that belong to either a {@link Request} or a {@link Response}. It is
+ * a multimap from String to String, with some additional methods for convenience. The keys of this map are compared by
+ * ignoring their case, so that <tt>get("foo")</tt> returns the same entry as <tt>get("FOO")</tt>.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class HeaderFields implements Map<String, List<String>> {
+
+ private final TreeMap<String, List<String>> content = new TreeMap<>(new Comparator<String>() {
+
+ @Override
+ public int compare(String lhs, String rhs) {
+ return lhs.compareToIgnoreCase(rhs);
+ }
+ });
+
+ @Override
+ public int size() {
+ return content.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return content.isEmpty();
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ return content.containsKey(key);
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ return content.containsValue(value);
+ }
+
+ /**
+ * <p>Convenience method for checking whether or not a named header contains a specific value. If the named header
+ * is not set, or if the given value is not contained within that header's value list, this method returns
+ * <em>false</em>.</p>
+ *
+ * <p><em>NOTE:</em> This method is case-SENSITIVE.</p>
+ *
+ * @param key The key whose values to search in.
+ * @param value The values to search for.
+ * @return True if the given value was found in the named header.
+ * @see #containsIgnoreCase
+ */
+ public boolean contains(String key, String value) {
+ List<String> lst = content.get(key);
+ if (lst == null) {
+ return false;
+ }
+ return lst.contains(value);
+ }
+
+ /**
+ * <p>Convenience method for checking whether or not a named header contains a specific value, regardless of case.
+ * If the named header is not set, or if the given value is not contained within that header's value list, this
+ * method returns <em>false</em>.</p>
+ *
+ * <p><em>NOTE:</em> This method is case-INSENSITIVE.</p>
+ *
+ * @param key The key whose values to search in.
+ * @param value The values to search for, ignoring case.
+ * @return True if the given value was found in the named header.
+ * @see #contains
+ */
+ public boolean containsIgnoreCase(String key, String value) {
+ List<String> lst = content.get(key);
+ if (lst == null) {
+ return false;
+ }
+ for (String val : lst) {
+ if (value.equalsIgnoreCase(val)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * <p>Adds the given value to the entry of the specified key. If no entry exists for the given key, a new one is
+ * created containing only the given value.</p>
+ *
+ * @param key The key with which the specified value is to be associated.
+ * @param value The value to be added to the list associated with the specified key.
+ */
+ public void add(String key, String value) {
+ List<String> lst = content.get(key);
+ if (lst != null) {
+ lst.add(value);
+ } else {
+ put(key, value);
+ }
+ }
+
+ /**
+ * <p>Adds the given values to the entry of the specified key. If no entry exists for the given key, a new one is
+ * created containing only the given values.</p>
+ *
+ * @param key The key with which the specified value is to be associated.
+ * @param values The values to be added to the list associated with the specified key.
+ */
+ public void add(String key, List<String> values) {
+ List<String> lst = content.get(key);
+ if (lst != null) {
+ lst.addAll(values);
+ } else {
+ put(key, values);
+ }
+ }
+
+ /**
+ * <p>Adds all the entries of the given map to this. This is the same as calling {@link #add(String, List)} for each
+ * entry in <tt>values</tt>.</p>
+ *
+ * @param values The values to be added to this.
+ */
+ public void addAll(Map<? extends String, ? extends List<String>> values) {
+ for (Entry<? extends String, ? extends List<String>> entry : values.entrySet()) {
+ add(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * <p>Convenience method to call {@link #put(String, List)} with a singleton list that contains the specified
+ * value.</p>
+ *
+ * @param key The key of the entry to put.
+ * @param value The value to put.
+ * @return The previous value associated with <tt>key</tt>, or <tt>null</tt> if there was no mapping for
+ * <tt>key</tt>.
+ */
+ public List<String> put(String key, String value) {
+ ArrayList<String> list = new ArrayList<String>(1);
+ list.add(value);
+ return content.put(key, list);
+ }
+
+ @Override
+ public List<String> put(String key, List<String> value) {
+ return content.put(key, new ArrayList<>(value));
+ }
+
+ @Override
+ public void putAll(Map<? extends String, ? extends List<String>> values) {
+ for (Entry<? extends String, ? extends List<String>> entry : values.entrySet()) {
+ put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ @Override
+ public List<String> remove(Object key) {
+ return content.remove(key);
+ }
+
+ /**
+ * <p>Removes the given value from the entry of the specified key.</p>
+ *
+ * @param key The key of the entry to remove from.
+ * @param value The value to remove from the entry.
+ * @return True if the value was removed.
+ */
+ public boolean remove(String key, String value) {
+ List<String> lst = content.get(key);
+ if (lst == null) {
+ return false;
+ }
+ if (!lst.remove(value)) {
+ return false;
+ }
+ if (lst.isEmpty()) {
+ content.remove(key);
+ }
+ return true;
+ }
+
+ @Override
+ public void clear() {
+ content.clear();
+ }
+
+ @Override
+ public List<String> get(Object key) {
+ return content.get(key);
+ }
+
+ /**
+ * <p>Convenience method for retrieving the first value of a named header field. If the header is not set, or if the
+ * value list is empty, this method returns null.</p>
+ *
+ * @param key The key whose first value to return.
+ * @return The first value of the named header, or null.
+ */
+ public String getFirst(String key) {
+ List<String> lst = get(key);
+ if (lst == null || lst.isEmpty()) {
+ return null;
+ }
+ return lst.get(0);
+ }
+
+ /**
+ * <p>Convenience method for checking whether or not a named header field is <em>true</em>. To satisfy this, the
+ * header field needs to have at least 1 entry, and Boolean.valueOf() of all its values must parse as
+ * <em>true</em>.</p>
+ *
+ * @param key The key whose values to parse as a boolean.
+ * @return The boolean value of the named header.
+ */
+ public boolean isTrue(String key) {
+ List<String> lst = content.get(key);
+ if (lst == null) {
+ return false;
+ }
+ for (String value : lst) {
+ if (!Boolean.valueOf(value)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public Set<String> keySet() {
+ return content.keySet();
+ }
+
+ @Override
+ public Collection<List<String>> values() {
+ return content.values();
+ }
+
+ @Override
+ public Set<Entry<String, List<String>>> entrySet() {
+ return content.entrySet();
+ }
+
+ @Override
+ public String toString() {
+ return content.toString();
+ }
+
+ /**
+ * <p>Returns an unmodifiable list of all key-value pairs of this. This provides a flattened view on the content of
+ * this map.</p>
+ *
+ * @return The collection of entries.
+ */
+ public List<Entry<String, String>> entries() {
+ List<Entry<String, String>> list = new ArrayList<>(content.size());
+ for (Entry<String, List<String>> entry : content.entrySet()) {
+ String key = entry.getKey();
+ for (String value : entry.getValue()) {
+ list.add(new MyEntry(key, value));
+ }
+ }
+ return ImmutableList.copyOf(list);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof HeaderFields && content.equals(((HeaderFields)obj).content);
+ }
+
+ @Override
+ public int hashCode() {
+ return content.hashCode();
+ }
+
+ private static class MyEntry implements Map.Entry<String, String> {
+
+ final String key;
+ final String value;
+
+ private MyEntry(String key, String value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ @Override
+ public String getKey() {
+ return key;
+ }
+
+ @Override
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public String setValue(String value) {
+ throw new UnsupportedOperationException();
+ }
+ }
+} \ No newline at end of file
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/Metric.java b/jdisc_core/src/main/java/com/yahoo/jdisc/Metric.java
new file mode 100644
index 00000000000..50b25dbbdf0
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/Metric.java
@@ -0,0 +1,61 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc;
+
+import com.google.inject.ProvidedBy;
+import com.google.inject.Provider;
+import com.yahoo.jdisc.application.MetricConsumer;
+import com.yahoo.jdisc.application.MetricProvider;
+
+import java.util.Map;
+
+/**
+ * <p>This interface provides an API for writing metric data to the configured {@link MetricConsumer}. If no {@link
+ * Provider} for the MetricConsumer class has been bound by the application, all calls to this interface are no-ops. The
+ * implementation of this interface uses thread local consumer instances, so as long as the {@link MetricConsumer} is
+ * thread-safe, so is this.</p>
+ *
+ * <p>An instance of this class can be injected anywhere.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+@ProvidedBy(MetricProvider.class)
+public interface Metric {
+
+ /**
+ * <p>Set a metric value. This is typically used with histogram-type metrics.</p>
+ *
+ * @param key The name of the metric to modify.
+ * @param val The value to assign to the named metric.
+ * @param ctx The context to further describe this entry.
+ */
+ void set(String key, Number val, Context ctx);
+
+ /**
+ * <p>Add to a metric value. This is typically used with counter-type metrics.</p>
+ *
+ * @param key The name of the metric to modify.
+ * @param val The value to add to the named metric.
+ * @param ctx The context to further describe this entry.
+ */
+ void add(String key, Number val, Context ctx);
+
+ /**
+ * <p>Creates a {@link MetricConsumer}-specific {@link Context} object that encapsulates the given properties. The
+ * returned Context object should be passed along every future call to {@link #set(String, Number, Context)} and
+ * {@link #add(String, Number, Context)} where the properties match those given here.</p>
+ *
+ * @param properties The properties to incorporate in the context.
+ * @return The created context.
+ */
+ Context createContext(Map<String, ?> properties);
+
+ /**
+ * <p>Declares the interface for the arbitrary context object to pass to both the {@link
+ * #set(String, Number, Context)} and {@link #add(String, Number, Context)} methods. This is intentionally empty so
+ * that implementations can vary.</p>
+ */
+ interface Context {
+
+ }
+
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/NoopSharedResource.java b/jdisc_core/src/main/java/com/yahoo/jdisc/NoopSharedResource.java
new file mode 100644
index 00000000000..fe4990dbd60
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/NoopSharedResource.java
@@ -0,0 +1,19 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc;
+
+/**
+ * An implementation of {@link SharedResource} that does not do anything.
+ * Useful base class for e.g. mocks of SharedResource sub-interfaces, where reference counting is not the focus.
+ *
+ * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+ */
+public class NoopSharedResource implements SharedResource {
+ @Override
+ public final ResourceReference refer() {
+ return References.NOOP_REFERENCE;
+ }
+
+ @Override
+ public final void release() {
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/ProxyRequestHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/ProxyRequestHandler.java
new file mode 100644
index 00000000000..6f3d342705c
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/ProxyRequestHandler.java
@@ -0,0 +1,239 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc;
+
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.NullContent;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.handler.ResponseHandler;
+
+import java.nio.ByteBuffer;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+* @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+*/
+class ProxyRequestHandler implements RequestHandler {
+
+ private static final CompletionHandler IGNORED_COMPLETION = new IgnoredCompletion();
+ private static final Logger log = Logger.getLogger(ProxyRequestHandler.class.getName());
+
+ final RequestHandler delegate;
+
+ ProxyRequestHandler(RequestHandler delegate) {
+ Objects.requireNonNull(delegate, "delegate");
+ this.delegate = delegate;
+ }
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler responseHandler) {
+ try (final ResourceReference requestReference = request.refer()) {
+ ContentChannel contentChannel;
+ final ResponseHandler proxyResponseHandler = new ProxyResponseHandler(
+ request, new NullContentResponseHandler(responseHandler));
+ try {
+ contentChannel = delegate.handleRequest(request, proxyResponseHandler);
+ Objects.requireNonNull(contentChannel, "contentChannel");
+ } catch (Throwable t) {
+ try {
+ proxyResponseHandler
+ .handleResponse(new Response(Response.Status.INTERNAL_SERVER_ERROR, t))
+ .close(IGNORED_COMPLETION);
+ } catch (Throwable ignored) {
+ // empty
+ }
+ throw t;
+ }
+ return new ProxyContentChannel(request, contentChannel);
+ }
+ }
+
+ @Override
+ public void handleTimeout(Request request, ResponseHandler responseHandler) {
+ delegate.handleTimeout(request, new NullContentResponseHandler(responseHandler));
+ }
+
+ @Override
+ public ResourceReference refer() {
+ return delegate.refer();
+ }
+
+ @Override
+ public void release() {
+ delegate.release();
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+
+ private static class ProxyResponseHandler implements ResponseHandler {
+
+ final SharedResource request;
+ final ResourceReference requestReference;
+ final ResponseHandler delegate;
+ final AtomicBoolean closed = new AtomicBoolean(false);
+
+ ProxyResponseHandler(SharedResource request, ResponseHandler delegate) {
+ Objects.requireNonNull(request, "request");
+ Objects.requireNonNull(delegate, "delegate");
+ this.request = request;
+ this.delegate = delegate;
+ this.requestReference = request.refer();
+ }
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ if (closed.getAndSet(true)) {
+ throw new IllegalStateException(delegate + " is already called.");
+ }
+ try (final ResourceReference ref = requestReference) {
+ ContentChannel contentChannel = delegate.handleResponse(response);
+ Objects.requireNonNull(contentChannel, "contentChannel");
+ return new ProxyContentChannel(request, contentChannel);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+ }
+
+ private static class ProxyContentChannel implements ContentChannel {
+
+ final SharedResource request;
+ final ResourceReference requestReference;
+ final ContentChannel delegate;
+
+ ProxyContentChannel(SharedResource request, ContentChannel delegate) {
+ Objects.requireNonNull(request, "request");
+ Objects.requireNonNull(delegate, "delegate");
+ this.request = request;
+ this.delegate = delegate;
+ this.requestReference = request.refer();
+ }
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler completionHandler) {
+ ProxyCompletionHandler proxyCompletionHandler = new ProxyCompletionHandler(request, completionHandler);
+ try {
+ delegate.write(buf, proxyCompletionHandler);
+ } catch (Throwable t) {
+ try {
+ proxyCompletionHandler.failed(t);
+ } catch (Throwable ignored) {
+ // empty
+ }
+ throw t;
+ }
+ }
+
+ @Override
+ public void close(CompletionHandler completionHandler) {
+ final ProxyCompletionHandler proxyCompletionHandler
+ = new ProxyCompletionHandler(request, completionHandler);
+ try (final ResourceReference ref = requestReference) {
+ delegate.close(proxyCompletionHandler);
+ } catch (Throwable t) {
+ try {
+ proxyCompletionHandler.failed(t);
+ } catch (Throwable ignored) {
+ // empty
+ }
+ throw t;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+ }
+
+ private static class ProxyCompletionHandler implements CompletionHandler {
+
+ final ResourceReference requestReference;
+ final CompletionHandler delegate;
+ final AtomicBoolean closed = new AtomicBoolean(false);
+
+ public ProxyCompletionHandler(SharedResource request, CompletionHandler delegate) {
+ this.delegate = delegate;
+ this.requestReference = request.refer();
+ }
+
+ @Override
+ public void completed() {
+ if (closed.getAndSet(true)) {
+ throw new IllegalStateException(delegate + " is already called.");
+ }
+ try {
+ if (delegate != null) {
+ delegate.completed();
+ }
+ } finally {
+ requestReference.close();
+ }
+ }
+
+ @Override
+ public void failed(Throwable t) {
+ if (closed.getAndSet(true)) {
+ throw new IllegalStateException(delegate + " is already called.");
+ }
+ try (final ResourceReference ref = requestReference) {
+ if (delegate != null) {
+ delegate.failed(t);
+ } else {
+ log.log(Level.WARNING, "Uncaught completion failure.", t);
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(delegate);
+ }
+ }
+
+ private static class NullContentResponseHandler implements ResponseHandler {
+
+ final ResponseHandler delegate;
+
+ NullContentResponseHandler(ResponseHandler delegate) {
+ Objects.requireNonNull(delegate, "delegate");
+ this.delegate = delegate;
+ }
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ ContentChannel contentChannel = delegate.handleResponse(response);
+ if (contentChannel == null) {
+ contentChannel = NullContent.INSTANCE;
+ }
+ return contentChannel;
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+ }
+
+ private static class IgnoredCompletion implements CompletionHandler {
+
+ @Override
+ public void completed() {
+ // ignore
+ }
+
+ @Override
+ public void failed(Throwable t) {
+ // ignore
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/ReferencedResource.java b/jdisc_core/src/main/java/com/yahoo/jdisc/ReferencedResource.java
new file mode 100644
index 00000000000..f55a46f1a05
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/ReferencedResource.java
@@ -0,0 +1,58 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc;
+
+/**
+ * <p>Utility class for working with reference-counted {@link SharedResource}s.</p>
+ *
+ * <p>Sometimes, you may want a method to return <i>both</i> a resource object <i>and</i>
+ * a {@link ResourceReference} that refers the resource object (for later release of the resource).
+ * Java methods cannot return multiple objects, so this class provides Pair-like functionality
+ * for returning both.</p>
+ *
+ * <p>Example usage:</p>
+ * <pre>
+ * ReferencedResource&lt;MyResource&gt; getResource() {
+ * final ResourceReference ref = resource.refer();
+ * return new ReferencedResource(resource, ref);
+ * }
+ *
+ * void useResource() {
+ * final ReferencedResource&lt;MyResource&gt; referencedResource = getResource();
+ * referencedResource.getResource().use();
+ * referencedResource.getReference().close();
+ * }
+ * </pre>
+ *
+ * <p>This class implements AutoCloseable, so the latter method may also be written as follows:</p>
+ * <pre>
+ * void useResource() {
+ * for (final ReferencedResource&lt;MyResource&gt; referencedResource = getResource()) {
+ * referencedResource.getResource().use();
+ * }
+ * }
+ * </pre>
+ *
+ * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+ */
+public class ReferencedResource<T extends SharedResource> implements AutoCloseable {
+ private final T resource;
+ private final ResourceReference reference;
+
+ public ReferencedResource(final T resource, final ResourceReference reference) {
+ this.resource = resource;
+ this.reference = reference;
+ }
+
+ public T getResource() {
+ return resource;
+ }
+
+ public ResourceReference getReference() {
+ return reference;
+ }
+
+ @Override
+ public void close() {
+ reference.close();
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/References.java b/jdisc_core/src/main/java/com/yahoo/jdisc/References.java
new file mode 100644
index 00000000000..868ae6ac720
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/References.java
@@ -0,0 +1,47 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc;
+
+/**
+ * Utility class for working with {@link SharedResource}s and {@link ResourceReference}s.
+ *
+ * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+ */
+public class References {
+ // Prevents instantiation.
+ private References() {
+ }
+
+ /**
+ * A {@link ResourceReference} that does nothing.
+ * Useful for e.g. testing of resource types when reference counting is not the focus.
+ */
+ public static final ResourceReference NOOP_REFERENCE = new ResourceReference() {
+ @Override
+ public void close() {
+ }
+ };
+
+ /**
+ * <p>Returns a {@link ResourceReference} that invokes {@link SharedResource#release()} on
+ * {@link ResourceReference#close() close}. Useful for treating the "main" reference of a {@link SharedResource}
+ * just as any other reference obtained by calling {@link SharedResource#refer()}. Example:</p>
+ * <pre>
+ * final Request request = new Request(...);
+ * try (final ResourceReference ref = References.fromResource(request)) {
+ * ....
+ * }
+ * // The request will be released on exit from the try block.
+ * </pre>
+ *
+ * @param resource The resource to create a ResourceReference for.
+ * @return a ResourceReference whose close() method will call release() on the given resource.
+ */
+ public static ResourceReference fromResource(final SharedResource resource) {
+ return new ResourceReference() {
+ @Override
+ public void close() {
+ resource.release();
+ }
+ };
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/Request.java b/jdisc_core/src/main/java/com/yahoo/jdisc/Request.java
new file mode 100644
index 00000000000..a210660aae5
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/Request.java
@@ -0,0 +1,411 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc;
+
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.jdisc.application.UriPattern;
+import com.yahoo.jdisc.handler.BindingNotFoundException;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.RequestDeniedException;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.service.CurrentContainer;
+import com.yahoo.jdisc.service.ServerProvider;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * <p>This class represents a single request (which may have any content model that a {@link ServerProvider} chooses to
+ * implement). The {@link #uri URI} is used by the {@link Container} to route it to the appropriate {@link
+ * RequestHandler}, which in turn will provide a {@link ContentChannel} to write content to.</p>
+ *
+ * <p>To ensure application consistency throughout the lifetime of a Request, the Request itself holds an active
+ * reference to the Container for which it was created. This has the unfortunate side-effect of requiring the creator of
+ * a Request to do explicit reference counting during the setup of a content stream.</p>
+ *
+ * <p>For every successfully dispatched Request (i.e. a non-null ContentChannel has been retrieved), there will be
+ * exactly one {@link Response} returned to the provided {@link ResponseHandler}.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ * @see Container
+ * @see Response
+ */
+public class Request extends AbstractResource {
+
+ private final Map<String, Object> context = new HashMap<>();
+ private final HeaderFields headers = new HeaderFields();
+ private final Container container;
+ private final Request parent;
+ private final ResourceReference parentReference;
+ private final long creationTime;
+ private volatile boolean cancel = false;
+ private BindingMatch<RequestHandler> bindingMatch;
+ private TimeoutManager timeoutManager;
+ private boolean serverRequest;
+ private Long timeout;
+ private URI uri;
+
+ /**
+ * <p>Creates a new instance of this class. As a {@link ServerProvider} you need to inject a {@link
+ * CurrentContainer} instance at construction time and use that as argument to this method. As a {@link
+ * RequestHandler} that needs to spawn child Requests, use the {@link #Request(Request, URI) other
+ * constructor}.</p>
+ *
+ * <p>Because a Request holds an active reference to the owning {@link Container}, it is necessary to call {@link
+ * #release()} once a {@link ContentChannel} has been established. Suggested usage:</p>
+ *
+ * <pre>
+ * Request request = null;
+ * ContentChannel content = null;
+ * try {
+ * request = new Request(currentContainer, uri);
+ * (...)
+ * content = request.connect(responseHandler);
+ * } finally {
+ * if (request != null) request.release();
+ * }
+ * content.write(...);
+ * </pre>
+ *
+ * @param current The CurrentContainer for which this Request is created.
+ * @param uri The identifier of this request.
+ */
+ public Request(CurrentContainer current, URI uri) {
+ container = current.newReference(uri);
+ parent = null;
+ parentReference = null;
+ creationTime = container.currentTimeMillis();
+ serverRequest = true;
+ setUri(uri);
+ }
+
+ /**
+ * <p>Creates a new instance of this class. As a {@link RequestHandler} you should use this method to spawn child
+ * Requests of another. As a {@link ServerProvider} that needs to spawn new Requests, us the {@link
+ * #Request(CurrentContainer, URI) other constructor}.</p>
+ *
+ * <p>Because a Request holds an active reference to the owning {@link Container}, it is necessary to call {@link
+ * #release()} once a {@link ContentChannel} has been established. Suggested usage:</p>
+ *
+ * <pre>
+ * Request request = null;
+ * ContentChannel content = null;
+ * try {
+ * request = new Request(parentRequest, uri);
+ * (...)
+ * content = request.connect(responseHandler);
+ * } finally {
+ * if (request != null) request.release();
+ * }
+ * content.write(...);
+ * </pre>
+ *
+ * @param parent The parent Request of this.
+ * @param uri The identifier of this request.
+ */
+ public Request(Request parent, URI uri) {
+ this.parent = parent;
+ this.parentReference = this.parent.refer();
+ container = null;
+ creationTime = parent.container().currentTimeMillis();
+ serverRequest = false;
+ setUri(uri);
+ }
+
+ /**
+ * <p>Returns the {@link Container} for which this Request was created.</p>
+ *
+ * @return The container instance.
+ */
+ public Container container() {
+ return parent != null ? parent.container() : container;
+ }
+
+ /**
+ * <p>Returns the Uniform Resource Identifier used by the {@link Container} to resolve the appropriate {@link
+ * RequestHandler} for this Request.</p>
+ *
+ * @return The resource identifier.
+ * @see #setUri(URI)
+ */
+ public URI getUri() {
+ return uri;
+ }
+
+ /**
+ * <p>Sets the Uniform Resource Identifier used by the {@link Container} to resolve the appropriate {@link
+ * RequestHandler} for this Request. Because access to the URI is not guarded by any lock, any changes made after
+ * calling {@link #connect(ResponseHandler)} might never become visible to other threads.</p>
+ *
+ * @param uri The URI to set.
+ * @return This, to allow chaining.
+ * @see #getUri()
+ */
+ public Request setUri(URI uri) {
+ this.uri = uri.normalize();
+ return this;
+ }
+
+ /**
+ * <p>Returns whether or not this Request was created by a {@link ServerProvider}. The value of this is used by
+ * {@link Container#resolveHandler(Request)} to decide whether to match against server- or client-bindings.</p>
+ *
+ * @return True, if this is a server request.
+ */
+ public boolean isServerRequest() {
+ return serverRequest;
+ }
+
+ /**
+ * <p>Sets whether or not this Request was created by a {@link ServerProvider}. The constructor that accepts a
+ * {@link CurrentContainer} sets this to <em>true</em>, whereas the constructor that accepts a parent Request sets
+ * this to <em>false</em>.</p>
+ *
+ * @param serverRequest Whether or not this is a server request.
+ * @return This, to allow chaining.
+ * @see #isServerRequest()
+ */
+ public Request setServerRequest(boolean serverRequest) {
+ this.serverRequest = serverRequest;
+ return this;
+ }
+
+ /**
+ * <p>Returns the last resolved {@link BindingMatch}, or null if none has been resolved yet. This is set
+ * automatically when calling the {@link Container#resolveHandler(Request)} method. The BindingMatch object holds
+ * information about the match of this Request's {@link #getUri() URI} to the {@link UriPattern} of the resolved
+ * {@link RequestHandler}. It allows you to reflect on the parts of the URI that were matched by wildcards in the
+ * UriPattern.</p>
+ *
+ * @return The last resolved BindingMatch, or null.
+ * @see #setBindingMatch(BindingMatch)
+ * @see Container#resolveHandler(Request)
+ */
+ public BindingMatch<RequestHandler> getBindingMatch() {
+ return bindingMatch;
+ }
+
+ /**
+ * <p>Sets the last resolved {@link BindingMatch} of this Request. This is called by the {@link
+ * Container#resolveHandler(Request)} method.</p>
+ *
+ * @param bindingMatch The BindingMatch to set.
+ * @return This, to allow chaining.
+ * @see #getBindingMatch()
+ */
+ public Request setBindingMatch(BindingMatch<RequestHandler> bindingMatch) {
+ this.bindingMatch = bindingMatch;
+ return this;
+ }
+
+ /**
+ * <p>Returns the named application context objects. This data is not intended for network transport, rather they
+ * are intended for passing shared data between components of an Application.</p>
+ *
+ * <p>Modifying the context map is a thread-unsafe operation -- any changes made after calling {@link
+ * #connect(ResponseHandler)} might never become visible to other threads, and might throw
+ * ConcurrentModificationExceptions in other threads.</p>
+ *
+ * @return The context map.
+ */
+ public Map<String, Object> context() {
+ return context;
+ }
+
+ /**
+ * <p>Returns the set of header fields of this Request. These are the meta-data of the Request, and are not applied
+ * to any internal {@link Container} logic. As opposed to the {@link #context()}, the headers ARE intended for
+ * network transport. Modifying headers is a thread-unsafe operation -- any changes made after calling {@link
+ * #connect(ResponseHandler)} might never become visible to other threads, and might throw
+ * ConcurrentModificationExceptions in other threads.</p>
+ *
+ * @return The header fields.
+ */
+ public HeaderFields headers() {
+ return headers;
+ }
+
+ /**
+ * <p>Sets a {@link TimeoutManager} to be called when {@link #setTimeout(long, TimeUnit)} is invoked. If a timeout
+ * has already been set for this Request, the TimeoutManager is called before returning. This method will throw an
+ * IllegalStateException if it has already been called.</p>
+ *
+ * <p><b>NOTE:</b> This is used by the default timeout management implementation, so unless you are replacing that
+ * mechanism you should avoid calling this method. If you <em>do</em> want to replace that mechanism, you need to
+ * call this method prior to calling the target {@link RequestHandler} (since that injects the default manager).</p>
+ *
+ * @param timeoutManager The manager to set.
+ * @throws NullPointerException If the TimeoutManager is null.
+ * @throws IllegalStateException If another TimeoutManager has already been set.
+ * @see #getTimeoutManager()
+ * @see #setTimeout(long, TimeUnit)
+ */
+ public void setTimeoutManager(TimeoutManager timeoutManager) {
+ Objects.requireNonNull(timeoutManager, "timeoutManager");
+ if (this.timeoutManager != null) {
+ throw new IllegalStateException("Timeout manager already set.");
+ }
+ this.timeoutManager = timeoutManager;
+ if (timeout != null) {
+ timeoutManager.scheduleTimeout(this);
+ }
+ }
+
+ /**
+ * <p>Returns the {@link TimeoutManager} of this request, or null if none has been assigned.</p>
+ *
+ * @return The TimeoutManager of this Request.
+ * @see #setTimeoutManager(TimeoutManager)
+ */
+ public TimeoutManager getTimeoutManager() {
+ return timeoutManager;
+ }
+
+ /**
+ * <p>Sets the allocated time that this Request is allowed to exist before the corresponding call to {@link
+ * ResponseHandler#handleResponse(Response)} must have been made. If no timeout value is assigned to a Request,
+ * there will be no timeout.</p>
+ *
+ * <p>Once the allocated time has expired, unless the {@link ResponseHandler} has already been called, the {@link
+ * RequestHandler#handleTimeout(Request, ResponseHandler)} method is invoked.</p>
+ *
+ * <p>Calls to {@link #isCancelled()} return <em>true</em> if timeout has been exceeded.</p>
+ *
+ * @param timeout The allocated amount of time.
+ * @param unit The time unit of the <em>timeout</em> argument.
+ * @see #getTimeout(TimeUnit)
+ * @see #timeRemaining(TimeUnit)
+ */
+ public void setTimeout(long timeout, TimeUnit unit) {
+ this.timeout = unit.toMillis(timeout);
+ if (timeoutManager != null) {
+ timeoutManager.scheduleTimeout(this);
+ }
+ }
+
+ /**
+ * <p>Returns the allocated number of milliseconds that this Request is allowed to exist. If no timeout has been set
+ * for this Request, this method returns <em>null</em>.</p>
+ *
+ * @param unit The unit to return the timeout in.
+ * @return The timeout of this Request.
+ * @see #setTimeout(long, TimeUnit)
+ */
+ public Long getTimeout(TimeUnit unit) {
+ if (timeout == null) {
+ return null;
+ }
+ return unit.convert(timeout, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * <p>Returns the time that this Request is allowed to exist. If no timeout has been set, this method will return
+ * <em>null</em>.</p>
+ *
+ * @param unit The unit to return the time in.
+ * @return The number of milliseconds left until this Request times out, or <em>null</em>.
+ */
+ public Long timeRemaining(TimeUnit unit) {
+ if (timeout == null) {
+ return null;
+ }
+ return unit.convert(timeout - (container().currentTimeMillis() - creationTime), TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * <p>Returns the time at which this Request was created. This is whatever value was returned by {@link
+ * Timer#currentTimeMillis()} when constructing this.</p>
+ *
+ * @param unit The unit to return the time in.
+ * @return The creation time of this Request.
+ */
+ public long creationTime(TimeUnit unit) {
+ return unit.convert(creationTime, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * <p>Returns whether or not this Request has been cancelled. This can be thought of as the {@link
+ * Thread#isInterrupted()} of Requests - it does not enforce anything in ways of blocking the Request, it is simply
+ * a signal to allow the developer to break early if the Request has already been dropped.</p>
+ *
+ * <p>This method will also return <em>true</em> if the Request has a non-null timeout, and that timeout has
+ * expired.</p>
+ *
+ * <p>Finally, this method will also return <em>true</em> if this Request has a parent Request that has been
+ * cancelled.</p>
+ *
+ * @return True if this Request has timed out or been cancelled.
+ * @see #cancel()
+ * @see #setTimeout(long, TimeUnit)
+ */
+ public boolean isCancelled() {
+ if (cancel) {
+ return true;
+ }
+ if (timeout != null && timeRemaining(TimeUnit.MILLISECONDS) <= 0) {
+ return true;
+ }
+ if (parent != null && parent.isCancelled()) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * <p>Mark this request as cancelled and frees any resources held by the request if possible.
+ * All subsequent calls to {@link #isCancelled()} on this Request return <em>true</em>.</p>
+ *
+ * @see #isCancelled()
+ */
+ public void cancel() {
+ if (cancel) return;
+
+ if (timeoutManager != null && timeout != null)
+ timeoutManager.unscheduleTimeout(this);
+ cancel = true;
+ }
+
+ /**
+ * <p>Attempts to resolve and connect to the {@link RequestHandler} appropriate for the {@link URI} of this Request.
+ * An exception is thrown if this operation fails at any point. This method is exception-safe.</p>
+ *
+ * @param responseHandler The handler to pass the corresponding {@link Response} to.
+ * @return The {@link ContentChannel} to write the Request content to.
+ * @throws NullPointerException If the {@link ResponseHandler} is null.
+ * @throws BindingNotFoundException If the corresponding call to {@link Container#resolveHandler(Request)} returns
+ * null.
+ */
+ public ContentChannel connect(final ResponseHandler responseHandler) {
+ try {
+ Objects.requireNonNull(responseHandler, "responseHandler");
+ RequestHandler requestHandler = container().resolveHandler(this);
+ if (requestHandler == null) {
+ throw new BindingNotFoundException(uri);
+ }
+ requestHandler = new ProxyRequestHandler(requestHandler);
+ ContentChannel content = requestHandler.handleRequest(this, responseHandler);
+ if (content == null) {
+ throw new RequestDeniedException(this);
+ }
+ return content;
+ }
+ catch (Throwable t) {
+ cancel();
+ throw t;
+ }
+ }
+
+ @Override
+ protected void destroy() {
+ if (parentReference != null) {
+ parentReference.close();
+ }
+ if (container != null) {
+ container.release();
+ }
+ }
+
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/ResourceReference.java b/jdisc_core/src/main/java/com/yahoo/jdisc/ResourceReference.java
new file mode 100644
index 00000000000..d004846d5b8
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/ResourceReference.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc;
+
+/**
+ * <p>Represents a live reference to a {@link SharedResource}. Only provides the ability to release the reference.</p>
+ *
+ * <p>Implements {@link AutoCloseable} so that it can be used in try-with-resources statements. Example</p>
+ * <pre>
+ * void doSomethingWithRequest(final Request request) {
+ * try (final ResourceReference ref = request.refer()) {
+ * // Do something with request
+ * }
+ * // ref.close() will be called automatically on exit from the try block, releasing the reference on 'request'.
+ * }
+ * </pre>
+ *
+ * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+ */
+public interface ResourceReference extends AutoCloseable {
+
+ /**
+ * <p>Decrements the reference count of the referenced resource.
+ * You call this method once you are done using an object
+ * that you have previously {@link SharedResource#refer() referred}.</p>
+ *
+ * <p>Note that this method is NOT idempotent; you must call it exactly once.</p>
+ *
+ * @see SharedResource#refer()
+ */
+ @Override
+ void close();
+
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/Response.java b/jdisc_core/src/main/java/com/yahoo/jdisc/Response.java
new file mode 100644
index 00000000000..809805fdcc4
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/Response.java
@@ -0,0 +1,220 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc;
+
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.handler.ResponseDispatch;
+import com.yahoo.jdisc.handler.ResponseHandler;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * <p>This class represents the single response (which may have any content model that a {@link RequestHandler} chooses
+ * to implement) of some single request. Contrary to the {@link Request} class, this has no active reference to the
+ * parent {@link Container} (this is tracked internally by counting the number of requests vs the number of responses
+ * seen). The {@link ResponseHandler} of a Response is implicit in the invocation of {@link
+ * RequestHandler#handleRequest(Request, ResponseHandler)}.</p>
+ *
+ * <p>The usage pattern of the Response is similar to that of the Request in that the {@link ResponseHandler} returns a
+ * {@link ContentChannel} into which to write the Response content.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ * @see Request
+ * @see ResponseHandler
+ */
+public class Response {
+
+ /**
+ * <p>This interface acts as a namespace for the built-in status codes of the jDISC core. These are identical to the
+ * common HTTP status codes (see <a href="http://www.rfc-editor.org/rfc/rfc2616.txt">RFC2616</a>).</p>
+ */
+ public interface Status {
+
+ /**
+ * <p>1xx: Informational - Request received, continuing process.</p>
+ */
+ int CONTINUE = 100;
+ int SWITCHING_PROTOCOLS = 101;
+ int PROCESSING = 102;
+
+ /**
+ * <p>2xx: Success - The action was successfully received, understood, and accepted.</p>
+ */
+ int OK = 200;
+ int CREATED = 201;
+ int ACCEPTED = 202;
+ int NON_AUTHORITATIVE_INFORMATION = 203;
+ int NO_CONTENT = 204;
+ int RESET_CONTENT = 205;
+ int PARTIAL_CONTENT = 206;
+ int MULTI_STATUS = 207;
+
+ /**
+ * <p>3xx: Redirection - Further action must be taken in order to complete the request.</p>
+ */
+ int MULTIPLE_CHOICES = 300;
+ int MOVED_PERMANENTLY = 301;
+ int FOUND = 302;
+ int SEE_OTHER = 303;
+ int NOT_MODIFIED = 304;
+ int USE_PROXY = 305;
+ int TEMPORARY_REDIRECT = 307;
+
+ /**
+ * <p>4xx: Client Error - The request contains bad syntax or cannot be fulfilled.</p>
+ */
+ int BAD_REQUEST = 400;
+ int UNAUTHORIZED = 401;
+ int PAYMENT_REQUIRED = 402;
+ int FORBIDDEN = 403;
+ int NOT_FOUND = 404;
+ int METHOD_NOT_ALLOWED = 405;
+ int NOT_ACCEPTABLE = 406;
+ int PROXY_AUTHENTICATION_REQUIRED = 407;
+ int REQUEST_TIMEOUT = 408;
+ int CONFLICT = 409;
+ int GONE = 410;
+ int LENGTH_REQUIRED = 411;
+ int PRECONDITION_FAILED = 412;
+ int REQUEST_TOO_LONG = 413;
+ int REQUEST_URI_TOO_LONG = 414;
+ int UNSUPPORTED_MEDIA_TYPE = 415;
+ int REQUESTED_RANGE_NOT_SATISFIABLE = 416;
+ int EXPECTATION_FAILED = 417;
+ int INSUFFICIENT_SPACE_ON_RESOURCE = 419;
+ int METHOD_FAILURE = 420;
+ int UNPROCESSABLE_ENTITY = 422;
+ int LOCKED = 423;
+ int FAILED_DEPENDENCY = 424;
+
+ /**
+ * <p>5xx: Server Error - The server failed to fulfill an apparently valid request.</p>
+ */
+ int INTERNAL_SERVER_ERROR = 500;
+ int NOT_IMPLEMENTED = 501;
+ int BAD_GATEWAY = 502;
+ int SERVICE_UNAVAILABLE = 503;
+ int GATEWAY_TIMEOUT = 504;
+ int VERSION_NOT_SUPPORTED = 505;
+ int INSUFFICIENT_STORAGE = 507;
+ }
+
+ private final Map<String, Object> context = new HashMap<>();
+ private final HeaderFields headers = new HeaderFields();
+ private Throwable error;
+ private int status;
+
+ /**
+ * <p>Creates a new instance of this class.</p>
+ *
+ * @param status The status code to assign to this.
+ */
+ public Response(int status) {
+ this(status, null);
+ }
+
+ /**
+ * <p>Creates a new instance of this class.</p>
+ *
+ * @param status The status code to assign to this.
+ * @param error The error to assign to this.
+ */
+ public Response(int status, Throwable error) {
+ this.status = status;
+ this.error = error;
+ }
+
+ /**
+ * <p>Returns the named application context objects. This data is not intended for network transport, rather they
+ * are intended for passing shared data between components of an Application.</p>
+ *
+ * <p>Modifying the context map is a thread-unsafe operation -- any changes made after calling {@link
+ * ResponseHandler#handleResponse(Response)} might never become visible to other threads, and might throw
+ * ConcurrentModificationExceptions in other threads.</p>
+ *
+ * @return The context map.
+ */
+ public Map<String, Object> context() {
+ return context;
+ }
+
+ /**
+ * <p>Returns the set of header fields of this Request. These are the meta-data of the Request, and are not applied
+ * to any internal {@link Container} logic. Modifying headers is a thread-unsafe operation -- any changes made after
+ * calling {@link ResponseHandler#handleResponse(Response)} might never become visible to other threads, and might
+ * throw ConcurrentModificationExceptions in other threads.</p>
+ *
+ * @return The header fields.
+ */
+ public HeaderFields headers() {
+ return headers;
+ }
+
+ /**
+ * <p>Returns the status code of this response. This is an integer result code of the attempt to understand and
+ * satisfy the corresponding {@link Request}. It is encouraged, although not enforced, to use the built-in {@link
+ * Status} codes whenever possible.</p>
+ *
+ * @return The status code.
+ * @see #setStatus(int)
+ */
+ public int getStatus() {
+ return status;
+ }
+
+ /**
+ * <p>Sets the status code of this response. This is an integer result code of the attempt to understand and
+ * satisfy the corresponding {@link Request}. It is encouraged, although not enforced, to use the built-in {@link
+ * Status} codes whenever possible. </p>
+ *
+ * <p>Because access to this field is not guarded by any lock, any changes made after calling {@link
+ * ResponseHandler#handleResponse(Response)} might never become visible to other threads.</p>
+ *
+ * @param status The status code to set.
+ * @return This, to allow chaining.
+ * @see #getStatus()
+ */
+ public Response setStatus(int status) {
+ this.status = status;
+ return this;
+ }
+
+ /**
+ * <p>Returns the error of this response, or null if none has been set. This is typically non-null if the status
+ * indicates an unsuccessful response.</p>
+ *
+ * @return The error.
+ * @see #getError()
+ */
+ public Throwable getError() {
+ return error;
+ }
+
+ /**
+ * <p>Sets the error of this response. It is encouraged, although not enforced, to use this field to attach
+ * additional information to an unsuccessful response.</p>
+ *
+ * <p>Because access to this field is not guarded by any lock, any changes made after calling {@link
+ * ResponseHandler#handleResponse(Response)} might never become visible to other threads.</p>
+ *
+ * @param error The error to set.
+ * @return This, to allow chaining.
+ * @see #getError()
+ */
+ public Response setError(Throwable error) {
+ this.error = error;
+ return this;
+ }
+
+ /**
+ * <p>This is a convenience method for creating a Response with status {@link Status#REQUEST_TIMEOUT} and passing
+ * that to the given {@link ResponseHandler#handleResponse(Response)}. For trivial implementations of {@link
+ * RequestHandler#handleTimeout(Request, ResponseHandler)}, simply call this method.</p>
+ *
+ * @param handler The handler to pass the timeout {@link Response} to.
+ */
+ public static void dispatchTimeout(ResponseHandler handler) {
+ ResponseDispatch.newInstance(Status.REQUEST_TIMEOUT).dispatch(handler);
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/SharedResource.java b/jdisc_core/src/main/java/com/yahoo/jdisc/SharedResource.java
new file mode 100644
index 00000000000..4552ba3fe3a
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/SharedResource.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc;
+
+import com.yahoo.jdisc.application.ContainerActivator;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.application.DeactivatedContainer;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.service.ClientProvider;
+import com.yahoo.jdisc.service.ServerProvider;
+
+/**
+ * <p>This interface defines a reference counted resource. This is the parent interface of {@link RequestHandler},
+ * {@link ClientProvider} and {@link ServerProvider}, and is used by jDISC to appropriately signal resources as they
+ * become candidates for deallocation. As a {@link ContainerBuilder} is {@link
+ * ContainerActivator#activateContainer(ContainerBuilder) activated}, all its components are {@link #refer() retained}
+ * by that {@link Container}. Once a {@link DeactivatedContainer} terminates, all of that Container's components are
+ * {@link ResourceReference#close() released}. This resource tracking allows an Application to implement a significantly
+ * simpler scheme for managing its resources than would otherwise be possible.</p>
+ *
+ * <p>Objects are created with an initial reference count of 1, representing the reference held by the object creator.
+ *
+ * <p>You should not really think about the management of resources in terms of reference counting, instead think of it
+ * in terms of resource ownership. You retain a resource to prevent it from being destroyed while you are using it, and
+ * you release a resource once you are done using it.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface SharedResource {
+ public static final String SYSTEM_PROPERTY_NAME_DEBUG = "jdisc.debug.resources";
+ public static final boolean DEBUG = Boolean.valueOf(System.getProperty(SYSTEM_PROPERTY_NAME_DEBUG));
+
+ /**
+ * <p>Increments the reference count of this resource. You call this method to prevent an object from being
+ * destroyed until you have finished using it.</p>
+ *
+ * <p>You MUST keep the returned {@link ResourceReference} object and release the reference by calling
+ * {@link ResourceReference#close()} on it. A reference created by this method can NOT be released by calling
+ * {@link #release()}.</p>
+ *
+ * @see ResourceReference#close()
+ */
+ ResourceReference refer();
+
+ /**
+ * <p>Releases the "main" reference to this resource (the implicit reference due to creation of the object).</p>
+ *
+ * <p>References obtained by calling {@link #refer()} must be released by calling {@link ResourceReference#close()}
+ * on the {@link ResourceReference} returned from {@link #refer()}, NOT by calling this method. You call this
+ * method once you are done using an object that you have previously caused instantiation of.</p>
+ *
+ * @see ResourceReference
+ */
+ void release();
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/TimeoutManager.java b/jdisc_core/src/main/java/com/yahoo/jdisc/TimeoutManager.java
new file mode 100644
index 00000000000..4bca8136b8f
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/TimeoutManager.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc;
+
+import com.yahoo.jdisc.handler.RequestHandler;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * <p>This interface provides a callback for when the {@link Request#setTimeout(long, TimeUnit)} is invoked. If no such
+ * handler is registered at the time where the target {@link RequestHandler} is called, the default timeout manager will
+ * be injected.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public interface TimeoutManager {
+
+ /**
+ * Schedule timeout management for a request.
+ * This is called by a request whenever {@link Request#setTimeout(long, TimeUnit)} is invoked;
+ * this may be called multiple times for the same {@link Request}.
+ *
+ * @param request the request whose timeout to schedule
+ */
+ public void scheduleTimeout(Request request);
+
+ /**
+ * Unschedule timeout management for a previously scheduled request.
+ * This is called whenever a request is cancelled, and the purpose is to free up
+ * resources taken by the implementation of this associated with the request.
+ * <p>
+ * This is only called once for a request, and only after at least one scheduleTimeout call.
+ * <p>
+ * The default implementation of this does nothing.
+ *
+ * @param request the previously scheduled timeout
+ */
+ default public void unscheduleTimeout(Request request) {
+ }
+
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/Timer.java b/jdisc_core/src/main/java/com/yahoo/jdisc/Timer.java
new file mode 100644
index 00000000000..1c42221e735
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/Timer.java
@@ -0,0 +1,29 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc;
+
+import com.google.inject.ImplementedBy;
+import com.yahoo.jdisc.core.SystemTimer;
+
+/**
+ * <p>This class provides access to the current time in milliseconds, as viewed by the {@link Container}. Inject an
+ * instance of this class into any component that needs to access time, instead of using
+ * <code>System.currentTimeMillis()</code>.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+@ImplementedBy(SystemTimer.class)
+public interface Timer {
+
+ /**
+ * <p>Returns the current time in milliseconds. Note that while the unit of time of the return value is a
+ * millisecond, the granularity of the value depends on the underlying operating system and may be larger. For
+ * example, many operating systems measure time in units of tens of milliseconds.</p>
+ *
+ * <p> See the description of the class <code>Date</code> for a discussion of slight discrepancies that may arise
+ * between "computer time" and coordinated universal time (UTC).</p>
+ *
+ * @return The difference, measured in milliseconds, between the current time and midnight, January 1, 1970 UTC.
+ * @see java.util.Date
+ */
+ public long currentTimeMillis();
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/AbstractApplication.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/AbstractApplication.java
new file mode 100644
index 00000000000..240ee605174
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/AbstractApplication.java
@@ -0,0 +1,108 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import com.google.inject.Inject;
+import com.yahoo.jdisc.service.CurrentContainer;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleException;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * <p>This class is a convenient parent class for {@link Application} developers that require simple access to the most
+ * commonly used jDISC APIs.</p>
+ *
+ * <p>A simple hello world application could be implemented like this:</p>
+ * <pre>
+ * class HelloApplication extends AbstractApplication {
+ *
+ * &#64;Inject
+ * public HelloApplication(BundleInstaller bundleInstaller, ContainerActivator activator,
+ * CurrentContainer container) {
+ * super(bundleInstaller, activator, container);
+ * }
+ *
+ * &#64;Override
+ * public void start() {
+ * ContainerBuilder builder = newContainerBuilder();
+ * ServerProvider myServer = new MyHttpServer();
+ * builder.serverProviders().install(myServer);
+ * builder.serverBindings().bind("http://&#42;/&#42;", new MyHelloWorldHandler());
+ *
+ * activateContainer(builder);
+ * myServer.start();
+ * myServer.release();
+ * }
+ * }
+ * </pre>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public abstract class AbstractApplication implements Application {
+
+ private final CountDownLatch destroyed = new CountDownLatch(1);
+ private final BundleInstaller bundleInstaller;
+ private final ContainerActivator activator;
+ private final CurrentContainer container;
+
+ @Inject
+ protected AbstractApplication(BundleInstaller bundleInstaller, ContainerActivator activator,
+ CurrentContainer container) {
+ this.bundleInstaller = bundleInstaller;
+ this.activator = activator;
+ this.container = container;
+ }
+
+ @Override
+ public void stop() {
+
+ }
+
+ @Override
+ public final void destroy() {
+ destroyed.countDown();
+ }
+
+ public final List<Bundle> installAndStartBundle(String... locations) throws BundleException {
+ return installAndStartBundle(Arrays.asList(locations));
+ }
+
+ public final List<Bundle> installAndStartBundle(Iterable<String> locations) throws BundleException {
+ return bundleInstaller.installAndStart(locations);
+ }
+
+ public final void stopAndUninstallBundle(Bundle... bundles) throws BundleException {
+ stopAndUninstallBundle(Arrays.asList(bundles));
+ }
+
+ public final void stopAndUninstallBundle(Iterable<Bundle> bundles) throws BundleException {
+ bundleInstaller.stopAndUninstall(bundles);
+ }
+
+ public final ContainerBuilder newContainerBuilder() {
+ return activator.newContainerBuilder();
+ }
+
+ public final DeactivatedContainer activateContainer(ContainerBuilder builder) {
+ return activator.activateContainer(builder);
+ }
+
+ public final CurrentContainer container() {
+ return container;
+ }
+
+ public final boolean isTerminated() {
+ return destroyed.getCount() == 0;
+ }
+
+ public final boolean awaitTermination(int timeout, TimeUnit unit) throws InterruptedException {
+ return destroyed.await(timeout, unit);
+ }
+
+ public final void awaitTermination() throws InterruptedException {
+ destroyed.await();
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/Application.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/Application.java
new file mode 100644
index 00000000000..f70e3c90884
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/Application.java
@@ -0,0 +1,42 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.core.ApplicationLoader;
+import com.yahoo.jdisc.service.ClientProvider;
+import com.yahoo.jdisc.service.ServerProvider;
+
+/**
+ * <p>This interface defines the API of the singleton Application that runs in a jDISC instance. An Application instance
+ * will always have its {@link #destroy()} method called, regardless of whether {@link #start()} or {@link #stop()}
+ * threw any exceptions.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface Application {
+
+ /**
+ * <p>This method is called by the {@link ApplicationLoader} just after creating this Application instance. Use this
+ * method to start the Application's worker thread, and to activate a {@link Container}. If you attempt to call
+ * {@link ContainerActivator#activateContainer(ContainerBuilder)} before this method is invoked, that call will
+ * throw an {@link ApplicationNotReadyException}. If this method does not throw an exception, the {@link #stop()}
+ * method will be called at some time in the future.</p>
+ */
+ void start();
+
+ /**
+ * <p>This method is called by the {@link ApplicationLoader} after the corresponding signal has been issued by the
+ * controlling start script. Once this method returns, all calls to {@link
+ * ContainerActivator#activateContainer(ContainerBuilder)} will throw {@link ApplicationNotReadyException}s. Use
+ * this method to prepare for termination (see {@link #destroy()}).</p>
+ */
+ void stop();
+
+ /**
+ * <p>This method is called by the {@link ApplicationLoader} after first calling {@link #stop()}, and all previous
+ * {@link DeactivatedContainer}s have terminated. Use this method to shut down all Application components such as
+ * {@link ClientProvider}s and {@link ServerProvider}s.</p>
+ */
+ void destroy();
+
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/ApplicationNotReadyException.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ApplicationNotReadyException.java
new file mode 100644
index 00000000000..fbd5f1b00c6
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ApplicationNotReadyException.java
@@ -0,0 +1,19 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+/**
+ * This exception is used to signal that no {@link Application} has been configured. An instance of this class will be
+ * thrown by the {@link ContainerActivator#activateContainer(ContainerBuilder)} method if it is called before the call
+ * to {@link Application#start()} or after the call to {@link Application#stop()}.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class ApplicationNotReadyException extends RuntimeException {
+
+ /**
+ * Constructs a new instance of this class with a detail message.
+ */
+ public ApplicationNotReadyException() {
+ super("Application not ready.");
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingMatch.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingMatch.java
new file mode 100644
index 00000000000..679fb52f0e7
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingMatch.java
@@ -0,0 +1,64 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import java.net.URI;
+import java.util.Objects;
+
+/**
+ * <p>This class holds the result of a {@link BindingSet#match(URI)} operation. It contains methods to inspect the
+ * groups captured during matching, where a <em>group</em> is defined as a sequence of characters matches by a wildcard
+ * in the {@link UriPattern}, and to retrieve the matched target.</p>
+ *
+ * @param <T> The class of the target.
+ */
+public class BindingMatch<T> {
+
+ private final UriPattern.Match match;
+ private final T target;
+
+ /**
+ * <p>Constructs a new instance of this class.</p>
+ *
+ * @param match The match information for this instance.
+ * @param target The target of this match.
+ * @throws NullPointerException If any argument is null.
+ */
+ public BindingMatch(UriPattern.Match match, T target) {
+ Objects.requireNonNull(match, "match");
+ Objects.requireNonNull(target, "target");
+ this.match = match;
+ this.target = target;
+ }
+
+ /**
+ * <p>Returns the number of captured groups of this match. Any non-negative integer smaller than the value returned
+ * by this method is a valid group index for this match.</p>
+ *
+ * @return The number of captured groups.
+ */
+ public int groupCount() {
+ return match.groupCount();
+ }
+
+ /**
+ * <p>Returns the input subsequence captured by the given group by this match. Groups are indexed from left to
+ * right, starting at zero. Note that some groups may match an empty string, in which case this method returns the
+ * empty string. This method never returns null.</p>
+ *
+ * @param idx The index of the group to return.
+ * @return The (possibly empty) substring captured by the group during matching, never <tt>null</tt>.
+ * @throws IndexOutOfBoundsException If there is no group in the match with the given index.
+ */
+ public String group(int idx) {
+ return match.group(idx);
+ }
+
+ /**
+ * <p>Returns the matched target.</p>
+ *
+ * @return The matched target.
+ */
+ public T target() {
+ return target;
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingRepository.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingRepository.java
new file mode 100644
index 00000000000..75d687eb619
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingRepository.java
@@ -0,0 +1,110 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.handler.RequestHandler;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.logging.Logger;
+
+/**
+ * <p>This is a mutable repository of bindings from {@link UriPattern}s to some target type T. The {@link
+ * ContainerBuilder} has a mapping of named instances of this class for {@link RequestHandler}s, and is used to
+ * configure the set of {@link BindingSet}s that eventually become part of the active {@link Container}.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class BindingRepository<T> implements Iterable<Map.Entry<UriPattern, T>> {
+
+ private static final Logger log = Logger.getLogger(BindingRepository.class.getName());
+
+ private final Map<UriPattern, T> bindings = new HashMap<>();
+
+ /**
+ * <p>Creates a {@link UriPattern} from the given pattern string, and calls {@link #put(UriPattern, Object)}.</p>
+ *
+ * @param uriPattern The URI pattern to parse and bind to the target.
+ * @param target The target to assign to the URI pattern.
+ * @throws NullPointerException If any argument is null.
+ * @throws IllegalArgumentException If the URI pattern string could not be parsed.
+ */
+ public void bind(String uriPattern, T target) {
+ put(new UriPattern(uriPattern), target);
+ }
+
+ /**
+ * <p>Convenient method for calling {@link #bind(String, Object)} for all entries in a collection of bindings.</p>
+ *
+ * @param bindings The collection of bindings to copy to this.
+ * @throws NullPointerException If argument is null or contains null.
+ */
+ public void bindAll(Map<String, T> bindings) {
+ for (Map.Entry<String, T> entry : bindings.entrySet()) {
+ bind(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * <p>Binds the given target to the given {@link UriPattern}. Although all bindings will eventually be evaluated by
+ * a call to {@link BindingSet#resolve(URI)}, where matching order is significant, the order in which bindings are
+ * added is NOT. Instead, the creation of the {@link BindingSet} in {@link #activate()} sorts the bindings in such a
+ * way that the more strict patterns are evaluated first. See class-level commentary on {@link UriPattern} for more
+ * on this.
+ *
+ * @param uriPattern The URI pattern to parse and bind to the target.
+ * @param target The target to assign to the URI pattern.
+ * @throws NullPointerException If any argument is null.
+ * @throws IllegalArgumentException If the pattern has already been bound to another target.
+ */
+ public void put(UriPattern uriPattern, T target) {
+ Objects.requireNonNull(uriPattern, "uriPattern");
+ Objects.requireNonNull(target, "target");
+ if (bindings.containsKey(uriPattern)) {
+ T boundTarget = bindings.get(uriPattern);
+ log.info("Pattern '" + uriPattern + "' was already bound to target of class " + boundTarget.getClass().getName()
+ + ", and will NOT be bound to target of class " + target.getClass().getName());
+ } else {
+ bindings.put(uriPattern, target);
+ }
+ }
+
+ /**
+ * <p>Convenient method for calling {@link #put(UriPattern, Object)} for all entries in a collection of
+ * bindings.</p>
+ *
+ * @param bindings The collection of bindings to copy to this.
+ * @throws NullPointerException If argument is null or contains null.
+ */
+ public void putAll(Iterable<Map.Entry<UriPattern, T>> bindings) {
+ for (Map.Entry<UriPattern, T> entry : bindings) {
+ put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * <p>Creates and returns an immutable {@link BindingSet} that contains the bindings of this BindingRepository.
+ * Notice that the BindingSet uses a snapshot of the current bindings so that this repository remains mutable and
+ * reusable.</p>
+ *
+ * @return The created BindingSet instance.
+ */
+ public BindingSet<T> activate() {
+ return new BindingSet<>(bindings.entrySet());
+ }
+
+ /**
+ * Removes all bindings from this repository.
+ */
+ public void clear() {
+ bindings.clear();
+ }
+
+ @Override
+ public Iterator<Map.Entry<UriPattern, T>> iterator() {
+ return bindings.entrySet().iterator();
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingSet.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingSet.java
new file mode 100644
index 00000000000..b14a832b1d4
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingSet.java
@@ -0,0 +1,84 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import com.google.common.collect.ImmutableList;
+
+import java.net.URI;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>This is an immutable set of ordered bindings from {@link UriPattern}s to some target type T. To create an instance
+ * of this class, you must 1) create a {@link BindingRepository}, 2) configure it using the {@link
+ * BindingRepository#bind(String, Object)} method, and finally 3) call {@link BindingRepository#activate()}.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class BindingSet<T> implements Iterable<Map.Entry<UriPattern, T>> {
+
+ public static final String DEFAULT = "default";
+ private final Collection<Map.Entry<UriPattern, T>> bindings;
+
+ BindingSet(Collection<Map.Entry<UriPattern, T>> bindings) {
+ this.bindings = sort(bindings);
+ }
+
+ /**
+ * <p>Resolves the binding that best matches (see commentary on {@link BindingRepository#bind(String, Object)}) the
+ * given {@link URI}, and returns a {@link BindingMatch} object that describes the match and contains the
+ * matched target. If there is no binding that matches the given URI, this method returns null.</p>
+ *
+ * @param uri The URI to match against the bindings in this set.
+ * @return A {@link BindingMatch} object describing the match found, or null if not found.
+ */
+ public BindingMatch<T> match(URI uri) {
+ for (Map.Entry<UriPattern, T> entry : bindings) {
+ UriPattern.Match match = entry.getKey().match(uri);
+ if (match != null) {
+ return new BindingMatch<>(match, entry.getValue());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * <p>Resolves the binding that best matches (see commentary on {@link BindingRepository#bind(String, Object)}) the
+ * given {@link URI}, and returns that target. If there is no binding that matches the given URI, this method
+ * returns null.</p>
+ *
+ * <p>Apart from a <em>null</em>-guard, this is equal to <code>return match(uri).target()</code>.</p>
+ *
+ * @param uri The URI to match against the bindings in this set.
+ * @return The best matched target, or null.
+ * @see #match(URI)
+ */
+ public T resolve(URI uri) {
+ BindingMatch<T> match = match(uri);
+ if (match == null) {
+ return null;
+ }
+ return match.target();
+ }
+
+ @Override
+ public Iterator<Map.Entry<UriPattern, T>> iterator() {
+ return bindings.iterator();
+ }
+
+ private static <T> Collection<Map.Entry<UriPattern, T>> sort(Collection<Map.Entry<UriPattern, T>> unsorted) {
+ List<Map.Entry<UriPattern, T>> ret = new LinkedList<>(unsorted);
+ Collections.sort(ret, new Comparator<Map.Entry<UriPattern, ?>>() {
+
+ @Override
+ public int compare(Map.Entry<UriPattern, ?> lhs, Map.Entry<UriPattern, ?> rhs) {
+ return lhs.getKey().compareTo(rhs.getKey());
+ }
+ });
+ return ImmutableList.copyOf(ret);
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingSetSelector.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingSetSelector.java
new file mode 100644
index 00000000000..a480d3968c9
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BindingSetSelector.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import com.google.inject.ImplementedBy;
+import com.google.inject.Module;
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.core.DefaultBindingSelector;
+import com.yahoo.jdisc.service.CurrentContainer;
+import com.yahoo.jdisc.service.NoBindingSetSelectedException;
+
+import java.net.URI;
+
+/**
+ * This interface defines the component that is used by the {@link CurrentContainer} to assign a {@link BindingSet} to a
+ * newly created {@link Container} based on the given {@link URI}. The default implementation of this interface returns
+ * {@link BindingSet#DEFAULT} regardless of input. To specify your own selector you need to {@link
+ * GuiceRepository#install(Module) install} a Guice {@link Module} that provides a binding for this interface.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+@ImplementedBy(DefaultBindingSelector.class)
+public interface BindingSetSelector {
+
+ /**
+ * Returns the name of the {@link BindingSet} to assign to the {@link Container} for the given {@link URI}. If this
+ * method returns <em>null</em>, the corresponding call to {@link CurrentContainer#newReference(URI)} will throw a
+ * {@link NoBindingSetSelectedException}.
+ *
+ * @param uri The URI to select on.
+ * @return The name of selected BindingSet.
+ */
+ public String select(URI uri);
+} \ No newline at end of file
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/BundleInstallationException.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BundleInstallationException.java
new file mode 100644
index 00000000000..deb0f4554ff
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BundleInstallationException.java
@@ -0,0 +1,34 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import com.google.common.collect.ImmutableList;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleException;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * <p>This exception is thrown by {@link OsgiFramework#installBundle(String)} if installation failed. Because </p>
+ *
+ * <p>Please see commentary on {@link OsgiFramework#installBundle(String)} and {@link
+ * OsgiFramework#startBundles(java.util.List, boolean)} for a description of exception-safety issues to consider when
+ * installing bundles that use the {@link OsgiHeader#PREINSTALL_BUNDLE} manifest instruction.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public final class BundleInstallationException extends BundleException {
+
+ private final List<Bundle> installedBundles;
+
+ public BundleInstallationException(Collection<Bundle> installedBundles, Throwable cause) {
+ super(cause.getMessage(), cause);
+ this.installedBundles = ImmutableList.copyOf(installedBundles);
+ }
+
+ public List<Bundle> installedBundles() {
+ return installedBundles;
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/BundleInstaller.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BundleInstaller.java
new file mode 100644
index 00000000000..273d29e8dfb
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/BundleInstaller.java
@@ -0,0 +1,86 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import com.google.inject.Inject;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleException;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * <p>This is a utility class to help with installing, starting, stopping and uninstalling OSGi Bundles. You can choose
+ * to inject an instance of this class, or it can be created explicitly by reference to a {@link OsgiFramework}.</p>
+ *
+ * <p>Please see commentary on {@link OsgiFramework#installBundle(String)} for a description of exception-safety issues
+ * to consider when installing bundles that use the {@link OsgiHeader#PREINSTALL_BUNDLE} manifest instruction.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class BundleInstaller {
+
+ private final OsgiFramework osgiFramework;
+
+ @Inject
+ public BundleInstaller(OsgiFramework osgiFramework) {
+ this.osgiFramework = osgiFramework;
+ }
+
+ public List<Bundle> installAndStart(String... locations) throws BundleException {
+ return installAndStart(Arrays.asList(locations));
+ }
+
+ public List<Bundle> installAndStart(Iterable<String> locations) throws BundleException {
+ List<Bundle> bundles = new LinkedList<>();
+ try {
+ for (String location : locations) {
+ bundles.addAll(osgiFramework.installBundle(location));
+ }
+ } catch (BundleInstallationException e) {
+ bundles.addAll(e.installedBundles());
+ throw new BundleInstallationException(bundles, e);
+ } catch (Exception e) {
+ throw new BundleInstallationException(bundles, e);
+ }
+ try {
+ for (Bundle bundle : bundles) {
+ start(bundle);
+ }
+ } catch (Exception e) {
+ throw new BundleInstallationException(bundles, e);
+ }
+ return bundles;
+ }
+
+ public void stopAndUninstall(Bundle... bundles) throws BundleException {
+ stopAndUninstall(Arrays.asList(bundles));
+ }
+
+ public void stopAndUninstall(Iterable<Bundle> bundles) throws BundleException {
+ for (Bundle bundle : bundles) {
+ stop(bundle);
+ }
+ for (Bundle bundle : bundles) {
+ bundle.uninstall();
+ }
+ }
+
+ private void start(Bundle bundle) throws BundleException {
+ if (bundle.getState() == Bundle.ACTIVE) {
+ throw new BundleException("OSGi bundle " + bundle.getSymbolicName() + " already started.");
+ }
+ if (!OsgiHeader.asList(bundle, OsgiHeader.APPLICATION).isEmpty()) {
+ throw new BundleException("OSGi header '" + OsgiHeader.APPLICATION + "' not allowed for " +
+ "non-application bundle " + bundle.getSymbolicName() + ".");
+ }
+ osgiFramework.startBundles(Arrays.asList(bundle), false);
+ }
+
+ private void stop(Bundle bundle) throws BundleException {
+ if (bundle.getState() != Bundle.ACTIVE) {
+ throw new BundleException("OSGi bundle " + bundle.getSymbolicName() + " not started.");
+ }
+ bundle.stop();
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerActivator.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerActivator.java
new file mode 100644
index 00000000000..105ce5c8d0f
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerActivator.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import com.yahoo.jdisc.Container;
+
+/**
+ * <p>This interface defines the API for changing the active {@link Container} of a jDISC application. An instance of
+ * this class is typically injected into the {@link Application} constructor. If injection is unavailable due to an
+ * Application design, an instance of this class is also available as an OSGi service under the full ContainerActivator
+ * class name.</p>
+ *
+ * <p>This interface allows one to create and active a new Container. To do so, one has to 1) call {@link
+ * #newContainerBuilder()}, 2) configure the returned {@link ContainerBuilder}, and 3) pass the builder to the {@link
+ * #activateContainer(ContainerBuilder)} method.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface ContainerActivator {
+
+ /**
+ * <p>This method creates and returns a new {@link ContainerBuilder} object that has the necessary references to the
+ * application and its internal components.</p>
+ *
+ * @return The created builder.
+ */
+ public ContainerBuilder newContainerBuilder();
+
+ /**
+ * <p>Creates and activates a {@link Container} based on the provided {@link ContainerBuilder}. By providing a
+ * <em>null</em> argument, this method can be used to deactivate the current Container. The returned object can be
+ * used to schedule a cleanup task that is executed once the the deactivated Container has terminated.</p>
+ *
+ * @param builder The builder to activate.
+ * @return The previous container, if any.
+ * @throws ApplicationNotReadyException If this method is called before {@link Application#start()} or after {@link
+ * Application#stop()}.
+ */
+ public DeactivatedContainer activateContainer(ContainerBuilder builder);
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerBuilder.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerBuilder.java
new file mode 100644
index 00000000000..f3b1e03b30f
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerBuilder.java
@@ -0,0 +1,133 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.AbstractModule;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.handler.RequestHandler;
+
+import java.util.*;
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * <p>This is the inactive, mutable {@link Container}. Because it requires references to the application internals, it
+ * should always be injected by guice or created by calling {@link ContainerActivator#newContainerBuilder()}. Once the
+ * builder has been configured, it is activated by calling {@link
+ * ContainerActivator#activateContainer(ContainerBuilder)}. You may use the {@link #setAppContext(Object)} method to
+ * attach an arbitrary object to a Container, which will be available in the corresponding {@link
+ * DeactivatedContainer}.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ContainerBuilder {
+
+ private final GuiceRepository guiceModules = new GuiceRepository();
+ private final ServerRepository serverProviders = new ServerRepository(guiceModules);
+ private final Map<String, BindingRepository<RequestHandler>> serverBindings = new HashMap<>();
+ private final Map<String, BindingRepository<RequestHandler>> clientBindings = new HashMap<>();
+ private Object appContext = null;
+
+ public ContainerBuilder(Iterable<Module> guiceModules) {
+ this.guiceModules.installAll(guiceModules);
+ this.guiceModules.install(new AbstractModule() {
+
+ @Override
+ public void configure() {
+ bind(ContainerBuilder.class).toInstance(ContainerBuilder.this);
+ }
+ });
+ this.serverBindings.put(BindingSet.DEFAULT, new BindingRepository<RequestHandler>());
+ this.clientBindings.put(BindingSet.DEFAULT, new BindingRepository<RequestHandler>());
+ }
+
+ public void setAppContext(Object ctx) {
+ appContext = ctx;
+ }
+
+ public Object appContext() {
+ return appContext;
+ }
+
+ public GuiceRepository guiceModules() {
+ return guiceModules;
+ }
+
+ public <T> T getInstance(Key<T> key) {
+ return guiceModules.getInstance(key);
+ }
+
+ public <T> T getInstance(Class<T> type) {
+ return guiceModules.getInstance(type);
+ }
+
+ public ServerRepository serverProviders() {
+ return serverProviders;
+ }
+
+ public BindingRepository<RequestHandler> serverBindings() {
+ return serverBindings.get(BindingSet.DEFAULT);
+ }
+
+ public BindingRepository<RequestHandler> serverBindings(String setName) {
+ BindingRepository<RequestHandler> ret = serverBindings.get(setName);
+ if (ret == null) {
+ ret = new BindingRepository<>();
+ serverBindings.put(setName, ret);
+ }
+ return ret;
+ }
+
+ public Map<String, BindingSet<RequestHandler>> activateServerBindings() {
+ Map<String, BindingSet<RequestHandler>> ret = new HashMap<>();
+ for (Map.Entry<String, BindingRepository<RequestHandler>> entry : serverBindings.entrySet()) {
+ ret.put(entry.getKey(), entry.getValue().activate());
+ }
+ return ImmutableMap.copyOf(ret);
+ }
+
+ public BindingRepository<RequestHandler> clientBindings() {
+ return clientBindings.get(BindingSet.DEFAULT);
+ }
+
+ public BindingRepository<RequestHandler> clientBindings(String setName) {
+ BindingRepository<RequestHandler> ret = clientBindings.get(setName);
+ if (ret == null) {
+ ret = new BindingRepository<>();
+ clientBindings.put(setName, ret);
+ }
+ return ret;
+ }
+
+ public Map<String, BindingSet<RequestHandler>> activateClientBindings() {
+ Map<String, BindingSet<RequestHandler>> ret = new HashMap<>();
+ for (Map.Entry<String, BindingRepository<RequestHandler>> entry : clientBindings.entrySet()) {
+ ret.put(entry.getKey(), entry.getValue().activate());
+ }
+ return ImmutableMap.copyOf(ret);
+ }
+
+ @SuppressWarnings({ "unchecked" })
+ public static <T> Class<T> safeClassCast(Class<T> baseClass, Class<?> someClass) {
+ if (!baseClass.isAssignableFrom(someClass)) {
+ throw new IllegalArgumentException("Expected " + baseClass.getName() + ", got " +
+ someClass.getName() + ".");
+ }
+ return (Class<T>)someClass;
+ }
+
+ public static List<String> safeStringSplit(Object obj, String delim) {
+ if (!(obj instanceof String)) {
+ return Collections.emptyList();
+ }
+ List<String> lst = new LinkedList<>();
+ for (String str : ((String)obj).split(delim)) {
+ str = str.trim();
+ if (!str.isEmpty()) {
+ lst.add(str);
+ }
+ }
+ return lst;
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerThread.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerThread.java
new file mode 100644
index 00000000000..38527acc099
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ContainerThread.java
@@ -0,0 +1,60 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * <p>This class decorates {@link Thread} to allow for internal jDISC optimizations. Whenever possible a jDISC
+ * application should use this class instead of Thread. The {@link ContainerThread.Factory} class is a helper-class for
+ * working with the {@link Executors} framework.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ContainerThread extends Thread {
+
+ private final MetricConsumer consumer;
+
+ /**
+ * <p>Allocates a new ContainerThread object. This constructor calls the parent {@link Thread#Thread(Runnable)}
+ * constructor.</p>
+ *
+ * @param target The object whose <code>run</code> method is called.
+ * @param consumer The MetricConsumer of this thread.
+ */
+ public ContainerThread(Runnable target, MetricConsumer consumer) {
+ super(target);
+ this.consumer = consumer;
+ }
+
+ /**
+ * <p>Returns the {@link MetricConsumer} of this. Note that this may be null.</p>
+ *
+ * @return The MetricConsumer of this, or null.
+ */
+ public MetricConsumer consumer() {
+ return consumer;
+ }
+
+ /**
+ * <p>This class implements the {@link ThreadFactory} interface on top of a {@link Provider} for {@link
+ * MetricConsumer} instances.</p>
+ */
+ public static class Factory implements ThreadFactory {
+
+ private final Provider<MetricConsumer> provider;
+
+ @Inject
+ public Factory(Provider<MetricConsumer> provider) {
+ this.provider = provider;
+ }
+
+ @Override
+ public Thread newThread(Runnable target) {
+ return new ContainerThread(target, provider.get());
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/DeactivatedContainer.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/DeactivatedContainer.java
new file mode 100644
index 00000000000..5f43a8644e6
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/DeactivatedContainer.java
@@ -0,0 +1,38 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.handler.ContentChannel;
+
+/**
+ * <p>This interface represents a {@link Container} which has been deactivated. An instance of this class is returned by
+ * the {@link ContainerActivator#activateContainer(ContainerBuilder)} method, and is used to schedule a cleanup task
+ * that is executed once the the deactivated Container has terminated.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface DeactivatedContainer {
+
+ /**
+ * <p>Returns the context object that was previously attached to the corresponding {@link ContainerBuilder} through
+ * the {@link ContainerBuilder#setAppContext(Object)} method. This is useful for tracking {@link Application}
+ * specific resources that are to be tracked alongside a {@link Container}.</p>
+ *
+ * @return The Application context.
+ */
+ Object appContext();
+
+ /**
+ * <p>Schedules the given {@link Runnable} to execute once this DeactivatedContainer has terminated. A
+ * DeactivatedContainer is considered to have terminated once there are no more {@link Request}s, {@link Response}s
+ * or corresponding {@link ContentChannel}s being processed by components that belong to it.</p>
+ *
+ * <p>If termination has already occured, this method immediately runs the given Runnable in the current thread.</p>
+ *
+ * @param task The task to run once this DeactivatedContainer has terminated.
+ */
+ void notifyTermination(Runnable task);
+
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/GlobPattern.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/GlobPattern.java
new file mode 100644
index 00000000000..101825328b4
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/GlobPattern.java
@@ -0,0 +1,191 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+class GlobPattern implements Comparable<GlobPattern> {
+
+ private static final GlobPattern WILDCARD = new WildcardPattern();
+ protected final String[] parts;
+
+ private GlobPattern(String... parts) {
+ this.parts = parts;
+ }
+
+ public final Match match(String text) {
+ return match(text, 0);
+ }
+
+ public Match match(String text, int offset) {
+ int[] pos = new int[parts.length - 1 << 1];
+ if (!matches(text, offset, 0, pos)) {
+ return null;
+ }
+ return new Match(text, pos);
+ }
+
+ private boolean matches(String text, int textIdx, int partIdx, int[] out) {
+ String part = parts[partIdx];
+ if (partIdx == parts.length - 1 && part.isEmpty()) {
+ out[partIdx - 1 << 1 | 1] = text.length();
+ return true; // optimize trailing wildcard
+ }
+ int partEnd = textIdx + part.length();
+ if (partEnd > text.length()|| !text.startsWith(part, textIdx)) {
+ return false;
+ }
+ if (partIdx == parts.length - 1) {
+ return partEnd == text.length();
+ }
+ out[partIdx << 1] = partEnd;
+ for (int i = partEnd; i <= text.length(); ++i) {
+ out[partIdx << 1 | 1] = i;
+ if (matches(text, i, partIdx + 1, out)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public int compareTo(GlobPattern rhs) {
+ // wildcard pattern always orders last
+ if (parts.length == 0 || rhs.parts.length == 0) {
+ return rhs.parts.length - parts.length;
+ }
+ // next is trailing wildcard
+ int cmp = compare(parts[parts.length - 1], rhs.parts[rhs.parts.length - 1], false);
+ if (cmp != 0) {
+ return cmp;
+ }
+ // then comes part comparison
+ for (int i = 0; i < parts.length && i < rhs.parts.length; ++i) {
+ cmp = compare(parts[i], rhs.parts[i], true);
+ if (cmp != 0) {
+ return cmp;
+ }
+ }
+ // one starts with the other, sort longest first
+ return rhs.parts.length - parts.length;
+ }
+
+ private static int compare(String lhs, String rhs, boolean compareNonEmpty) {
+ if ((lhs.isEmpty() || rhs.isEmpty()) && !lhs.equals(rhs)) {
+ return rhs.length() - lhs.length();
+ }
+ if (!compareNonEmpty) {
+ return 0;
+ }
+ return rhs.compareTo(lhs);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof GlobPattern)) {
+ return false;
+ }
+ GlobPattern rhs = (GlobPattern)obj;
+ if (!Arrays.equals(parts, rhs.parts)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(parts);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder ret = new StringBuilder();
+ for (int i = 0; i < parts.length; ++i) {
+ ret.append(parts[i]);
+ if (i < parts.length - 1) {
+ ret.append("*");
+ }
+ }
+ return ret.toString();
+ }
+
+ public static Match match(String glob, String text) {
+ return compile(glob).match(text);
+ }
+
+ public static GlobPattern compile(String pattern) {
+ if (pattern.equals("*")) {
+ return WILDCARD;
+ }
+ if (pattern.indexOf('*') < 0) {
+ return new VerbatimPattern(pattern);
+ }
+ List<String> arr = new LinkedList<>();
+ for (int prev = 0, next = 0; next <= pattern.length(); ++next) {
+ if (next == pattern.length() || pattern.charAt(next) == '*') {
+ arr.add(pattern.substring(prev, next));
+ prev = next + 1;
+ }
+ }
+ return new GlobPattern(arr.toArray(new String[arr.size()]));
+ }
+
+ public static class Match {
+
+ private final String str;
+ private final int[] pos;
+
+ private Match(String str, int[] pos) {
+ this.str = str;
+ this.pos = pos;
+ }
+
+ public int groupCount() {
+ return pos.length >> 1;
+ }
+
+ public String group(int idx) {
+ return str.substring(pos[idx << 1], pos[idx << 1 | 1]);
+ }
+ }
+
+ private static class VerbatimPattern extends GlobPattern {
+
+ VerbatimPattern(String value) {
+ super(value);
+ }
+
+ @Override
+ public Match match(String text, int offset) {
+ int len = text.length() - offset;
+ if (len != parts[0].length()) {
+ return null;
+ }
+ if (!parts[0].regionMatches(0, text, offset, len)) {
+ return null;
+ }
+ return new Match(parts[0], new int[0]);
+ }
+ }
+
+ private static class WildcardPattern extends GlobPattern {
+
+ @Override
+ public Match match(String text, int offset) {
+ int len = text.length();
+ if (len <= offset) {
+ return new Match(text, new int[] { 0, 0 });
+ }
+ return new Match(text, new int[] { offset, len });
+ }
+
+ @Override
+ public String toString() {
+ return "*";
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/GuiceRepository.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/GuiceRepository.java
new file mode 100644
index 00000000000..9412d51bb49
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/GuiceRepository.java
@@ -0,0 +1,127 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.*;
+import com.google.inject.spi.DefaultElementVisitor;
+import com.google.inject.spi.Element;
+import com.google.inject.spi.Elements;
+import com.yahoo.jdisc.Container;
+import org.osgi.framework.Bundle;
+
+import java.util.*;
+import java.util.logging.Logger;
+
+/**
+ * This is a repository of {@link Module}s. An instance of this class is owned by the {@link ContainerBuilder}, and is
+ * used to configure the set of Modules that eventually form the {@link Injector} of the active {@link Container}.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class GuiceRepository implements Iterable<Module> {
+
+ private static final Logger log = Logger.getLogger(GuiceRepository.class.getName());
+ private final Map<Module, List<Element>> modules = new LinkedHashMap<>();
+ private Injector injector;
+
+ public GuiceRepository(Module... modules) {
+ installAll(Arrays.asList(modules));
+ }
+
+ public Injector activate() {
+ return getInjector();
+ }
+
+ public List<Module> installAll(Bundle bundle, Iterable<String> moduleNames) throws ClassNotFoundException {
+ List<Module> lst = new LinkedList<>();
+ for (String moduleName : moduleNames) {
+ lst.add(install(bundle, moduleName));
+ }
+ return lst;
+ }
+
+ public Module install(Bundle bundle, String moduleName) throws ClassNotFoundException {
+ log.finer("Installing Guice module '" + moduleName + "'.");
+ Class<?> namedClass = bundle.loadClass(moduleName);
+ Class<Module> moduleClass = ContainerBuilder.safeClassCast(Module.class, namedClass);
+ Module module = getInstance(moduleClass);
+ install(module);
+ return module;
+ }
+
+ public void installAll(Iterable<? extends Module> modules) {
+ for (Module module : modules) {
+ install(module);
+ }
+ }
+
+ public void install(Module module) {
+ modules.put(module, Elements.getElements(module));
+ injector = null;
+ }
+
+ public void uninstallAll(Iterable<? extends Module> modules) {
+ for (Module module : modules) {
+ uninstall(module);
+ }
+ }
+
+ public void uninstall(Module module) {
+ modules.remove(module);
+ injector = null;
+ }
+
+ public Injector getInjector() {
+ if (injector == null) {
+ injector = Guice.createInjector(createModule());
+ }
+ return injector;
+ }
+
+ public <T> T getInstance(Key<T> key) {
+ return getInjector().getInstance(key);
+ }
+
+ public <T> T getInstance(Class<T> type) {
+ return getInjector().getInstance(type);
+ }
+
+ public Collection<Module> collection() { return ImmutableSet.copyOf(modules.keySet()); }
+
+ @Override
+ public Iterator<Module> iterator() {
+ return collection().iterator();
+ }
+
+ private Module createModule() {
+ List<Element> allElements = new LinkedList<>();
+ for (List<Element> moduleElements : modules.values()) {
+ allElements.addAll(moduleElements);
+ }
+ ElementCollector collector = new ElementCollector();
+ for (ListIterator<Element> it = allElements.listIterator(allElements.size()); it.hasPrevious(); ) {
+ it.previous().acceptVisitor(collector);
+ }
+ return Elements.getModule(collector.elements);
+ }
+
+ private static class ElementCollector extends DefaultElementVisitor<Boolean> {
+
+ final Set<Key<?>> seenKeys = new HashSet<>();
+ final List<Element> elements = new LinkedList<>();
+
+ @Override
+ public <T> Boolean visit(Binding<T> binding) {
+ if (seenKeys.add(binding.getKey())) {
+ elements.add(binding);
+ }
+ return Boolean.TRUE;
+ }
+
+ @Override
+ public Boolean visitOther(Element element) {
+ elements.add(element);
+ return Boolean.TRUE;
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricConsumer.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricConsumer.java
new file mode 100644
index 00000000000..d057321565c
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricConsumer.java
@@ -0,0 +1,68 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import com.google.inject.ProvidedBy;
+import com.google.inject.Provider;
+import com.yahoo.jdisc.Metric;
+
+import java.util.Map;
+
+/**
+ * <p>This interface defines the consumer counterpart of the {@link Metric} interface. All Metric objects contain their
+ * own thread local instance of this interface, so most implementations will require a registry of sorts to manage the
+ * aggregation of state across MetricConsumers.</p>
+ *
+ * <p>An {@link Application} needs to bind a {@link Provider} of this interface to an implementation, or else all calls
+ * to the Metric objects become no-ops. An implementation will look similar to:</p>
+ *
+ * <pre>
+ * private final MyMetricRegistry myMetricRegistry = new MyMetricRegistry();
+ * void createContainer() {
+ * ContainerBuilder builder = containerActivator.newContainerBuilder();
+ * builder.guice().install(new MyGuiceModule());
+ * (...)
+ * }
+ * class MyGuiceModule extends com.google.inject.AbstractModule {
+ * void configure() {
+ * bind(MetricConsumer.class).toProvider(myMetricRegistry);
+ * (...)
+ * }
+ * }
+ * class MyMetricRegistry implements com.google.inject.Provider&lt;MetricConsumer&gt; {
+ * (...)
+ * }
+ * </pre>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+@ProvidedBy(MetricNullProvider.class)
+public interface MetricConsumer {
+
+ /**
+ * <p>Consume a call to <tt>Metric.set(String, Number, Metric.Context)</tt>.</p>
+ *
+ * @param key The name of the metric to modify.
+ * @param val The value to assign to the named metric.
+ * @param ctx The context to further describe this entry.
+ */
+ public void set(String key, Number val, Metric.Context ctx);
+
+ /**
+ * <p>Consume a call to <tt>Metric.add(String, Number, Metric.Context)</tt>.</p>
+ *
+ * @param key The name of the metric to modify.
+ * @param val The value to add to the named metric.
+ * @param ctx The context to further describe this entry.
+ */
+ public void add(String key, Number val, Metric.Context ctx);
+
+ /**
+ * <p>Creates a <tt>Metric.Context</tt> object that encapsulates the given properties. The returned Context object
+ * will be passed along every future call to <tt>set(String, Number, Metric.Context)</tt> and
+ * <tt>add(String, Number, Metric.Context)</tt> where the properties match those given here.</p>
+ *
+ * @param properties The properties to incorporate in the context.
+ * @return The created context.
+ */
+ public Metric.Context createContext(Map<String, ?> properties);
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricImpl.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricImpl.java
new file mode 100644
index 00000000000..8fab60429d0
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricImpl.java
@@ -0,0 +1,68 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.yahoo.jdisc.Metric;
+
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+class MetricImpl implements Metric {
+
+ private final LocalConsumer consumer;
+
+ @Inject
+ public MetricImpl(Provider<MetricConsumer> provider) {
+ consumer = new LocalConsumer(provider);
+ }
+
+ @Override
+ public void set(String key, Number val, Context ctx) {
+ MetricConsumer consumer = currentConsumer();
+ if (consumer != null) {
+ consumer.set(key, val, ctx);
+ }
+ }
+
+ @Override
+ public void add(String key, Number val, Context ctx) {
+ MetricConsumer consumer = currentConsumer();
+ if (consumer != null) {
+ consumer.add(key, val, ctx);
+ }
+ }
+
+ @Override
+ public Context createContext(Map<String, ?> keys) {
+ MetricConsumer consumer = currentConsumer();
+ if (consumer == null) {
+ return null;
+ }
+ return consumer.createContext(keys);
+ }
+
+ private MetricConsumer currentConsumer() {
+ Thread thread = Thread.currentThread();
+ if (thread instanceof ContainerThread) {
+ return ((ContainerThread)thread).consumer();
+ }
+ return consumer.get();
+ }
+
+ private static class LocalConsumer extends ThreadLocal<MetricConsumer> {
+
+ final Provider<MetricConsumer> factory;
+
+ LocalConsumer(Provider<MetricConsumer> factory) {
+ this.factory = factory;
+ }
+
+ @Override
+ protected MetricConsumer initialValue() {
+ return factory.get();
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricNullProvider.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricNullProvider.java
new file mode 100644
index 00000000000..e23ccbc95c0
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricNullProvider.java
@@ -0,0 +1,15 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import com.google.inject.Provider;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+class MetricNullProvider implements Provider<MetricConsumer> {
+
+ @Override
+ public MetricConsumer get() {
+ return null;
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricProvider.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricProvider.java
new file mode 100644
index 00000000000..51fdcdd1c87
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/MetricProvider.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.yahoo.jdisc.Metric;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class MetricProvider implements Provider<Metric> {
+
+ private final Provider<MetricConsumer> consumerProvider;
+
+ @Inject
+ public MetricProvider(Provider<MetricConsumer> consumerProvider) {
+ this.consumerProvider = consumerProvider;
+ }
+
+ @Override
+ public Metric get() {
+ return new MetricImpl(consumerProvider);
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/OsgiFramework.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/OsgiFramework.java
new file mode 100644
index 00000000000..615b36fef1f
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/OsgiFramework.java
@@ -0,0 +1,99 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+
+import java.util.List;
+
+/**
+ * <p>This is an abstraction of the OSGi framework that hides the actual implementation details. If you need access to
+ * this interface, simply inject it into your Application. In most cases, however, you are better of injecting a
+ * {@link BundleInstaller} since that provides common convenience methods.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface OsgiFramework {
+
+ /**
+ * <p>Installs a bundle from the specified location. The specified location identifier will be used as the identity
+ * of the bundle. If a bundle containing the same location identifier is already installed, the <tt>Bundle</tt>
+ * object for that bundle is returned. All bundles listed in the {@link OsgiHeader#PREINSTALL_BUNDLE} manifest
+ * header are also installed. The bundle at index 0 of the returned list matches the <tt>bundleLocation</tt>
+ * argument.</p>
+ *
+ * <p><b>NOTE:</b> When this method installs more than one bundle, <em>AND</em> one of those bundles throw an
+ * exception during installation, the bundles installed prior to throwing the expcetion will remain installed. To
+ * enable the caller to recover from such a situation, this method wraps any thrown exception within a {@link
+ * BundleInstallationException} that contains the list of successfully installed bundles.</p>
+ *
+ * <p>It would be preferable if this method was exception-safe (that it would roll-back all installed bundles in the
+ * case of an exception), but that can not be implemented thread-safely since an <tt>Application</tt> may choose to
+ * install bundles concurrently through any available <tt>BundleContext</tt>.</p>
+ *
+ * @param bundleLocation The location identifier of the bundle to install.
+ * @return The list of Bundle objects installed, the object at index 0 matches the given location.
+ * @throws BundleInstallationException If the input stream cannot be read, or the installation of a bundle failed,
+ * or the caller does not have the appropriate permissions, or the system {@link
+ * BundleContext} is no longer valid.
+ */
+ public List<Bundle> installBundle(String bundleLocation) throws BundleException;
+
+ /**
+ * <p>Starts the given {@link Bundle}s. The parameter <tt>privileged</tt> tells the framework whether or not
+ * privileges are available, and is checked against the {@link OsgiHeader#PRIVILEGED_ACTIVATOR} header of each
+ * Bundle being started. Any bundle that is a fragment is silently ignored.</p>
+ *
+ * @param bundles The bundles to start.
+ * @param privileged Whether or not privileges are available.
+ * @throws BundleException If a bundle could not be started. This could be because a code dependency could not
+ * be resolved or the specified BundleActivator could not be loaded or threw an
+ * exception.
+ * @throws SecurityException If the caller does not have the appropriate permissions.
+ * @throws IllegalStateException If this bundle has been uninstalled or this bundle tries to change its own state.
+ */
+ public void startBundles(List<Bundle> bundles, boolean privileged) throws BundleException;
+
+ /**
+ * <p>This method <em>synchronously</em> refreshes all bundles currently loaded. Once this method returns, the
+ * class loaders of all bundles will reflect on the current set of loaded bundles.</p>
+ */
+ public void refreshPackages();
+
+ /**
+ * <p>Returns the BundleContext of this framework's system bundle. The returned BundleContext can be used by the
+ * caller to act on behalf of this bundle. This method may return <tt>null</tt> if it has no valid
+ * BundleContext.</p>
+ *
+ * @return A <tt>BundleContext</tt> for the system bundle, or <tt>null</tt>.
+ * @throws SecurityException If the caller does not have the appropriate permissions.
+ * @since 2.0
+ */
+ public BundleContext bundleContext();
+
+ /**
+ * <p>Returns an iterable collection of all installed bundles. This method returns a list of all bundles installed
+ * in the OSGi environment at the time of the call to this method. However, since the OsgiFramework is a very
+ * dynamic environment, bundles can be installed or uninstalled at anytime.</p>
+ *
+ * @return An iterable collection of Bundle objects, one object per installed bundle.
+ */
+ public List<Bundle> bundles();
+
+ /**
+ * <p>This method starts the framework instance. Before this method is called, any call to {@link
+ * #installBundle(String)} or {@link #bundles()} will generate a {@link NullPointerException}.</p>
+ *
+ * @throws BundleException If any error occurs.
+ */
+ public void start() throws BundleException;
+
+ /**
+ * <p>This method <em>synchronously</em> shuts down the framework. It must be called at the end of a session in
+ * order to shutdown all active bundles.</p>
+ *
+ * @throws BundleException If any error occurs.
+ */
+ public void stop() throws BundleException;
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/OsgiHeader.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/OsgiHeader.java
new file mode 100644
index 00000000000..524b23808e0
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/OsgiHeader.java
@@ -0,0 +1,41 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import org.osgi.framework.Bundle;
+
+import java.util.List;
+
+/**
+ * This interface acts as a namespace for the supported OSGi bundle headers.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public abstract class OsgiHeader {
+
+ public static final String APPLICATION = "X-JDisc-Application";
+ public static final String PREINSTALL_BUNDLE = "X-JDisc-Preinstall-Bundle";
+ public static final String PRIVILEGED_ACTIVATOR = "X-JDisc-Privileged-Activator";
+
+ /**
+ * Returns true if the named header is present in the manifest of the given bundle.
+ *
+ * @param bundle The bundle whose manifest to check.
+ * @param headerName The name of the header to check for.
+ * @return True if header is present.
+ */
+ public static boolean isSet(Bundle bundle, String headerName) {
+ return Boolean.valueOf(String.valueOf(bundle.getHeaders().get(headerName)));
+ }
+
+ /**
+ * This method reads the named header from the manifest of the given bundle, and parses it as a comma-separated list
+ * of values. If the header is not set, this method returns an empty list.
+ *
+ * @param bundle The bundle whose manifest to parse the header from.
+ * @param headerName The name of the header to parse.
+ * @return A list of parsed header values, may be empty.
+ */
+ public static List<String> asList(Bundle bundle, String headerName) {
+ return ContainerBuilder.safeStringSplit(bundle.getHeaders().get(headerName), ",");
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/ResourcePool.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ResourcePool.java
new file mode 100644
index 00000000000..4d62377d461
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ResourcePool.java
@@ -0,0 +1,169 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import com.google.inject.Key;
+import com.yahoo.jdisc.AbstractResource;
+import com.yahoo.jdisc.ResourceReference;
+import com.yahoo.jdisc.SharedResource;
+import com.yahoo.jdisc.References;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * <p>This is a utility class to help manage {@link SharedResource}s while configuring a {@link ContainerBuilder}. This
+ * class can still be used without a ContainerBuilder, albeit with the injection APIs (i.e. {@link #get(Class)} and
+ * {@link #get(com.google.inject.Key)}) disabled.</p>
+ * <p>The core problem with SharedResources is that they need to be tracked carefully to ensure exception safety in the
+ * code that creates and registers them with a ContainerBuilder. The code for this typically looks like this:</p>
+ * <pre>
+ * MyServerProvider serverProvider = null;
+ * MyRequestHandler requestHandler = null;
+ * try {
+ * serverProvider = builder.getInstance(MyServerProvider.class);
+ * serverProvider.start();
+ * containerBuilder.serverProviders().install(serverProvider);
+ *
+ * requestHandler = builder.getInstance(MyRequestHandler.class);
+ * containerBuilder.serverBindings().bind("http://host/path", requestHandler);
+ *
+ * containerActivator.activateContainer(containerBuilder);
+ * } finally {
+ * if (serverProvider != null) {
+ * serverProvider.release();
+ * }
+ * if (requestHandler != null) {
+ * requestHandler.release();
+ * }
+ * }
+ * </pre>
+ *
+ * <p>The ResourcePool helps remove the boiler-plate code used to track the resources from outside the try-finally
+ * block. Using the ResourcePool, the above snippet can be rewritten to the following:</p>
+ * <pre>
+ * try (ResourcePool resources = new ResourcePool(containerBuilder)) {
+ * ServerProvider serverProvider = resources.get(MyServerProvider.class);
+ * serverProvider.start();
+ * containerBuilder.serverProviders().install(serverProvider);
+ *
+ * RequestHandler requestHandler = resources.get(MyRequestHandler.class);
+ * containerBuilder.serverBindings().bind("http://host/path", requestHandler);
+ *
+ * containerActivator.activateContainer(containerBuilder);
+ * }
+ * </pre>
+ *
+ * <p>This class is not thread-safe.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public final class ResourcePool extends AbstractResource implements AutoCloseable {
+
+ private final List<ResourceReference> resources = new ArrayList<>();
+ private final ContainerBuilder builder;
+
+ /**
+ * <p>Creates a new instance of this class without a backing {@link ContainerBuilder}. A ResourcePool created with
+ * this constructor will throw a NullPointerException if either {@link #get(Class)} or {@link #get(Key)} is
+ * called.</p>
+ */
+ public ResourcePool() {
+ this(null);
+ }
+
+ /**
+ * <p>Creates a new instance of this class. All calls to {@link #get(Class)} and {@link #get(Key)} are forwarded to
+ * the {@link ContainerBuilder} given to this constructor.</p>
+ *
+ * @param builder The ContainerBuilder that provides the injection functionality for this ResourcePool.
+ */
+ public ResourcePool(ContainerBuilder builder) {
+ this.builder = builder;
+ }
+
+ /**
+ * <p>Adds the given {@link SharedResource} to this ResourcePool. Note that this DOES NOT call {@link
+ * SharedResource#refer()}, as opposed to {@link #retain(SharedResource)}. When this ResourcePool is
+ * destroyed, it will release the main reference to the resource (by calling {@link SharedResource#release()}).</p>
+ *
+ * @param t The SharedResource to add.
+ * @param <T> The class of parameter <tt>t</tt>.
+ * @return The parameter <tt>t</tt>, to allow inlined calls to this function.
+ */
+ public <T extends SharedResource> T add(T t) {
+ try {
+ resources.add(References.fromResource(t));
+ } catch (IllegalStateException e) {
+ // Ignore. TODO(bakksjo): Don't rely on ISE to detect duplicates; handle that in this class instead.
+ }
+ return t;
+ }
+
+ /**
+ * <p>Returns the appropriate instance for the given injection key. Note that this DOES NOT call {@link
+ * SharedResource#refer()}. This is the equivalent of doing:</p>
+ * <pre>
+ * t = containerBuilder.getInstance(key);
+ * resourcePool.add(t);
+ * </pre>
+ *
+ * <p>When this ResourcePool is destroyed, it will release the main reference to the resource
+ * (by calling {@link SharedResource#release()}).</p>
+ *
+ * @param key The injection key to return.
+ * @param <T> The class of the injection type.
+ * @return The appropriate instance of T.
+ * @throws NullPointerException If this pool was constructed without a ContainerBuilder.
+ */
+ public <T extends SharedResource> T get(Key<T> key) {
+ return add(builder.getInstance(key));
+ }
+
+ /**
+ * <p>Returns the appropriate instance for the given injection type. Note that this DOES NOT call {@link
+ * SharedResource#refer()}. This is the equivalent of doing:</p>
+ * <pre>
+ * t = containerBuilder.getInstance(type);
+ * resourcePool.add(t);
+ * </pre>
+ *
+ * <p>When this ResourcePool is destroyed, it will release the main reference to the resource
+ * (by calling {@link SharedResource#release()}).</p>
+ *
+ * @param type The injection type to return.
+ * @param <T> The class of the injection type.
+ * @return The appropriate instance of T.
+ * @throws NullPointerException If this pool was constructed without a ContainerBuilder.
+ */
+ public <T extends SharedResource> T get(Class<T> type) {
+ return add(builder.getInstance(type));
+ }
+
+ /**
+ * <p>Retains and adds the given {@link SharedResource} to this ResourcePool. Note that this DOES call {@link
+ * SharedResource#refer()}, as opposed to {@link #add(SharedResource)}.
+ *
+ * <p>When this ResourcePool is destroyed, it will release the resource reference returned by the
+ * {@link SharedResource#refer()} call.</p>
+ *
+ * @param t The SharedResource to retain and add.
+ * @param <T> The class of parameter <tt>t</tt>.
+ * @return The parameter <tt>t</tt>, to allow inlined calls to this function.
+ */
+ public <T extends SharedResource> T retain(T t) {
+ resources.add(t.refer());
+ return t;
+ }
+
+ @Override
+ protected void destroy() {
+ for (ResourceReference resource : resources) {
+ resource.close();
+ }
+ }
+
+ @Override
+ public void close() throws Exception {
+ release();
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/ServerRepository.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ServerRepository.java
new file mode 100644
index 00000000000..83aa8e7d9d7
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/ServerRepository.java
@@ -0,0 +1,75 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import com.google.common.collect.ImmutableList;
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.service.ServerProvider;
+import org.osgi.framework.Bundle;
+
+import java.util.*;
+import java.util.logging.Logger;
+
+/**
+ * This is a repository of {@link ServerProvider}s. An instance of this class is owned by the {@link ContainerBuilder},
+ * and is used to configure the set of ServerProviders that eventually become part of the active {@link Container}.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ServerRepository implements Iterable<ServerProvider> {
+
+ private static final Logger log = Logger.getLogger(ServerRepository.class.getName());
+ private final List<ServerProvider> servers = new LinkedList<>();
+ private final GuiceRepository guice;
+
+ public ServerRepository(GuiceRepository guice) {
+ this.guice = guice;
+ }
+
+ public Iterable<ServerProvider> activate() { return ImmutableList.copyOf(servers); }
+
+ public List<ServerProvider> installAll(Bundle bundle, Iterable<String> serverNames) throws ClassNotFoundException {
+ List<ServerProvider> lst = new LinkedList<>();
+ for (String serverName : serverNames) {
+ lst.add(install(bundle, serverName));
+ }
+ return lst;
+ }
+
+ public ServerProvider install(Bundle bundle, String serverName) throws ClassNotFoundException {
+ log.finer("Installing server provider '" + serverName + "'.");
+ Class<?> namedClass = bundle.loadClass(serverName);
+ Class<ServerProvider> serverClass = ContainerBuilder.safeClassCast(ServerProvider.class, namedClass);
+ ServerProvider server = guice.getInstance(serverClass);
+ install(server);
+ return server;
+ }
+
+ public void installAll(Iterable<? extends ServerProvider> servers) {
+ for (ServerProvider server : servers) {
+ install(server);
+ }
+ }
+
+ public void install(ServerProvider server) {
+ servers.add(server);
+ }
+
+ public void uninstallAll(Iterable<? extends ServerProvider> handlers) {
+ for (ServerProvider handler : handlers) {
+ uninstall(handler);
+ }
+ }
+
+ public void uninstall(ServerProvider handler) {
+ servers.remove(handler);
+ }
+
+ public Collection<ServerProvider> collection() {
+ return Collections.unmodifiableCollection(servers);
+ }
+
+ @Override
+ public Iterator<ServerProvider> iterator() {
+ return collection().iterator();
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/UriPattern.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/UriPattern.java
new file mode 100644
index 00000000000..6f587057c77
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/UriPattern.java
@@ -0,0 +1,217 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.application;
+
+import java.net.URI;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * <p>This class holds a regular expression designed so that it only matches certain {@link URI}s. The constructor of
+ * this class accepts a simplified pattern string, and turns that into something that can be used to quickly match
+ * against URIs. This class also implements {@link Comparable} in such a way that stricter patterns order before looser
+ * patterns.</p>
+ *
+ * <p>Here are some examples of ordering:</p>
+ * <ul>
+ * <li><code>http://host/path</code> evaluated before <code>*://host/path</code></li>
+ * <li><code>http://host/path</code> evaluated before <code>http://&#42;/path</code></li>
+ * <li><code>http://a.host/path</code> evaluated before <code>http://*.host/path</code></li>
+ * <li><code>http://*.host/path</code> evaluated before <code>http://host/path</code></li>
+ * <li><code>http://host.a/path</code> evaluated before <code>http://host.&#42;/path</code></li>
+ * <li><code>http://host.&#42;/path</code> evaluated before <code>http://host/path</code></li>
+ * <li><code>http://host:80/path</code> evaluated before <code>http://host:&#42;/path</code></li>
+ * <li><code>http://host/path</code> evaluated before <code>http://host/*</code></li>
+ * <li><code>http://host/path/*</code> evaluated before <code>http://host/path</code></li>
+ * </ul>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class UriPattern implements Comparable<UriPattern> {
+
+ public static final int DEFAULT_PRIORITY = 0;
+ private static final Pattern PATTERN = Pattern.compile("([^:]+)://([^:/]+)(:((\\*)|([0-9]+)))?/(.*)",
+ Pattern.UNICODE_CASE | Pattern.CANON_EQ);
+ private final String pattern;
+ private final GlobPattern scheme;
+ private final GlobPattern host;
+ private final int port;
+ private final GlobPattern path;
+ private final int priority;
+
+ /**
+ * <p>Creates a new instance of this class that represents the given pattern string, with a priority of <tt>0</tt>.
+ * The input string must be on the form <code>&lt;scheme&gt;://&lt;host&gt;[:&lt;port&gt;]&lt;path&gt;</code>, where
+ * '*' can be used as a wildcard character at any position.</p>
+ *
+ * @param uri The pattern to parse.
+ * @throws IllegalArgumentException If the pattern could not be parsed.
+ */
+ public UriPattern(String uri) {
+ this(uri, DEFAULT_PRIORITY);
+ }
+
+ /**
+ * <p>Creates a new instance of this class that represents the given pattern string, with the given priority. The
+ * input string must be on the form <code>&lt;scheme&gt;://&lt;host&gt;[:&lt;port&gt;]&lt;path&gt;</code>, where
+ * '*' can be used as a wildcard character at any position.</p>
+ *
+ * @param uri The pattern to parse.
+ * @param priority The priority of this pattern.
+ * @throws IllegalArgumentException If the pattern could not be parsed.
+ */
+ public UriPattern(String uri, int priority) {
+ Matcher matcher = PATTERN.matcher(uri);
+ if (!matcher.find()) {
+ throw new IllegalArgumentException(uri);
+ }
+ scheme = GlobPattern.compile(resolvePatternComponent(matcher.group(1)));
+ host = GlobPattern.compile(resolvePatternComponent(matcher.group(2)));
+ port = resolvePortPattern(matcher.group(4));
+ path = GlobPattern.compile(resolvePatternComponent(matcher.group(7)));
+ pattern = scheme + "://" + host + ":" + (port > 0 ? port : "*") + "/" + path;
+ this.priority = priority;
+ }
+
+ /**
+ * <p>Attempts to match the given {@link URI} to this pattern. Note that only the scheme, host, port, and path
+ * components of the URI are used. Any query or fragment part is simply ignored.</p>
+ *
+ * @param uri The URI to match.
+ * @return A {@link Match} object describing the match found, or null if not found.
+ */
+ public Match match(URI uri) {
+ // Performance optimization: Match path first since scheme and host are often the same in a given binding repository.
+ String uriPath = resolveUriComponent(uri.getPath());
+ GlobPattern.Match pathMatch = path.match(uriPath, uriPath.startsWith("/") ? 1 : 0);
+ if (pathMatch == null) {
+ return null;
+ }
+ if (port > 0 && port != uri.getPort()) {
+ return null;
+ }
+ // Match scheme before host because it has a higher chance of differing (e.g. http versus https)
+ GlobPattern.Match schemeMatch = scheme.match(resolveUriComponent(uri.getScheme()));
+ if (schemeMatch == null) {
+ return null;
+ }
+ GlobPattern.Match hostMatch = host.match(resolveUriComponent(uri.getHost()));
+ if (hostMatch == null) {
+ return null;
+ }
+ return new Match(schemeMatch, hostMatch, port > 0 ? 0 : uri.getPort(), pathMatch);
+ }
+
+ @Override
+ public int hashCode() {
+ return pattern.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof UriPattern && pattern.equals(((UriPattern)obj).pattern);
+ }
+
+ @Override
+ public String toString() {
+ return pattern;
+ }
+
+ @Override
+ public int compareTo(UriPattern rhs) {
+ int cmp;
+ cmp = rhs.priority - priority;
+ if (cmp != 0) {
+ return cmp;
+ }
+ cmp = scheme.compareTo(rhs.scheme);
+ if (cmp != 0) {
+ return cmp;
+ }
+ cmp = host.compareTo(rhs.host);
+ if (cmp != 0) {
+ return cmp;
+ }
+ cmp = path.compareTo(rhs.path);
+ if (cmp != 0) {
+ return cmp;
+ }
+ cmp = rhs.port - port;
+ if (cmp != 0) {
+ return cmp;
+ }
+ return 0;
+ }
+
+ private static String resolveUriComponent(String str) {
+ return str != null ? str : "";
+ }
+
+ private static String resolvePatternComponent(String val) {
+ return val != null ? val : "*";
+ }
+
+ private static int resolvePortPattern(String str) {
+ if (str == null || str.equals("*")) {
+ return 0;
+ }
+ return Integer.parseInt(str);
+ }
+
+ /**
+ * <p>This class holds the result of a {@link UriPattern#match(URI)} operation. It contains methods to inspect the
+ * groups captured during matching, where a <em>group</em> is defined as a sequence of characters matches by a
+ * wildcard in the {@link UriPattern}.</p>
+ */
+ public static class Match {
+
+ private final GlobPattern.Match scheme;
+ private final GlobPattern.Match host;
+ private final int port;
+ private final GlobPattern.Match path;
+
+ private Match(GlobPattern.Match scheme, GlobPattern.Match host, int port, GlobPattern.Match path) {
+ this.scheme = scheme;
+ this.host = host;
+ this.port = port;
+ this.path = path;
+ }
+
+ /**
+ * <p>Returns the number of captured groups of this match. Any non-negative integer smaller than the value
+ * returned by this method is a valid group index for this match.</p>
+ *
+ * @return The number of captured groups.
+ */
+ public int groupCount() {
+ return scheme.groupCount() + host.groupCount() + (port > 0 ? 1 : 0) + path.groupCount();
+ }
+
+ /**
+ * <p>Returns the input subsequence captured by the given group by this match. Groups are indexed from left to
+ * right, starting at zero. Note that some groups may match an empty string, in which case this method returns
+ * the empty string. This method never returns null.</p>
+ *
+ * @param idx The index of the group to return.
+ * @return The (possibly empty) substring captured by the group during matching, never <tt>null</tt>.
+ * @throws IndexOutOfBoundsException If there is no group in the match with the given index.
+ */
+ public String group(int idx) {
+ int len = scheme.groupCount();
+ if (idx < len) {
+ return scheme.group(idx);
+ }
+ idx = idx - len;
+ len = host.groupCount();
+ if (idx < len) {
+ return host.group(idx);
+ }
+ idx = idx - len;
+ len = port > 0 ? 1 : 0;
+ if (idx < len) {
+ return String.valueOf(port);
+ }
+ idx = idx - len;
+ return path.group(idx);
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/application/package-info.java b/jdisc_core/src/main/java/com/yahoo/jdisc/application/package-info.java
new file mode 100644
index 00000000000..1e864cc4688
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/application/package-info.java
@@ -0,0 +1,152 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * <p>Provides classes and interfaces for implementing an {@link com.yahoo.jdisc.application.Application
+ * Application}.</p>
+ *
+ * <h3>Application</h3>
+ * <p>In every jDISC process there is exactly one Application instance, it is created during jDISC startup, and it is
+ * destroyed during jDISC shutdown. The Application uses the {@link com.yahoo.jdisc.application.ContainerBuilder
+ * ContainerBuilder} interface to load OSGi {@link org.osgi.framework.Bundle Bundles}, install Guice {@link
+ * com.google.inject.Module Modules}, create and start {@link com.yahoo.jdisc.service.ServerProvider ServerProviders},
+ * inject a {@link com.yahoo.jdisc.application.BindingSetSelector BindingSetSelector}, and configure {@link
+ * com.yahoo.jdisc.application.BindingRepository BindingSets} with {@link com.yahoo.jdisc.handler.RequestHandler
+ * RequestHandlers} and {@link com.yahoo.jdisc.service.ClientProvider ClientProviders}. Once the ContainerBuilder is
+ * appropriately configured, it is passed to the local {@link com.yahoo.jdisc.application.ContainerActivator} to perform
+ * an atomic switch from current to new {@link com.yahoo.jdisc.Container Container}.</p>
+ *
+<pre>
+&#64;Inject
+MyApplication(ContainerActivator activator) {
+ ContainerBuilder builder = activator.newContainerBuilder();
+ builder.guiceModules().install(new MyBindings());
+ Bundle bundle = builder.osgiBundles().install("file:$VESPA_HOME/lib/jars/jdisc_http.jar");
+ builder.serverProviders().install(bundle, "com.yahoo.disc.service.http.HttpServer");
+ builder.serverBindings().bind("http://localhost/admin/*", new MyAdminHandler());
+ builder.serverBindings().bind("http://localhost/*", new MyRequestHandler());
+ activator.activateContainer(builder);
+}
+</pre>
+ *
+ * <p>Because the {@link com.yahoo.jdisc.Request Request} owns a reference to the Container that was active on Request-
+ * construction, jDISC is able to guarantee that no component is shut down as long as there are pending Requests that
+ * can reach them. When activating a new Container, the previous Container is returned as a {@link
+ * com.yahoo.jdisc.application.DeactivatedContainer DeactivatedContainer} instance - an API that can be used by the
+ * Application to asynchronously wait for Container termination in order to completely shut down components that are no
+ * longer required. This activation pattern is used both for Application startup, runtime reconfigurations, as well as
+ * for Application shutdown. It allows all jDISC Application to continously serve Requests during reconfiguration,
+ * causing no down time other than what the Application itself explicitly enforces.</p>
+ *
+<pre>
+void reconfigureApplication() {
+ (...)
+ reconfiguredContainerBuilder.handlers().install(myRetainedClients);
+ reconfiguredContainerBuilder.servers().install(myRetainedServers);
+ myExpiredServers.close();
+ DeactivatedContainer deactivatedContainer = containerActivator.activateContainer(reconfiguredContainerBuilder);
+ deactivatedContainer.notifyTermination(new Runnable() {
+ void run() {
+ myExpiredClients.destroy();
+ myExpiredServers.destroy();
+ }
+ });
+}
+</pre>
+ *
+ * <h3>Application and OSGi</h3>
+ * <p>At the heart of jDISC is an OSGi framework. An Application is always packaged as an OSGi bundle. The OSGi
+ * technology itself is a set of specifications that define a dynamic component system for Java. These specifications
+ * enable a development model where applications are (dynamically) composed of many different (reusable) components. The
+ * OSGi specifications enable components to hide their implementations from other components while communicating through
+ * common interfaces (in our case, defined by jDISC's core API) or services (which are objects that are explicitly
+ * shared between components). Initially this framework is used to load and bootstrap the application from an OSGi
+ * bundle specified on deployment, but because it is exposed through the ContainerBuilder interface, an Application
+ * itself can load other bundles as required.</p>
+ *
+ * <p>The OSGi integration in jDISC adds the following manifest instructions:</p>
+ * <dl>
+ * <dt>X-JDisc-Privileged-Activator</dt>
+ * <dd>
+ * if "true", this tells jDISC that this bundle requires root privileges for its {@link
+ * org.osgi.framework.BundleActivator BundleActivator}. If privileges can not be provided, this bundle should not be
+ * installed. Only the Application bundle and its dependencies can ever be given privileges, as jDISC itself drops
+ * its privileges after the bootstrapping step.
+ * </dd>
+ * <dt>X-JDisc-Preinstall-Bundle</dt>
+ * <dd>
+ * a comma-separated list of bundle locations that must be installed prior to this. Because the named bundles are
+ * loaded through the same framework, all transitive dependencies are also resolved. This is an extension to the
+ * standard OSGi instruction "Require-Bundle" which simply states that this bundle requires another.
+ *
+ * It is fairly tricky to get this right during integration testing, since dependencies might be part of the build
+ * tree instead of being installed on the host. To facilitate this, JDisc will prefix any non-schemed location (e.g.
+ * "my_dependency.jar") with the system property "jdisc.bundle.path". This property defaults to the current
+ * directory when running inside an IDE, but is set to "$VESPA_HOME/lib/jars/" by the jdisc_start script.
+ *
+ * One may also reference system properties in a bundle location using the syntax "${propertyName}". If the property
+ * is not found, it defaults to an empty string.
+ * </dd>
+ * <dt>X-JDisc-Application</dt>
+ * <dd>
+ * the name of the Application class to load from the bundle. This instruction is ignored unless it is part of the
+ * first loaded bundle.
+ * </dd>
+ * </dl>
+ *
+ * <p>One of the benefits of using OSGi is that it provides Classloader isolation, meaning that one bundle can not
+ * inadvertently affect the inernals of another. jDISC leverages this to isolate the different implementations of
+ * RequestHandlers, ServerProviders, and jDISC's core internals.</p>
+ *
+ * <p>The OSGi manifest instruction "X-JDisc-Application" tells jDISC the name of the Application class to inject from
+ * the loaded bundle during startup. To this end, it is necessary for the named Application to offer an
+ * injection-enabled constructor (annotated with the <code>Inject</code> keyword). At a minimum, an Application
+ * typically needs to have the ContainerActivator injected and saved to a member variable. Because of jDISC's additional
+ * OSGi manifest instruction "X-JDisc-Preinstall-Bundle", an Application bundle can be built with compile-time
+ * dependencies on other OSGi bundles (using the "provided" scope in maven) without having to repack those dependency
+ * into the application itself. Unless incompatible API changes are made to 3rd party jDISC components, it should be
+ * possible to upgrade dependencies without having to recompile and redeploy the Application.</p>
+ *
+ * <h3>Application deployment</h3>
+ * <p>jDISC allows a single binary to execute any application without having to change the command line parameters.
+ * Instead of
+ * modifying the parameters of the single application binary, changing the application is achieved by setting a single
+ * environment variable. The planned method of deployment is therefore to 1) install the application's OSGi bundle,
+ * 2) set the necessary "jdisc.application" environment variable, and 3) restart the package.</p>
+ *
+<pre>
+$ install myapp_jar
+$ set jdisc.application="myapp.jar"
+$ restart jdisc
+</pre>
+ *
+ * For testing and development, the jDISC binary also supports command line parameters to start and stop a local
+ * application.
+ *
+<pre>
+$ install jdisc-dev
+$ emacs src/main/java/edu/disc/MyApplication.java
+$ mvn install
+$ sudo jdisc_start target/myapp.jar
+</pre>
+ *
+ * <p>It is the responsibility of the Application itself to create, configure
+ * and activate a Container instance. Although jDISC offers an API that allows for- and manages the change of an active
+ * Container instance, making the necessary calls to do so is also considered Application logic. When jDISC receives an
+ * external signal to shut down, it instructs the running Application to initiate a graceful shutdown, and waits for it
+ * to terminate. Any in-flight Requests should complete, and all services will close.</p>
+ *
+ * <p>Because jDISC runs as a Daemon it has the opportunity to run code with root privileges, and it can be configured
+ * to provide these privileges to an application's initialization code. However, 1) deployment-time configuration must
+ * explicitly enable this capability (by setting the environment variable "jdisc.privileged" to "true"), and 2) the
+ * application bundle must explicitly declare that it requires privileges (by including the manifest header
+ * "X-JDisc-Privileged-Activator" with the value "true"). If privileges are required but unavailable, deployment of the
+ * application will fail. Code that requires privileges will never be run WITHOUT privileges, and code that does not
+ * explicitly request privileges will never be run WITH privileges. Finally, the code snippet that is run with
+ * privileges is separate from the Application class to avoid unintentionally passing privileges to third-party
+ * code.</p>
+ *
+ * @see com.yahoo.jdisc
+ * @see com.yahoo.jdisc.handler
+ * @see com.yahoo.jdisc.service
+ */
+@com.yahoo.api.annotations.PublicApi
+package com.yahoo.jdisc.application;
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/client/AbstractClientApplication.java b/jdisc_core/src/main/java/com/yahoo/jdisc/client/AbstractClientApplication.java
new file mode 100644
index 00000000000..032384cb9ba
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/client/AbstractClientApplication.java
@@ -0,0 +1,55 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.client;
+
+import com.google.inject.Inject;
+import com.yahoo.jdisc.application.AbstractApplication;
+import com.yahoo.jdisc.application.BundleInstaller;
+import com.yahoo.jdisc.application.ContainerActivator;
+import com.yahoo.jdisc.service.CurrentContainer;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * <p>This is a convenient parent class for {@link ClientApplication} developers. It extends {@link AbstractApplication}
+ * and implements {@link Runnable} to wait for {@link #shutdown()} to be called. When using this class, you implement
+ * {@link #start()} (and optionally {@link #stop()}), and provide a reference to it to whatever component is responsible
+ * for signaling shutdown.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public abstract class AbstractClientApplication extends AbstractApplication implements ClientApplication {
+
+ private final CountDownLatch done = new CountDownLatch(1);
+
+ @Inject
+ public AbstractClientApplication(BundleInstaller bundleInstaller, ContainerActivator activator,
+ CurrentContainer container) {
+ super(bundleInstaller, activator, container);
+ }
+
+ @Override
+ public final void run() {
+ try {
+ done.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ public final void shutdown() {
+ done.countDown();
+ }
+
+ public final boolean isShutdown() {
+ return done.getCount() == 0;
+ }
+
+ public final boolean awaitShutdown(int timeout, TimeUnit unit) throws InterruptedException {
+ return done.await(timeout, unit);
+ }
+
+ public final void awaitShutdown() throws InterruptedException {
+ done.await();
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/client/ClientApplication.java b/jdisc_core/src/main/java/com/yahoo/jdisc/client/ClientApplication.java
new file mode 100644
index 00000000000..27e4a65b96f
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/client/ClientApplication.java
@@ -0,0 +1,16 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.client;
+
+import com.yahoo.jdisc.application.Application;
+
+/**
+ * <p>This interface extends the {@link Application} interface, and is intended to be used with the {@link ClientDriver}
+ * to implement stand-alone client applications on top of jDISC. The difference from Application is that this interface
+ * provides a {@link Runnable#run()} method that will be invoked once the Application has been created and {@link
+ * Application#start() started}. When run() returns, the {@link ClientDriver} will initiate Application shutdown.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface ClientApplication extends Application, Runnable {
+
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/client/ClientDriver.java b/jdisc_core/src/main/java/com/yahoo/jdisc/client/ClientDriver.java
new file mode 100644
index 00000000000..f06be2af155
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/client/ClientDriver.java
@@ -0,0 +1,133 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.client;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+import com.yahoo.jdisc.application.Application;
+import com.yahoo.jdisc.application.OsgiFramework;
+import com.yahoo.jdisc.core.ApplicationLoader;
+import com.yahoo.jdisc.core.FelixFramework;
+import com.yahoo.jdisc.core.FelixParams;
+import com.yahoo.jdisc.test.NonWorkingOsgiFramework;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * <p>This class provides a unified way to set up and run a {@link ClientApplication}. It provides you with a
+ * programmable interface to instantiate and run the whole jDISC framework as if it was started as a Daemon, and it
+ * provides you with a thread in which to run your application logic. Once your return from the {@link
+ * ClientApplication#run()} method, the ClientProvider will initiate {@link Application} shutdown.</p>
+ *
+ * <p>A ClientApplication is typically a self-contained JAR file that bundles all of its dependencies, and contains a
+ * single "main" method. The typical implementation of that method is:</p>
+ * <pre>
+ * public static void main(String[] args) throws Exception {
+ * ClientDriver.runApplication(MyApplication.class);
+ * }
+ * </pre>
+ *
+ * <p>Alternatively, the ClientApplication can be created up front:</p>
+ * <pre>
+ * public static void main(String[] args) throws Exception {
+ * MyApplication app = new MyApplication();
+ * (... configure app ...)
+ * ClientDriver.runApplication(app);
+ * }
+ * </pre>
+ *
+ * <p>Because all of the dependencies of a ClientApplication is expected to be part of the application JAR, the OSGi
+ * framework created by this ClientDriver is disabled. Calling any method on that framework will throw an
+ * exception. If you need OSGi support, use either of the runApplicationWithOsgi() methods.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public abstract class ClientDriver {
+
+ /**
+ * <p>Creates and runs the given {@link ClientApplication}.</p>
+ *
+ * @param app The ClientApplication to inject.
+ * @param guiceModules The Guice {@link Module Modules} to install prior to startup.
+ * @throws Exception If an exception was thrown by the ClientApplication.
+ */
+ public static void runApplication(ClientApplication app, Module... guiceModules)
+ throws Exception
+ {
+ runApplication(newNonWorkingOsgiFramework(), newModuleList(app, guiceModules));
+ }
+
+ /**
+ * <p>Creates and runs an instance of the given {@link ClientApplication} class.</p>
+ *
+ * @param appClass The ClientApplication class to inject.
+ * @param guiceModules The Guice {@link Module Modules} to install prior to startup.
+ * @throws Exception If an exception was thrown by the ClientApplication.
+ */
+ public static void runApplication(Class<? extends ClientApplication> appClass, Module... guiceModules)
+ throws Exception
+ {
+ runApplication(newNonWorkingOsgiFramework(), newModuleList(appClass, guiceModules));
+ }
+
+ /**
+ * <p>Creates and runs an instance of the the given {@link ClientApplication} class with OSGi support.</p>
+ *
+ * @param cachePath The path to use for the OSGi bundle cache.
+ * @param appClass The ClientApplication class to inject.
+ * @param guiceModules The Guice {@link Module Modules} to install prior to startup.
+ * @throws Exception If an exception was thrown by the ClientApplication.
+ */
+ public static void runApplicationWithOsgi(String cachePath, Class<? extends ClientApplication> appClass,
+ Module... guiceModules) throws Exception
+ {
+ runApplication(newOsgiFramework(cachePath), newModuleList(appClass, guiceModules));
+ }
+
+ private static OsgiFramework newNonWorkingOsgiFramework() {
+ return new NonWorkingOsgiFramework();
+ }
+
+ private static FelixFramework newOsgiFramework(String cachePath) {
+ return new FelixFramework(new FelixParams().setCachePath(cachePath));
+ }
+
+ private static List<Module> newModuleList(final ClientApplication appInstance, Module... guiceModules) {
+ List<Module> lst = new LinkedList<>(Arrays.asList(guiceModules));
+ lst.add(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(Application.class).toInstance(appInstance);
+ }
+ });
+ return lst;
+ }
+
+ private static List<Module> newModuleList(final Class<? extends ClientApplication> appClass,
+ Module... guiceModules)
+ {
+ List<Module> lst = new LinkedList<>(Arrays.asList(guiceModules));
+ lst.add(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(Application.class).to(appClass);
+ }
+ });
+ return lst;
+ }
+
+ private static void runApplication(OsgiFramework osgi, List<Module> modules) throws Exception {
+ ApplicationLoader loader = new ApplicationLoader(osgi, modules);
+ loader.init(null, false);
+ try {
+ loader.start();
+ ((ClientApplication)loader.application()).run();
+ loader.stop();
+ } finally {
+ loader.destroy();
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/client/package-info.java b/jdisc_core/src/main/java/com/yahoo/jdisc/client/package-info.java
new file mode 100644
index 00000000000..c5e53ed8a90
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/client/package-info.java
@@ -0,0 +1,9 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * <p>Provides classes and interfaces for implementing a {@link com.yahoo.jdisc.client.ClientApplication
+ * ClientApplication}.</p>
+ *
+ * @see com.yahoo.jdisc.client.ClientApplication
+ */
+@com.yahoo.api.annotations.PublicApi
+package com.yahoo.jdisc.client;
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ActiveContainer.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ActiveContainer.java
new file mode 100644
index 00000000000..a296bd1e327
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ActiveContainer.java
@@ -0,0 +1,137 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Injector;
+import com.yahoo.jdisc.AbstractResource;
+import com.yahoo.jdisc.SharedResource;
+import com.yahoo.jdisc.application.BindingSet;
+import com.yahoo.jdisc.application.BindingSetSelector;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.application.ResourcePool;
+import com.yahoo.jdisc.application.UriPattern;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.service.BindingSetNotFoundException;
+import com.yahoo.jdisc.service.CurrentContainer;
+import com.yahoo.jdisc.service.NoBindingSetSelectedException;
+import com.yahoo.jdisc.service.ServerProvider;
+
+import java.net.URI;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ActiveContainer extends AbstractResource implements CurrentContainer {
+
+ private final ContainerTermination termination;
+ private final Injector guiceInjector;
+ private final Iterable<ServerProvider> serverProviders;
+ private final ResourcePool resourceReferences = new ResourcePool();
+ private final Map<String, BindingSet<RequestHandler>> serverBindings;
+ private final Map<String, BindingSet<RequestHandler>> clientBindings;
+ private final BindingSetSelector bindingSetSelector;
+ private final TimeoutManagerImpl timeoutMgr;
+
+ public ActiveContainer(ContainerBuilder builder) {
+ serverProviders = builder.serverProviders().activate();
+ for (SharedResource resource : serverProviders) {
+ resourceReferences.retain(resource);
+ }
+ serverBindings = builder.activateServerBindings();
+ for (BindingSet<RequestHandler> set : serverBindings.values()) {
+ for (Map.Entry<UriPattern, RequestHandler> entry : set) {
+ resourceReferences.retain(entry.getValue());
+ }
+ }
+ clientBindings = builder.activateClientBindings();
+ for (BindingSet<RequestHandler> set : clientBindings.values()) {
+ for (Map.Entry<UriPattern, RequestHandler> entry : set) {
+ resourceReferences.retain(entry.getValue());
+ }
+ }
+ bindingSetSelector = builder.getInstance(BindingSetSelector.class);
+ timeoutMgr = builder.getInstance(TimeoutManagerImpl.class);
+ timeoutMgr.start();
+ builder.guiceModules().install(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(TimeoutManagerImpl.class).toInstance(timeoutMgr);
+ }
+ });
+ guiceInjector = builder.guiceModules().activate();
+ termination = new ContainerTermination(builder.appContext());
+ }
+
+ @Override
+ protected void destroy() {
+ resourceReferences.release();
+ timeoutMgr.shutdown();
+ termination.run();
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (retainCount() > 0) {
+ destroy();
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Make this instance retain a reference to the resource until it is destroyed.
+ */
+ void retainReference(SharedResource resource) {
+ resourceReferences.retain(resource);
+ }
+
+ public ContainerTermination shutdown() {
+ return termination;
+ }
+
+ public Injector guiceInjector() {
+ return guiceInjector;
+ }
+
+ public Iterable<ServerProvider> serverProviders() {
+ return serverProviders;
+ }
+
+ public Map<String, BindingSet<RequestHandler>> serverBindings() {
+ return serverBindings;
+ }
+
+ public BindingSet<RequestHandler> serverBindings(String setName) {
+ return serverBindings.get(setName);
+ }
+
+ public Map<String, BindingSet<RequestHandler>> clientBindings() {
+ return clientBindings;
+ }
+
+ public BindingSet<RequestHandler> clientBindings(String setName) {
+ return clientBindings.get(setName);
+ }
+
+ TimeoutManagerImpl timeoutManager() {
+ return timeoutMgr;
+ }
+
+ @Override
+ public ContainerSnapshot newReference(URI uri) {
+ String name = bindingSetSelector.select(uri);
+ if (name == null) {
+ throw new NoBindingSetSelectedException(uri);
+ }
+ BindingSet<RequestHandler> serverBindings = serverBindings(name);
+ BindingSet<RequestHandler> clientBindings = clientBindings(name);
+ if (serverBindings == null || clientBindings == null) {
+ throw new BindingSetNotFoundException(name);
+ }
+ return new ContainerSnapshot(this, serverBindings, clientBindings);
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationConfigModule.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationConfigModule.java
new file mode 100644
index 00000000000..00908df4249
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationConfigModule.java
@@ -0,0 +1,64 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.AbstractModule;
+import com.google.inject.name.Names;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.*;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+class ApplicationConfigModule extends AbstractModule {
+
+ private final Map<String, String> config;
+
+ ApplicationConfigModule(Map<String, String> config) {
+ this.config = normalizeConfig(config);
+ }
+
+ @Override
+ protected void configure() {
+ for (Map.Entry<String, String> entry : config.entrySet()) {
+ bind(String.class).annotatedWith(Names.named(entry.getKey())).toInstance(entry.getValue());
+ }
+ }
+
+ public static ApplicationConfigModule newInstanceFromFile(String fileName) throws IOException {
+ Properties props = new Properties();
+ InputStream in = null;
+ try {
+ in = new FileInputStream(fileName);
+ props.load(in);
+ } finally {
+ if (in != null) {
+ in.close();
+ }
+ }
+ Map<String, String> ret = new HashMap<>();
+ for (String name : props.stringPropertyNames()) {
+ ret.put(name, props.getProperty(name));
+ }
+ return new ApplicationConfigModule(ret);
+ }
+
+ private static Map<String, String> normalizeConfig(Map<String, String> raw) {
+ List<String> names = new ArrayList<>(raw.keySet());
+ Collections.sort(names, new Comparator<String>() {
+
+ @Override
+ public int compare(String lhs, String rhs) {
+ return -lhs.compareTo(rhs); // reverse alphabetical order, i.e. lower-case before upper-case
+ }
+ });
+ Map<String, String> ret = new HashMap<>();
+ for (String name : names) {
+ ret.put(name.toLowerCase(Locale.US), raw.get(name));
+ }
+ return ImmutableMap.copyOf(ret);
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationEnvironmentModule.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationEnvironmentModule.java
new file mode 100644
index 00000000000..c6d6efd0ee9
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationEnvironmentModule.java
@@ -0,0 +1,37 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.yahoo.jdisc.application.ContainerActivator;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.application.ContainerThread;
+import com.yahoo.jdisc.application.OsgiFramework;
+import com.yahoo.jdisc.service.CurrentContainer;
+
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+class ApplicationEnvironmentModule extends AbstractModule {
+
+ private final ApplicationLoader loader;
+
+ public ApplicationEnvironmentModule(ApplicationLoader loader) {
+ this.loader = loader;
+ }
+
+ @Override
+ protected void configure() {
+ bind(ContainerActivator.class).toInstance(loader);
+ bind(CurrentContainer.class).toInstance(loader);
+ bind(OsgiFramework.class).toInstance(loader.osgiFramework());
+ bind(ThreadFactory.class).to(ContainerThread.Factory.class);
+ }
+
+ @Provides
+ public ContainerBuilder containerBuilder() {
+ return loader.newContainerBuilder();
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationLoader.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationLoader.java
new file mode 100644
index 00000000000..2dd7f7eb879
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationLoader.java
@@ -0,0 +1,261 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.yahoo.jdisc.AbstractResource;
+import com.yahoo.jdisc.application.*;
+import com.yahoo.jdisc.service.ContainerNotReadyException;
+import com.yahoo.jdisc.service.CurrentContainer;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+
+import java.lang.ref.WeakReference;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ApplicationLoader implements BootstrapLoader, ContainerActivator, CurrentContainer {
+
+ private static final Logger log = Logger.getLogger(ApplicationLoader.class.getName());
+ private final OsgiFramework osgiFramework;
+ private final GuiceRepository guiceModules = new GuiceRepository();
+ private final AtomicReference<ActiveContainer> containerRef = new AtomicReference<>();
+ private final Object appLock = new Object();
+ private final List<Bundle> appBundles = new ArrayList<>();
+ private Application application;
+ private ApplicationInUseTracker applicationInUseTracker;
+
+ public ApplicationLoader(OsgiFramework osgiFramework, Iterable<? extends Module> guiceModules) {
+ this.osgiFramework = osgiFramework;
+ this.guiceModules.install(new ApplicationEnvironmentModule(this));
+ this.guiceModules.installAll(guiceModules);
+ }
+
+ @Override
+ public ContainerBuilder newContainerBuilder() {
+ return new ContainerBuilder(guiceModules);
+ }
+
+ @Override
+ public DeactivatedContainer activateContainer(ContainerBuilder builder) {
+ ActiveContainer next = builder != null ? new ActiveContainer(builder) : null;
+ final ActiveContainer prev;
+ synchronized (appLock) {
+ if (application == null && next != null) {
+ next.release();
+ throw new ApplicationNotReadyException();
+ }
+
+ if (next != null) {
+ next.retainReference(applicationInUseTracker);
+ }
+
+ prev = containerRef.getAndSet(next);
+ if (prev == null) {
+ return null;
+ }
+ }
+ prev.release();
+ DeactivatedContainer deactivatedContainer = prev.shutdown();
+
+ final WeakReference<ActiveContainer> prevContainerReference = new WeakReference<>(prev);
+ final Runnable deactivationMonitor = () -> {
+ long waitTimeSeconds = 30L;
+ long totalTimeWaited = 0L;
+
+ while (!Thread.interrupted()) {
+ final long currentWaitTimeSeconds = waitTimeSeconds;
+ totalTimeWaited += currentWaitTimeSeconds;
+
+ Interruption.mapExceptionToThreadState(() ->
+ Thread.sleep(TimeUnit.MILLISECONDS.convert(currentWaitTimeSeconds, TimeUnit.SECONDS))
+ );
+
+ final ActiveContainer prevContainer = prevContainerReference.get();
+ if (prevContainer == null) {
+ return;
+ }
+ if (prevContainer.retainCount() == 0) {
+ return;
+ }
+ log.warning("Previous container not terminated in the last " + totalTimeWaited + " seconds."
+ + " Reference state={ " + prevContainer.currentState() + " }");
+
+ waitTimeSeconds = (long) (waitTimeSeconds * 1.2);
+ }
+ log.warning("Deactivation monitor thread unexpectedly interrupted");
+ };
+ final Thread deactivationMonitorThread = new Thread(deactivationMonitor, "Container deactivation monitor");
+ deactivationMonitorThread.setDaemon(true);
+ deactivationMonitorThread.start();
+
+ return deactivatedContainer;
+ }
+
+ @Override
+ public ContainerSnapshot newReference(URI uri) {
+ ActiveContainer container = containerRef.get();
+ if (container == null) {
+ throw new ContainerNotReadyException();
+ }
+ return container.newReference(uri);
+ }
+
+ @Override
+ public void init(String appLocation, boolean privileged) throws Exception {
+ log.finer("Initializing application loader.");
+ osgiFramework.start();
+ BundleContext ctx = osgiFramework.bundleContext();
+ if (ctx != null) {
+ ctx.registerService(CurrentContainer.class.getName(), this, null);
+ }
+ if(appLocation == null) {
+ return; // application class bound by another module
+ }
+ try {
+ final Class<Application> appClass = ContainerBuilder.safeClassCast(Application.class, Class.forName(appLocation));
+ guiceModules.install(new AbstractModule() {
+ @Override
+ public void configure() {
+ bind(Application.class).to(appClass);
+ }
+ });
+ return; // application class found on class path
+ } catch (ClassNotFoundException e) {
+ // location is not a class name
+ if (log.isLoggable(Level.FINE)) {
+ log.fine("App location is not a class name. Installing bundle");
+ }
+ }
+ appBundles.addAll(osgiFramework.installBundle(appLocation));
+ if (OsgiHeader.isSet(appBundles.get(0), OsgiHeader.PRIVILEGED_ACTIVATOR)) {
+ osgiFramework.startBundles(appBundles, privileged);
+ }
+
+ }
+
+ @Override
+ public void start() throws Exception {
+ log.finer("Initializing application.");
+ Injector injector = guiceModules.activate();
+ Application app;
+ if (!appBundles.isEmpty()) {
+ Bundle appBundle = appBundles.get(0);
+ if (!OsgiHeader.isSet(appBundle, OsgiHeader.PRIVILEGED_ACTIVATOR)) {
+ osgiFramework.startBundles(appBundles, false);
+ }
+ List<String> header = OsgiHeader.asList(appBundle, OsgiHeader.APPLICATION);
+ if (header.size() != 1) {
+ throw new IllegalArgumentException("OSGi header '" + OsgiHeader.APPLICATION + "' has " + header.size() +
+ " entries, expected 1.");
+ }
+ String appName = header.get(0);
+ log.finer("Loading application class " + appName + " from bundle '" + appBundle.getSymbolicName() + "'.");
+ Class<Application> appClass = ContainerBuilder.safeClassCast(Application.class,
+ appBundle.loadClass(appName));
+ app = injector.getInstance(appClass);
+ } else {
+ app = injector.getInstance(Application.class);
+ log.finer("Injecting instance of " + app.getClass().getName() + ".");
+ }
+ try {
+ synchronized (appLock) {
+ application = app;
+ applicationInUseTracker = new ApplicationInUseTracker();
+ }
+ app.start();
+ } catch (Exception e) {
+ log.log(Level.WARNING, "Exception thrown while activating application.", e);
+ synchronized (appLock) {
+ application = null;
+ applicationInUseTracker = null;
+ }
+ app.destroy();
+ throw e;
+ }
+ }
+
+ @Override
+ public void stop() throws Exception {
+ log.finer("Destroying application.");
+ Application app;
+ ApplicationInUseTracker applicationInUseTracker;
+ synchronized (appLock) {
+ app = application;
+ applicationInUseTracker = this.applicationInUseTracker;
+ }
+ if (app == null || applicationInUseTracker == null) {
+ return;
+ }
+ try {
+ app.stop();
+ } catch (Exception e) {
+ log.log(Level.WARNING, "Exception thrown while deactivating application.", e);
+ }
+ synchronized (appLock) {
+ application = null;
+ }
+ activateContainer(null);
+ synchronized (appLock) {
+ this.applicationInUseTracker = null;
+ }
+ applicationInUseTracker.release();
+ applicationInUseTracker.applicationInUseLatch.await();
+ app.destroy();
+ }
+
+ @Override
+ public void destroy() {
+ log.finer("Destroying application loader.");
+ try {
+ osgiFramework.stop();
+ } catch (BundleException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public Application application() {
+ synchronized (appLock) {
+ return application;
+ }
+ }
+
+ public OsgiFramework osgiFramework() {
+ return osgiFramework;
+ }
+
+ private static class ApplicationInUseTracker extends AbstractResource {
+ //opened when the application has been stopped and there's no active containers
+ final CountDownLatch applicationInUseLatch = new CountDownLatch(1);
+
+ @Override
+ protected void destroy() {
+ applicationInUseLatch.countDown();
+ }
+ }
+
+ private static class Interruption {
+ interface Runnable_throws<E extends Throwable> {
+ void run() throws E;
+ }
+
+ public static void mapExceptionToThreadState(Runnable_throws<InterruptedException> runnable) {
+ try {
+ runnable.run();
+ } catch (InterruptedException ignored) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapDaemon.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapDaemon.java
new file mode 100644
index 00000000000..21c52d6047d
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapDaemon.java
@@ -0,0 +1,104 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import com.google.inject.Module;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.application.OsgiFramework;
+import org.apache.commons.daemon.Daemon;
+import org.apache.commons.daemon.DaemonContext;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.logging.Logger;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class BootstrapDaemon implements Daemon {
+
+ private static final Logger log = Logger.getLogger(BootstrapDaemon.class.getName());
+ private final BootstrapLoader loader;
+ private final boolean privileged;
+ private String bundleLocation;
+
+ static {
+ // force load slf4j to avoid other logging frameworks from initializing before it
+ org.slf4j.LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
+ }
+
+ public BootstrapDaemon() {
+ this(new ApplicationLoader(newOsgiFramework(), newConfigModule()),
+ Boolean.valueOf(System.getProperty("jdisc.privileged")));
+ }
+
+ BootstrapDaemon(BootstrapLoader loader, boolean privileged) {
+ this.loader = loader;
+ this.privileged = privileged;
+ }
+
+ BootstrapLoader loader() {
+ return loader;
+ }
+
+ @Override
+ public void init(DaemonContext context) throws Exception {
+ String[] args = context.getArguments();
+ if (args == null || args.length != 1 || args[0] == null) {
+ throw new IllegalArgumentException("Expected 1 argument, got " + Arrays.toString(args) + ".");
+ }
+ bundleLocation = args[0];
+ if (privileged) {
+ log.finer("Initializing application with privileges.");
+ loader.init(bundleLocation, true);
+ }
+ }
+
+ @Override
+ public void start() throws Exception {
+ if (!privileged) {
+ log.finer("Initializing application without privileges.");
+ loader.init(bundleLocation, false);
+ }
+ loader.start();
+ }
+
+ @Override
+ public void stop() throws Exception {
+ loader.stop();
+ }
+
+ @Override
+ public void destroy() {
+ loader.destroy();
+ }
+
+ private static OsgiFramework newOsgiFramework() {
+ String cachePath = System.getProperty("jdisc.cache.path");
+ if (cachePath == null) {
+ throw new IllegalStateException("System property 'jdisc.cache.path' not set.");
+ }
+ FelixParams params = new FelixParams()
+ .setCachePath(cachePath)
+ .setLoggerEnabled(Boolean.valueOf(System.getProperty("jdisc.logger.enabled", "true")));
+ for (String str : ContainerBuilder.safeStringSplit(System.getProperty("jdisc.export.packages"), ",")) {
+ params.exportPackage(str);
+ }
+ return new FelixFramework(params);
+ }
+
+ private static Iterable<Module> newConfigModule() {
+ String configFile = System.getProperty("jdisc.config.file");
+ if (configFile == null) {
+ return Collections.emptyList();
+ }
+ Module configModule;
+ try {
+ configModule = ApplicationConfigModule.newInstanceFromFile(configFile);
+ } catch (IOException e) {
+ throw new IllegalStateException("Exception thrown while reading config file '" + configFile + "'.", e);
+ }
+ return Arrays.asList(configModule);
+ }
+
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapLoader.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapLoader.java
new file mode 100644
index 00000000000..68e9f58c7ff
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapLoader.java
@@ -0,0 +1,16 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface BootstrapLoader {
+
+ public void init(String bundleLocation, boolean privileged) throws Exception;
+
+ public void start() throws Exception;
+
+ public void stop() throws Exception;
+
+ public void destroy();
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/BundleLocationResolver.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/BundleLocationResolver.java
new file mode 100644
index 00000000000..a65040b0451
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/BundleLocationResolver.java
@@ -0,0 +1,70 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+class BundleLocationResolver {
+
+ static final String BUNDLE_PATH = System.getProperty("jdisc.bundle.path", ".") + "/";
+
+ public static String resolve(String bundleLocation) {
+ bundleLocation = expandSystemProperties(bundleLocation);
+ bundleLocation = bundleLocation.trim();
+ String scheme = getLocationScheme(bundleLocation);
+ if (scheme == null) {
+ bundleLocation = "file:" + getCanonicalPath(BUNDLE_PATH + bundleLocation);
+ } else if (scheme.equalsIgnoreCase("file")) {
+ bundleLocation = "file:" + getCanonicalPath(bundleLocation.substring(5));
+ }
+ return bundleLocation;
+ }
+
+ private static String expandSystemProperties(String str) {
+ StringBuilder ret = new StringBuilder();
+ int prev = 0;
+ while (true) {
+ int from = str.indexOf("${", prev);
+ if (from < 0) {
+ break;
+ }
+ ret.append(str.substring(prev, from));
+ prev = from;
+
+ int to = str.indexOf("}", from);
+ if (to < 0) {
+ break;
+ }
+ ret.append(System.getProperty(str.substring(from + 2, to), ""));
+ prev = to + 1;
+ }
+ if (prev >= 0) {
+ ret.append(str.substring(prev));
+ }
+ return ret.toString();
+ }
+
+ private static String getCanonicalPath(String path) {
+ try {
+ return new File(path).getCanonicalPath();
+ } catch (IOException e) {
+ return path;
+ }
+ }
+
+ private static String getLocationScheme(String bundleLocation) {
+ char[] arr = bundleLocation.toCharArray();
+ for (int i = 0; i < arr.length; ++i) {
+ if (arr[i] == ':' && i > 0) {
+ return bundleLocation.substring(0, i);
+ }
+ if (!Character.isLetterOrDigit(arr[i])) {
+ return null;
+ }
+ }
+ return null;
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogFormatter.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogFormatter.java
new file mode 100644
index 00000000000..899e8a98aa7
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogFormatter.java
@@ -0,0 +1,199 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.log.LogEntry;
+import org.osgi.service.log.LogService;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+class ConsoleLogFormatter {
+
+ // The string used as a replacement for absent/null values.
+ static final String ABSENCE_REPLACEMENT = "-";
+
+ private final String hostName;
+ private final String processId;
+ private final String serviceName;
+
+ public ConsoleLogFormatter(String hostName, String processId, String serviceName) {
+ this.hostName = formatOptional(hostName);
+ this.processId = formatOptional(processId);
+ this.serviceName = formatOptional(serviceName);
+ }
+
+ public String formatEntry(LogEntry entry) {
+ StringBuilder ret = new StringBuilder();
+ formatTime(entry, ret).append('\t');
+ formatHostName(ret).append('\t');
+ formatProcessId(entry, ret).append('\t');
+ formatServiceName(ret).append('\t');
+ formatComponent(entry, ret).append('\t');
+ formatLevel(entry, ret).append('\t');
+ formatMessage(entry, ret);
+ formatException(entry, ret);
+ return ret.toString();
+ }
+
+ // TODO: The non-functional, side effect-laden coding style here is ugly and makes testing hard. See ticket 7128315.
+
+ private StringBuilder formatTime(LogEntry entry, StringBuilder out) {
+ String str = Long.toString(Long.MAX_VALUE & entry.getTime()); // remove sign bit for good measure
+ int len = str.length();
+ if (len > 3) {
+ out.append(str, 0, len - 3);
+ } else {
+ out.append('0');
+ }
+ out.append('.');
+ if (len > 2) {
+ out.append(str, len - 3, len);
+ } else if (len == 2) {
+ out.append('0').append(str, len - 2, len); // should never happen
+ } else if (len == 1) {
+ out.append("00").append(str, len - 1, len); // should never happen
+ }
+ return out;
+ }
+
+ private StringBuilder formatHostName(StringBuilder out) {
+ out.append(hostName);
+ return out;
+ }
+
+ private StringBuilder formatProcessId(LogEntry entry, StringBuilder out) {
+ out.append(processId);
+ String threadId = getProperty(entry, "THREAD_ID");
+ if (threadId != null) {
+ out.append('/').append(threadId);
+ }
+ return out;
+ }
+
+ private StringBuilder formatServiceName(StringBuilder out) {
+ out.append(serviceName);
+ return out;
+ }
+
+ private StringBuilder formatComponent(LogEntry entry, StringBuilder out) {
+ Bundle bundle = entry.getBundle();
+ String loggerName = getProperty(entry, "LOGGER_NAME");
+ if (bundle == null && loggerName == null) {
+ out.append("-");
+ } else {
+ if (bundle != null) {
+ out.append(bundle.getSymbolicName());
+ }
+ if (loggerName != null) {
+ out.append('/').append(loggerName);
+ }
+ }
+ return out;
+ }
+
+ private StringBuilder formatLevel(LogEntry entry, StringBuilder out) {
+ switch (entry.getLevel()) {
+ case LogService.LOG_ERROR:
+ out.append("error");
+ break;
+ case LogService.LOG_WARNING:
+ out.append("warning");
+ break;
+ case LogService.LOG_INFO:
+ out.append("info");
+ break;
+ case LogService.LOG_DEBUG:
+ out.append("debug");
+ break;
+ default:
+ out.append("unknown");
+ break;
+ }
+ return out;
+ }
+
+ private StringBuilder formatMessage(LogEntry entry, StringBuilder out) {
+ String msg = entry.getMessage();
+ if (msg != null) {
+ formatString(msg, out);
+ }
+ return out;
+ }
+
+ private StringBuilder formatException(LogEntry entry, StringBuilder out) {
+ Throwable t = entry.getException();
+ if (t != null) {
+ if (entry.getLevel() == LogService.LOG_INFO) {
+ out.append(": ");
+ String msg = t.getMessage();
+ if (msg != null) {
+ formatString(msg, out);
+ } else {
+ out.append(t.getClass().getName());
+ }
+ } else {
+ Writer buf = new StringWriter();
+ t.printStackTrace(new PrintWriter(buf));
+ formatString("\n" + buf, out);
+ }
+ }
+ return out;
+ }
+
+ private static StringBuilder formatString(String str, StringBuilder out) {
+ for (int i = 0, len = str.length(); i < len; ++i) {
+ char c = str.charAt(i);
+ switch (c) {
+ case '\n':
+ out.append("\\n");
+ break;
+ case '\r':
+ out.append("\\r");
+ break;
+ case '\t':
+ out.append("\\t");
+ break;
+ case '\\':
+ out.append("\\\\");
+ break;
+ default:
+ out.append(c);
+ break;
+ }
+ }
+ return out;
+ }
+
+ private static String getProperty(LogEntry entry, String name) {
+ ServiceReference<?> ref = entry.getServiceReference();
+ if (ref == null) {
+ return null;
+ }
+ Object val = ref.getProperty(name);
+ if (val == null) {
+ return null;
+ }
+ return val.toString();
+ }
+
+ static String formatOptional(String str) {
+ return formatOptional(str, ABSENCE_REPLACEMENT);
+ }
+
+ private static String formatOptional(final String str, final String replacementIfAbsent) {
+ if (str == null) {
+ return replacementIfAbsent;
+ }
+ final String result = str.trim();
+ if (result.isEmpty()) {
+ return replacementIfAbsent;
+ }
+ return result;
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogListener.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogListener.java
new file mode 100644
index 00000000000..b41e195f6a7
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogListener.java
@@ -0,0 +1,109 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import org.osgi.service.log.LogEntry;
+import org.osgi.service.log.LogListener;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.lang.management.ManagementFactory;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a>
+ */
+class ConsoleLogListener implements LogListener {
+
+ public static final int DEFAULT_LOG_LEVEL = Integer.MAX_VALUE;
+ private final ConsoleLogFormatter formatter;
+ private final PrintStream out;
+ private final int maxLevel;
+
+ ConsoleLogListener(PrintStream out, String serviceName, String logLevel) {
+ this.out = out;
+ this.formatter = new ConsoleLogFormatter(getHostname(), getProcessId(), serviceName);
+ this.maxLevel = parseLogLevel(logLevel);
+ }
+
+ @Override
+ public void logged(LogEntry entry) {
+ if (entry.getLevel() > maxLevel) {
+ return;
+ }
+ out.println(formatter.formatEntry(entry));
+ }
+
+ public static int parseLogLevel(String logLevel) {
+ if (logLevel == null || logLevel.isEmpty()) {
+ return DEFAULT_LOG_LEVEL;
+ }
+ if (logLevel.equalsIgnoreCase("OFF")) {
+ return Integer.MIN_VALUE;
+ }
+ if (logLevel.equalsIgnoreCase("ERROR")) {
+ return 1;
+ }
+ if (logLevel.equalsIgnoreCase("WARNING")) {
+ return 2;
+ }
+ if (logLevel.equalsIgnoreCase("INFO")) {
+ return 3;
+ }
+ if (logLevel.equalsIgnoreCase("DEBUG")) {
+ return 4;
+ }
+ if (logLevel.equalsIgnoreCase("ALL")) {
+ return Integer.MAX_VALUE;
+ }
+ try {
+ return Integer.valueOf(logLevel);
+ } catch (NumberFormatException e) {
+ // fall through
+ }
+ return DEFAULT_LOG_LEVEL;
+ }
+
+ public static ConsoleLogListener newInstance() {
+ return new ConsoleLogListener(System.out,
+ System.getProperty("jdisc.logger.tag"),
+ System.getProperty("jdisc.logger.level"));
+ }
+
+ static String getProcessId() {
+ // platform independent
+ String jvmName = ManagementFactory.getRuntimeMXBean().getName();
+ if (jvmName != null) {
+ int idx = jvmName.indexOf('@');
+ if (idx > 0) {
+ try {
+ return Long.toString(Long.valueOf(jvmName.substring(0, jvmName.indexOf('@'))));
+ } catch (NumberFormatException e) {
+ // fall through
+ }
+ }
+ }
+
+ // linux specific
+ File file = new File("/proc/self");
+ if (file.exists()) {
+ try {
+ return file.getCanonicalFile().getName();
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ // fallback
+ return null;
+ }
+
+ static String getHostname() {
+ try {
+ return InetAddress.getLocalHost().getCanonicalHostName();
+ } catch (UnknownHostException e) {
+ return null;
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogManager.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogManager.java
new file mode 100644
index 00000000000..c5e8602c861
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogManager.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.log.LogReaderService;
+import org.osgi.util.tracker.ServiceTracker;
+import org.osgi.util.tracker.ServiceTrackerCustomizer;
+
+/**
+ * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a>
+ */
+class ConsoleLogManager {
+
+ private final ConsoleLogListener listener = ConsoleLogListener.newInstance();
+ private ServiceTracker<LogReaderService,LogReaderService> tracker;
+
+ @SuppressWarnings("unchecked")
+ public void install(final BundleContext osgiContext) {
+ if (tracker != null) {
+ throw new IllegalStateException("ConsoleLogManager already installed.");
+ }
+ tracker = new ServiceTracker<LogReaderService,LogReaderService>(osgiContext, LogReaderService.class.getName(),
+ new ServiceTrackerCustomizer<LogReaderService,LogReaderService>() {
+
+ @Override
+ public LogReaderService addingService(ServiceReference<LogReaderService> reference) {
+ LogReaderService service = osgiContext.getService(reference);
+ service.addLogListener(listener);
+ return service;
+ }
+
+ @Override
+ public void modifiedService(ServiceReference<LogReaderService> reference, LogReaderService service) {
+
+ }
+
+ @Override
+ public void removedService(ServiceReference<LogReaderService> reference, LogReaderService service) {
+ service.removeLogListener(listener);
+ }
+ });
+ tracker.open();
+ }
+
+ public boolean uninstall() {
+ if (tracker == null) {
+ return false;
+ }
+ tracker.close();
+ tracker = null;
+ return true;
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerSnapshot.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerSnapshot.java
new file mode 100644
index 00000000000..4f4544fa8f8
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerSnapshot.java
@@ -0,0 +1,112 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import com.google.inject.Key;
+import com.yahoo.jdisc.AbstractResource;
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.ResourceReference;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.jdisc.application.BindingSet;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.NullContent;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.handler.ResponseHandler;
+
+import java.util.Objects;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+class ContainerSnapshot extends AbstractResource implements Container {
+
+ private final TimeoutManagerImpl timeoutMgr;
+ private final ActiveContainer container;
+ private final ResourceReference containerReference;
+ private final BindingSet<RequestHandler> serverBindings;
+ private final BindingSet<RequestHandler> clientBindings;
+
+ ContainerSnapshot(ActiveContainer container, BindingSet<RequestHandler> serverBindings,
+ BindingSet<RequestHandler> clientBindings)
+ {
+ this.timeoutMgr = container.timeoutManager();
+ this.container = container;
+ this.serverBindings = serverBindings;
+ this.clientBindings = clientBindings;
+ this.containerReference = container.refer();
+ }
+
+ @Override
+ public <T> T getInstance(Key<T> key) {
+ return container.guiceInjector().getInstance(key);
+ }
+
+ @Override
+ public <T> T getInstance(Class<T> type) {
+ return container.guiceInjector().getInstance(type);
+ }
+
+ @Override
+ public RequestHandler resolveHandler(Request request) {
+ BindingMatch<RequestHandler> match = request.isServerRequest() ? serverBindings.match(request.getUri())
+ : clientBindings.match(request.getUri());
+ if (match == null) {
+ return null;
+ }
+ request.setBindingMatch(match);
+ RequestHandler ret = new NullContentRequestHandler(match.target());
+ if (request.getTimeoutManager() == null) {
+ ret = timeoutMgr.manageHandler(ret);
+ }
+ return ret;
+ }
+
+ @Override
+ protected void destroy() {
+ containerReference.close();
+ }
+
+ @Override
+ public long currentTimeMillis() {
+ return timeoutMgr.timer().currentTimeMillis();
+ }
+
+ private static class NullContentRequestHandler implements RequestHandler {
+
+ final RequestHandler delegate;
+
+ NullContentRequestHandler(RequestHandler delegate) {
+ Objects.requireNonNull(delegate, "delegate");
+ this.delegate = delegate;
+ }
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler responseHandler) {
+ ContentChannel content = delegate.handleRequest(request, responseHandler);
+ if (content == null) {
+ content = NullContent.INSTANCE;
+ }
+ return content;
+ }
+
+ @Override
+ public void handleTimeout(Request request, ResponseHandler responseHandler) {
+ delegate.handleTimeout(request, responseHandler);
+ }
+
+ @Override
+ public ResourceReference refer() {
+ return delegate.refer();
+ }
+
+ @Override
+ public void release() {
+ delegate.release();
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerTermination.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerTermination.java
new file mode 100644
index 00000000000..0fd25bfb390
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerTermination.java
@@ -0,0 +1,51 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import com.yahoo.jdisc.application.DeactivatedContainer;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ContainerTermination implements DeactivatedContainer, Runnable {
+
+ private final Object lock = new Object();
+ private final Object appContext;
+ private Runnable task;
+ private boolean done;
+
+ public ContainerTermination(Object appContext) {
+ this.appContext = appContext;
+ }
+
+ @Override
+ public Object appContext() {
+ return appContext;
+ }
+
+ @Override
+ public void notifyTermination(Runnable task) {
+ boolean done;
+ synchronized (lock) {
+ if (this.task != null) {
+ throw new IllegalStateException();
+ }
+ this.task = task;
+ done = this.done;
+ }
+ if (done) {
+ task.run();
+ }
+ }
+
+ @Override
+ public void run() {
+ Runnable task;
+ synchronized (lock) {
+ done = true;
+ task = this.task;
+ }
+ if (task != null) {
+ task.run();
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/DefaultBindingSelector.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/DefaultBindingSelector.java
new file mode 100644
index 00000000000..7e4a7b6ec5e
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/DefaultBindingSelector.java
@@ -0,0 +1,18 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import com.yahoo.jdisc.application.BindingSet;
+import com.yahoo.jdisc.application.BindingSetSelector;
+
+import java.net.URI;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class DefaultBindingSelector implements BindingSetSelector {
+
+ @Override
+ public String select(URI uri) {
+ return BindingSet.DEFAULT;
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ExportPackages.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ExportPackages.java
new file mode 100644
index 00000000000..afe43718bc5
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ExportPackages.java
@@ -0,0 +1,98 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import com.yahoo.container.plugin.bundle.AnalyzeBundle;
+import com.yahoo.container.plugin.bundle.TransformExportPackages;
+import com.yahoo.container.plugin.osgi.ExportPackages.Export;
+import org.apache.felix.framework.util.Util;
+import org.osgi.framework.Constants;
+import scala.collection.immutable.List;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.Properties;
+import java.util.jar.JarInputStream;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class ExportPackages {
+
+ public static final String PROPERTIES_FILE = "/exportPackages.properties";
+ public static final String EXPORT_PACKAGES = "exportPackages";
+ private static final String REPLACE_VERSION_PREFIX = "__REPLACE_VERSION__";
+
+ public static void main(String[] args) throws IOException {
+ String fileName = args[0];
+ if (!fileName.endsWith(PROPERTIES_FILE)) {
+ throw new IllegalArgumentException("Expected '" + PROPERTIES_FILE + "', got '" + fileName + "'.");
+ }
+ StringBuilder out = new StringBuilder();
+ out.append(getSystemPackages()).append(",")
+ .append("com.sun.security.auth,")
+ .append("com.sun.security.auth.module,")
+ .append("com.sun.management,")
+ .append("com.yahoo.jdisc,")
+ .append("com.yahoo.jdisc.application,")
+ .append("com.yahoo.jdisc.handler,")
+ .append("com.yahoo.jdisc.service,")
+ .append("javax.inject;version=1.0.0,") // Included in guice, but not exported. Needed by container-jersey.
+ .append("org.aopalliance.intercept,")
+ .append("org.aopalliance.aop,")
+ .append("org.w3c.dom.css,")
+ .append("org.w3c.dom.html,")
+ .append("org.w3c.dom.ranges,")
+ .append("org.w3c.dom.stylesheets,")
+ .append("org.w3c.dom.traversal,")
+ .append("org.w3c.dom.views,")
+ .append("sun.misc,")
+ .append("sun.net.util,")
+ .append("sun.security.krb5");
+ for (int i = 1; i < args.length; ++i) {
+ out.append(",").append(getExportedPackages(args[i]));
+ }
+ Properties props = new Properties();
+ props.setProperty(EXPORT_PACKAGES, out.toString());
+
+ try (FileWriter writer = new FileWriter(new File(fileName))) {
+ props.store(writer, "generated by " + ExportPackages.class.getName());
+ }
+ }
+
+ public static String readExportProperty() {
+ Properties props = new Properties();
+ try {
+ props.load(ExportPackages.class.getResourceAsStream(PROPERTIES_FILE));
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to read resource '" + PROPERTIES_FILE + "'.");
+ }
+ return props.getProperty(EXPORT_PACKAGES);
+ }
+
+ public static String getSystemPackages() {
+ return Util.getDefaultProperty(null, "org.osgi.framework.system.packages");
+ }
+
+ private static String getExportedPackages(String argument) throws IOException {
+ if (argument.startsWith(REPLACE_VERSION_PREFIX)) {
+ String jarFile = argument.substring(REPLACE_VERSION_PREFIX.length());
+ return readExportHeader(jarFile);
+ } else {
+ return readExportHeader(argument);
+ }
+ }
+
+ private static String readExportHeader(String jarFile) throws IOException {
+ try (JarInputStream jar = new JarInputStream(new FileInputStream(jarFile))) {
+ return jar.getManifest().getMainAttributes().getValue(Constants.EXPORT_PACKAGE);
+ }
+ }
+
+ private static String transformExports(List<Export> exports, String newVersion) {
+ return TransformExportPackages.toExportPackageProperty(
+ TransformExportPackages.removeUses(
+ TransformExportPackages.replaceVersions(exports, newVersion)));
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixFramework.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixFramework.java
new file mode 100644
index 00000000000..6509d505c70
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixFramework.java
@@ -0,0 +1,175 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import com.google.inject.Inject;
+import com.yahoo.jdisc.application.BundleInstallationException;
+import com.yahoo.jdisc.application.OsgiFramework;
+import com.yahoo.jdisc.application.OsgiHeader;
+import org.apache.felix.framework.Felix;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Constants;
+import org.osgi.framework.FrameworkEvent;
+import org.osgi.framework.FrameworkListener;
+import org.osgi.framework.wiring.FrameworkWiring;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class FelixFramework implements OsgiFramework {
+
+ private static final Logger log = Logger.getLogger(FelixFramework.class.getName());
+ private final OsgiLogManager logHandler = OsgiLogManager.newInstance();
+ private final OsgiLogService logService = new OsgiLogService();
+ private final ConsoleLogManager logListener;
+ private final Felix felix;
+
+ @Inject
+ public FelixFramework(FelixParams params) {
+ deleteDirContents(new File(params.getCachePath()));
+ felix = new Felix(params.toConfig());
+ logListener = params.isLoggerEnabled() ? new ConsoleLogManager() : null;
+ }
+
+ @Override
+ public void start() throws BundleException {
+ log.finer("Starting Felix.");
+ felix.start();
+
+ BundleContext ctx = felix.getBundleContext();
+ logService.start(ctx);
+ logHandler.install(ctx);
+ if (logListener != null) {
+ logListener.install(ctx);
+ }
+ }
+
+ @Override
+ public void stop() throws BundleException {
+ log.fine("Stopping felix.");
+ BundleContext ctx = felix.getBundleContext();
+ if (ctx != null) {
+ if (logListener != null) {
+ logListener.uninstall();
+ }
+ logHandler.uninstall();
+ logService.stop();
+ }
+ felix.stop();
+ try {
+ felix.waitForStop(0);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ @Override
+ public List<Bundle> installBundle(String bundleLocation) throws BundleException {
+ List<Bundle> bundles = new LinkedList<>();
+ try {
+ installBundle(bundleLocation, new HashSet<>(), bundles);
+ } catch (Exception e) {
+ throw new BundleInstallationException(bundles, e);
+ }
+ return bundles;
+ }
+
+ @Override
+ public void startBundles(List<Bundle> bundles, boolean privileged) throws BundleException {
+ for (Bundle bundle : bundles) {
+ if (!privileged && OsgiHeader.isSet(bundle, OsgiHeader.PRIVILEGED_ACTIVATOR)) {
+ log.log(Level.INFO, "OSGi bundle '" + bundle.getSymbolicName() + "' " +
+ "states that it requires privileged " +
+ "initialization, but privileges are not available. YMMV.");
+ }
+ if (bundle.getHeaders().get(Constants.FRAGMENT_HOST) != null) {
+ continue; // fragments can not be started
+ }
+ bundle.start();
+ }
+ }
+
+ @Override
+ public void refreshPackages() {
+ FrameworkWiring wiring = felix.adapt(FrameworkWiring.class);
+ final CountDownLatch latch = new CountDownLatch(1);
+ wiring.refreshBundles(null,
+ event -> {
+ switch (event.getType()) {
+ case FrameworkEvent.PACKAGES_REFRESHED:
+ latch.countDown();
+ break;
+ case FrameworkEvent.ERROR:
+ log.log(Level.SEVERE, "ERROR FrameworkEvent received.", event.getThrowable());
+ break;
+ }
+ });
+ try {
+ long TIMEOUT_SECONDS = 60L;
+ if (!latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+ log.warning("No PACKAGES_REFRESHED FrameworkEvent received within " + TIMEOUT_SECONDS +
+ " seconds of calling FrameworkWiring.refreshBundles()");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ @Override
+ public BundleContext bundleContext() {
+ return felix.getBundleContext();
+ }
+
+ @Override
+ public List<Bundle> bundles() {
+ return Arrays.asList(felix.getBundleContext().getBundles());
+ }
+
+ private void installBundle(String bundleLocation, Set<String> mask, List<Bundle> out) throws BundleException {
+ bundleLocation = BundleLocationResolver.resolve(bundleLocation);
+ if (mask.contains(bundleLocation)) {
+ log.finer("OSGi bundle from '" + bundleLocation + "' already installed.");
+ return;
+ }
+ log.finer("Installing OSGi bundle from '" + bundleLocation + "'.");
+ mask.add(bundleLocation);
+
+ Bundle bundle = felix.getBundleContext().installBundle(bundleLocation);
+ String symbol = bundle.getSymbolicName();
+ if (symbol == null) {
+ bundle.uninstall();
+ throw new BundleException("Missing Bundle-SymbolicName in manifest from '" + bundleLocation + " " +
+ "(it might not be an OSGi bundle).");
+ }
+ out.add(bundle);
+ for (String preInstall : OsgiHeader.asList(bundle, OsgiHeader.PREINSTALL_BUNDLE)) {
+ log.finer("OSGi bundle '" + symbol + "' requires install from '" + preInstall + "'.");
+ installBundle(preInstall, mask, out);
+ }
+ }
+
+ private static void deleteDirContents(File parent) {
+ File[] children = parent.listFiles();
+ if (children != null) {
+ for (File child : children) {
+ deleteDirContents(child);
+ boolean deleted = child.delete();
+ if (! deleted)
+ throw new RuntimeException(
+ "Could not delete file '" + child.getAbsolutePath() +"'. Please check file permissions!");
+ }
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixParams.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixParams.java
new file mode 100644
index 00000000000..0fe09798ccc
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixParams.java
@@ -0,0 +1,50 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import org.apache.felix.framework.cache.BundleCache;
+import org.osgi.framework.Constants;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class FelixParams {
+
+ private final StringBuilder exportPackages = new StringBuilder(ExportPackages.readExportProperty());
+ private String cachePath = null;
+ private boolean loggerEnabled = true;
+
+ public FelixParams exportPackage(String pkg) {
+ exportPackages.append(",").append(pkg);
+ return this;
+ }
+
+ public FelixParams setCachePath(String cachePath) {
+ this.cachePath = cachePath;
+ return this;
+ }
+
+ public String getCachePath() {
+ return cachePath;
+ }
+
+ public FelixParams setLoggerEnabled(boolean loggerEnabled) {
+ this.loggerEnabled = loggerEnabled;
+ return this;
+ }
+
+ public boolean isLoggerEnabled() {
+ return loggerEnabled;
+ }
+
+ public Map<String, String> toConfig() {
+ Map<String, String> ret = new HashMap<>();
+ ret.put(BundleCache.CACHE_ROOTDIR_PROP, cachePath);
+ ret.put(Constants.FRAMEWORK_SYSTEMPACKAGES, exportPackages.toString());
+ ret.put(Constants.SUPPORTS_BOOTCLASSPATH_EXTENSION, "true");
+ ret.put(Constants.FRAMEWORK_BOOTDELEGATION, "com.yourkit.runtime,com.yourkit.probes,com.yourkit.probes.builtin");
+ return ret;
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogHandler.java
new file mode 100644
index 00000000000..c4de1d5a7ac
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogHandler.java
@@ -0,0 +1,164 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import com.google.common.collect.ImmutableMap;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.log.LogService;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+class OsgiLogHandler extends Handler {
+
+ private static enum LogRecordProperty {
+
+ LEVEL,
+ LOGGER_NAME,
+ MESSAGE,
+ MILLIS,
+ PARAMETERS,
+ RESOURCE_BUNDLE,
+ RESOURCE_BUNDLE_NAME,
+ SEQUENCE_NUMBER,
+ SOURCE_CLASS_NAME,
+ SOURCE_METHOD_NAME,
+ THREAD_ID,
+ THROWN
+ }
+
+ private final static Map<String, LogRecordProperty> PROPERTY_MAP = createDictionary(LogRecordProperty.values());
+ private final static String[] PROPERTY_KEYS = toStringArray(LogRecordProperty.values());
+ private final LogService logService;
+
+ public OsgiLogHandler(LogService logService) {
+ this.logService = logService;
+ }
+
+ @Override
+ public void publish(LogRecord record) {
+ logService.log(new LogRecordReference(record), toServiceLevel(record.getLevel()), record.getMessage(),
+ record.getThrown());
+ }
+
+ @Override
+ public void flush() {
+ // empty
+ }
+
+ @Override
+ public void close() {
+ // empty
+ }
+
+ public static int toServiceLevel(Level level) {
+ int val = level.intValue();
+ if (val >= Level.SEVERE.intValue()) {
+ return LogService.LOG_ERROR;
+ }
+ if (val >= Level.WARNING.intValue()) {
+ return LogService.LOG_WARNING;
+ }
+ if (val >= Level.INFO.intValue()) {
+ return LogService.LOG_INFO;
+ }
+ // Level.CONFIG
+ // Level.FINE
+ // Level.FINER
+ // Level.FINEST
+ return LogService.LOG_DEBUG;
+ }
+
+ private static <T> Map<String, T> createDictionary(T[] in) {
+ Map<String, T> out = new HashMap<>();
+ for (T t : in) {
+ out.put(String.valueOf(t), t);
+ }
+ return ImmutableMap.copyOf(out);
+ }
+
+ private static String[] toStringArray(Object[] in) {
+ String[] out = new String[in.length];
+ for (int i = 0; i < in.length; ++i) {
+ out[i] = String.valueOf(in[i]);
+ }
+ return out;
+ }
+
+ private static class LogRecordReference implements ServiceReference<LogRecord> {
+
+ final LogRecord record;
+
+ LogRecordReference(LogRecord record) {
+ this.record = record;
+ }
+
+ @Override
+ public Object getProperty(String s) {
+ LogRecordProperty property = PROPERTY_MAP.get(s);
+ if (property == null) {
+ return null;
+ }
+ switch (property) {
+ case LEVEL:
+ return record.getLevel();
+ case LOGGER_NAME:
+ return record.getLoggerName();
+ case MESSAGE:
+ return record.getMessage();
+ case MILLIS:
+ return record.getMillis();
+ case PARAMETERS:
+ return record.getParameters();
+ case RESOURCE_BUNDLE:
+ return record.getResourceBundle();
+ case RESOURCE_BUNDLE_NAME:
+ return record.getResourceBundleName();
+ case SEQUENCE_NUMBER:
+ return record.getSequenceNumber();
+ case SOURCE_CLASS_NAME:
+ return record.getSourceClassName();
+ case SOURCE_METHOD_NAME:
+ return record.getSourceMethodName();
+ case THREAD_ID:
+ return record.getThreadID();
+ case THROWN:
+ return record.getThrown();
+ default:
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ @Override
+ public String[] getPropertyKeys() {
+ return PROPERTY_KEYS;
+ }
+
+ @Override
+ public Bundle getBundle() {
+ return null;
+ }
+
+ @Override
+ public Bundle[] getUsingBundles() {
+ return new Bundle[0];
+ }
+
+ @Override
+ public boolean isAssignableTo(Bundle bundle, String s) {
+ return false;
+ }
+
+ @Override
+ public int compareTo(Object o) {
+ return 0;
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogManager.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogManager.java
new file mode 100644
index 00000000000..af2ee5832aa
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogManager.java
@@ -0,0 +1,102 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.log.LogService;
+import org.osgi.util.tracker.ServiceTracker;
+import org.osgi.util.tracker.ServiceTrackerCustomizer;
+
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+class OsgiLogManager implements LogService {
+
+ private static final Object globalLock = new Object();
+ private final CopyOnWriteArrayList<LogService> services = new CopyOnWriteArrayList<>();
+ private final boolean configureLogLevel;
+ private ServiceTracker<LogService,LogService> tracker;
+
+ OsgiLogManager(boolean configureLogLevel) {
+ this.configureLogLevel = configureLogLevel;
+ }
+
+ @SuppressWarnings("unchecked")
+ public void install(final BundleContext osgiContext) {
+ if (tracker != null) {
+ throw new IllegalStateException("OsgiLogManager already installed.");
+ }
+ tracker = new ServiceTracker<>(osgiContext, LogService.class, new ServiceTrackerCustomizer<LogService,LogService>() {
+
+ @Override
+ public LogService addingService(ServiceReference<LogService> reference) {
+ LogService service = osgiContext.getService(reference);
+ services.add(service);
+ return service;
+ }
+
+ @Override
+ public void modifiedService(ServiceReference<LogService> reference, LogService service) {
+
+ }
+
+ @Override
+ public void removedService(ServiceReference<LogService> reference, LogService service) {
+ services.remove(service);
+ }
+ });
+ tracker.open();
+ synchronized (globalLock) {
+ Logger root = Logger.getLogger("");
+ if (configureLogLevel) {
+ root.setLevel(Level.ALL);
+ }
+ for (Handler handler : root.getHandlers()) {
+ root.removeHandler(handler);
+ }
+ root.addHandler(new OsgiLogHandler(this));
+ }
+ }
+
+ public boolean uninstall() {
+ if (tracker == null) {
+ return false;
+ }
+ tracker.close(); // implicitly clears the services array
+ tracker = null;
+ return true;
+ }
+
+ @Override
+ public void log(int level, String message) {
+ log(null, level, message, null);
+ }
+
+ @Override
+ public void log(int level, String message, Throwable throwable) {
+ log(null, level, message, throwable);
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void log(ServiceReference serviceRef, int level, String message) {
+ log(serviceRef, level, message, null);
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void log(ServiceReference serviceRef, int level, String message, Throwable throwable) {
+ for (LogService obj : services) {
+ obj.log(serviceRef, level, message, throwable);
+ }
+ }
+
+ public static OsgiLogManager newInstance() {
+ return new OsgiLogManager(System.getProperty("java.util.logging.config.file") == null);
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogService.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogService.java
new file mode 100644
index 00000000000..0e2a31938ce
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogService.java
@@ -0,0 +1,60 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import org.osgi.framework.*;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+class OsgiLogService {
+
+ private ServiceRegistration<OsgiLogService> registration;
+
+ public void start(BundleContext ctx) {
+ if (registration != null) {
+ throw new IllegalStateException();
+ }
+ ctx.addServiceListener(new ActivatorProxy(ctx));
+ registration = ctx.registerService(OsgiLogService.class, this, null);
+ }
+
+ public void stop() {
+ registration.unregister();
+ registration = null;
+ }
+
+ private class ActivatorProxy implements ServiceListener {
+
+ final BundleActivator activator = new org.apache.felix.log.Activator();
+ final BundleContext ctx;
+
+ ActivatorProxy(BundleContext ctx) {
+ this.ctx = ctx;
+ }
+
+ @Override
+ public void serviceChanged(ServiceEvent event) {
+ if (ctx.getService(event.getServiceReference()) != OsgiLogService.this) {
+ return;
+ }
+ switch (event.getType()) {
+ case ServiceEvent.REGISTERED:
+ try {
+ activator.start(ctx);
+ } catch (Exception e) {
+ throw new RuntimeException("Exception thrown while starting " +
+ activator.getClass().getName() + ".", e);
+ }
+ break;
+ case ServiceEvent.UNREGISTERING:
+ try {
+ activator.stop(ctx);
+ } catch (Exception e) {
+ throw new RuntimeException("Exception thrown while stopping " +
+ activator.getClass().getName() + ".", e);
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ScheduledQueue.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ScheduledQueue.java
new file mode 100644
index 00000000000..ef0e549516a
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ScheduledQueue.java
@@ -0,0 +1,136 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import java.util.Objects;
+import java.util.Queue;
+
+/**
+ * @author <a href="mailto:havardpe@yahoo-inc.com">Haavard Pettersen</a>
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+class ScheduledQueue {
+
+ public static final int MILLIS_PER_SLOT = 100;
+ public static final int NUM_SLOTS = 512;
+ public static final int NUM_SLOTS_UNDILATED = 3;
+ public static final int SLOT_MASK = 511; // bitmask to modulo NUM_SLOTS
+ public static final int ITER_SHIFT = 9; // number of bits to shift off SLOT_MASK
+
+ private final Entry[] slots = new Entry[NUM_SLOTS + 1];
+ private final int[] counts = new int[NUM_SLOTS + 1];
+ private int currIter = 0;
+ private int currSlot = 0;
+ private long nextTick;
+
+ public ScheduledQueue(long currentTimeMillis) {
+ this.nextTick = currentTimeMillis + MILLIS_PER_SLOT;
+ }
+
+ public Entry newEntry(Object payload) {
+ Objects.requireNonNull(payload, "payload");
+ return new Entry(payload);
+ }
+
+ public synchronized void drainTo(long currentTimeMillis, Queue<Object> out) {
+ if (slots[NUM_SLOTS] == null && currentTimeMillis < nextTick) {
+ return;
+ }
+ drainTo(NUM_SLOTS, 0, out);
+ for (int i = 0; currentTimeMillis >= nextTick; i++, nextTick += MILLIS_PER_SLOT) {
+ if (i < NUM_SLOTS_UNDILATED) {
+ if (++currSlot >= NUM_SLOTS) {
+ currSlot = 0;
+ currIter++;
+ }
+ drainTo(currSlot, currIter, out);
+ }
+ }
+ }
+
+ private void drainTo(int slot, int iter, Queue<Object> out) {
+ int cnt = counts[slot];
+ Entry entry = slots[slot];
+ for (int i = 0; i < cnt; i++) {
+ Entry next = entry.next;
+ if (entry.iter == iter) {
+ linkOut(entry);
+ out.add(entry.payload);
+ }
+ entry = next;
+ }
+ }
+
+ private synchronized void scheduleAt(Entry entry, long expireAtMillis) {
+ if (entry.next != null) {
+ linkOut(entry);
+ }
+ long delayMillis = expireAtMillis - nextTick;
+ if (delayMillis < 0) {
+ entry.slot = NUM_SLOTS;
+ entry.iter = 0;
+ } else {
+ long ticks = 1 + (int)((delayMillis + MILLIS_PER_SLOT / 2) / MILLIS_PER_SLOT);
+ entry.slot = (int)((ticks + currSlot) & SLOT_MASK);
+ entry.iter = currIter + (int)((ticks + currSlot) >> ITER_SHIFT);
+ }
+ linkIn(entry);
+ }
+
+ private synchronized void unschedule(Entry entry) {
+ if (entry.next != null) {
+ linkOut(entry);
+ }
+ }
+
+ private void linkIn(Entry entry) {
+ Entry head = slots[entry.slot];
+ if (head == null) {
+ entry.next = entry;
+ entry.prev = entry;
+ slots[entry.slot] = entry;
+ } else {
+ entry.next = head;
+ entry.prev = head.prev;
+ head.prev.next = entry;
+ head.prev = entry;
+ }
+ ++counts[entry.slot];
+ }
+
+ private void linkOut(Entry entry) {
+ Entry head = slots[entry.slot];
+ if (entry.next == entry) {
+ slots[entry.slot] = null;
+ } else {
+ entry.prev.next = entry.next;
+ entry.next.prev = entry.prev;
+ if (head == entry) {
+ slots[entry.slot] = entry.next;
+ }
+ }
+ entry.next = null;
+ entry.prev = null;
+ --counts[entry.slot];
+ }
+
+ public class Entry {
+
+ private final Object payload;
+ private int slot;
+ private int iter;
+ private Entry next;
+ private Entry prev;
+
+ private Entry(Object payload) {
+ this.payload = payload;
+ }
+
+ public void scheduleAt(long expireAtMillis) {
+ ScheduledQueue.this.scheduleAt(this, expireAtMillis);
+ }
+
+ public void unschedule() {
+ ScheduledQueue.this.unschedule(this);
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/SystemTimer.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/SystemTimer.java
new file mode 100644
index 00000000000..371ab52f26b
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/SystemTimer.java
@@ -0,0 +1,17 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import com.yahoo.jdisc.Timer;
+
+/**
+ * A timer which returns the System time
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class SystemTimer implements Timer {
+
+ @Override
+ public long currentTimeMillis() {
+ return System.currentTimeMillis();
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/TimeoutManagerImpl.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/TimeoutManagerImpl.java
new file mode 100644
index 00000000000..8e0c624b348
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/TimeoutManagerImpl.java
@@ -0,0 +1,244 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.core;
+
+import com.google.inject.Inject;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.ResourceReference;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.TimeoutManager;
+import com.yahoo.jdisc.Timer;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.handler.ResponseHandler;
+
+import java.nio.ByteBuffer;
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class TimeoutManagerImpl {
+
+ private static final ContentChannel IGNORED_CONTENT = new IgnoredContent();
+ private static final Logger log = Logger.getLogger(TimeoutManagerImpl.class.getName());
+ private final ScheduledQueue schedules[] = new ScheduledQueue[Runtime.getRuntime().availableProcessors()];
+ private final Thread thread;
+ private final Timer timer;
+ private volatile int nextScheduler = 0;
+ private volatile int queueSize = 0;
+ private volatile boolean done = false;
+
+ @Inject
+ public TimeoutManagerImpl(ThreadFactory factory, Timer timer) {
+ this.thread = factory.newThread(new ManagerTask());
+ this.thread.setName(getClass().getName());
+ this.timer = timer;
+
+ long now = timer.currentTimeMillis();
+ for (int i = 0; i < schedules.length; ++i) {
+ schedules[i] = new ScheduledQueue(now);
+ }
+ }
+
+ public void start() {
+ thread.start();
+ }
+
+ public void shutdown() {
+ done = true;
+ }
+
+ public RequestHandler manageHandler(RequestHandler handler) {
+ return new ManagedRequestHandler(handler);
+ }
+
+ int queueSize() {
+ return queueSize; // unstable snapshot, only for test purposes
+ }
+
+ Timer timer() {
+ return timer;
+ }
+
+ void checkTasks(long currentTimeMillis) {
+ Queue<Object> queue = new LinkedList<>();
+ for (ScheduledQueue schedule : schedules) {
+ schedule.drainTo(currentTimeMillis, queue);
+ }
+ while (!queue.isEmpty()) {
+ TimeoutHandler timeoutHandler = (TimeoutHandler)queue.poll();
+ invokeTimeout(timeoutHandler.requestHandler, timeoutHandler.request, timeoutHandler);
+ }
+ }
+
+ private void invokeTimeout(RequestHandler requestHandler, Request request, ResponseHandler responseHandler) {
+ try {
+ requestHandler.handleTimeout(request, responseHandler);
+ } catch (RuntimeException e) {
+ log.log(Level.WARNING, "Ignoring exception thrown by " + requestHandler.getClass().getName() +
+ " in timeout manager.", e);
+ }
+ if (Thread.currentThread().isInterrupted()) {
+ log.log(Level.WARNING, "Ignoring interrupt signal from " + requestHandler.getClass().getName() +
+ " in timeout manager.");
+ Thread.interrupted();
+ }
+ }
+
+ private class ManagerTask implements Runnable {
+
+ @Override
+ public void run() {
+ while (!done) {
+ try {
+ Thread.sleep(ScheduledQueue.MILLIS_PER_SLOT);
+ } catch (InterruptedException e) {
+ log.log(Level.WARNING, "Ignoring interrupt signal in timeout manager.", e);
+ }
+ checkTasks(timer.currentTimeMillis());
+ }
+ }
+ }
+
+ private class ManagedRequestHandler implements RequestHandler {
+
+ final RequestHandler delegate;
+
+ ManagedRequestHandler(RequestHandler delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler responseHandler) {
+ TimeoutHandler timeoutHandler = new TimeoutHandler(request, delegate, responseHandler);
+ request.setTimeoutManager(timeoutHandler);
+ try {
+ return delegate.handleRequest(request, timeoutHandler);
+ } catch (Throwable throwable) {
+ //This is only needed when this method is invoked outside of Request.connect,
+ //and that seems to be the case for jetty right now.
+ //To prevent this from being called outside Request.connect,
+ //manageHandler() and com.yahoo.jdisc.Container.resolveHandler() must also be made non-public.
+ //
+ //The underlying framework will handle the request,
+ //the application code is no longer responsible for calling responseHandler.handleResponse.
+ timeoutHandler.unscheduleTimeout();
+ throw throwable;
+ }
+ }
+
+ @Override
+ public void handleTimeout(Request request, ResponseHandler responseHandler) {
+ delegate.handleTimeout(request, responseHandler);
+ }
+
+ @Override
+ public ResourceReference refer() {
+ return delegate.refer();
+ }
+
+ @Override
+ public void release() {
+ delegate.release();
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+ }
+
+ private class TimeoutHandler implements ResponseHandler, TimeoutManager {
+
+ final ResponseHandler responseHandler;
+ final RequestHandler requestHandler;
+ final Request request;
+ ScheduledQueue.Entry timeoutQueueEntry = null;
+ boolean responded = false;
+
+ TimeoutHandler(Request request, RequestHandler requestHandler, ResponseHandler responseHandler) {
+ this.request = request;
+ this.requestHandler = requestHandler;
+ this.responseHandler = responseHandler;
+ }
+
+ @Override
+ public synchronized void scheduleTimeout(Request request) {
+ if (responded) {
+ return;
+ }
+ if (timeoutQueueEntry == null) {
+ timeoutQueueEntry = schedules[(++nextScheduler & 0xffff) % schedules.length].newEntry(this);
+ }
+ timeoutQueueEntry.scheduleAt(request.creationTime(TimeUnit.MILLISECONDS) + request.getTimeout(TimeUnit.MILLISECONDS));
+ ++queueSize;
+ }
+
+ synchronized void unscheduleTimeout() {
+ if (!responded && timeoutQueueEntry != null) {
+ timeoutQueueEntry.unschedule();
+ //guard against unscheduling from ManagedRequestHandler.handleRequest catch block
+ //followed by unscheduling in another thread from TimeoutHandler.handleResponse
+ timeoutQueueEntry = null;
+ }
+ --queueSize;
+ }
+
+ @Override
+ public void unscheduleTimeout(Request request) {
+ unscheduleTimeout();
+ }
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ synchronized (this) {
+ unscheduleTimeout();
+ if (responded) {
+ return IGNORED_CONTENT;
+ }
+ responded = true;
+ }
+ return responseHandler.handleResponse(response);
+ }
+
+ @Override
+ public String toString() {
+ return responseHandler.toString();
+ }
+ }
+
+ private static class IgnoredContent implements ContentChannel {
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ if (handler == null) {
+ return;
+ }
+ try {
+ handler.completed();
+ } catch (RuntimeException e) {
+ log.log(Level.WARNING, "Ignoring exception thrown by " + handler.getClass().getName() +
+ " in timeout manager.", e);
+ }
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ if (handler == null) {
+ return;
+ }
+ try {
+ handler.completed();
+ } catch (RuntimeException e) {
+ log.log(Level.WARNING, "Ignoring exception thrown by " + handler.getClass().getName() +
+ " in timeout manager.", e);
+ }
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/AbstractContentOutputStream.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/AbstractContentOutputStream.java
new file mode 100644
index 00000000000..de656842f10
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/AbstractContentOutputStream.java
@@ -0,0 +1,68 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+abstract class AbstractContentOutputStream extends OutputStream {
+
+ public static final int BUFFERSIZE = 4096;
+ private ByteBuffer current;
+
+ @Override
+ public final void write(int b) {
+ if (current == null) {
+ current = ByteBuffer.allocate(BUFFERSIZE);
+ }
+ current.put((byte)b);
+ if (current.remaining() == 0) {
+ flush();
+ }
+ }
+
+ @Override
+ public final void write(byte[] buf, int offset, int length) {
+ Objects.requireNonNull(buf, "buf");
+ if (current == null) {
+ current = ByteBuffer.allocate(BUFFERSIZE + length);
+ }
+ int part = Math.min(length, current.remaining());
+ current.put(buf, offset, part);
+ if (current.remaining() == 0) {
+ flush();
+ }
+ if (part < length) {
+ write(buf, offset + part, length - part);
+ }
+ }
+
+ @Override
+ public final void write(byte[] buf) {
+ write(buf, 0, buf.length);
+ }
+
+ @Override
+ public final void flush() {
+ if (current == null || current.position() == 0) {
+ return;
+ }
+ ByteBuffer buf = current;
+ current = null;
+ buf.flip();
+ doFlush(buf);
+ }
+
+ @Override
+ public final void close() {
+ flush();
+ doClose();
+ }
+
+ protected abstract void doFlush(ByteBuffer buf);
+
+ protected abstract void doClose();
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/AbstractRequestHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/AbstractRequestHandler.java
new file mode 100644
index 00000000000..9bc934cf724
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/AbstractRequestHandler.java
@@ -0,0 +1,36 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+
+/**
+ * <p>This class provides an abstract {@link RequestHandler} implementation with reasonable defaults for everything but
+ * {@link #handleRequest(Request, ResponseHandler)}.</p>
+ *
+ * <p>A very simple hello world handler could be implemented like this:</p>
+ * <pre>
+ * class HelloWorldHandler extends AbstractRequestHandler {
+ *
+ * &#64;Override
+ * public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ * ContentWriter writer = ResponseDispatch.newInstance(Response.Status.OK).connectWriter(handler);
+ * try {
+ * writer.write("Hello World!");
+ * } finally {
+ * writer.close();
+ * }
+ * return null;
+ * }
+ * }
+ * </pre>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public abstract class AbstractRequestHandler extends com.yahoo.jdisc.AbstractResource implements RequestHandler {
+
+ @Override
+ public void handleTimeout(Request request, ResponseHandler responseHandler) {
+ Response.dispatchTimeout(responseHandler);
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/BindingNotFoundException.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/BindingNotFoundException.java
new file mode 100644
index 00000000000..8cfb894b6bf
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/BindingNotFoundException.java
@@ -0,0 +1,38 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.application.BindingSet;
+
+import java.net.URI;
+
+/**
+ * This exception is used to signal that no binding was found for the {@link URI} of a given {@link Request}. An
+ * instance of this class will be thrown by the {@link Request#connect(ResponseHandler)} method when the current {@link
+ * BindingSet} has not binding that matches the corresponding Request's URI.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class BindingNotFoundException extends RuntimeException {
+
+ private final URI uri;
+
+ /**
+ * Constructs a new instance of this class with a detail message that contains the {@link URI} that has no binding.
+ *
+ * @param uri The URI that has no binding.
+ */
+ public BindingNotFoundException(URI uri) {
+ super("No binding for URI '" + uri + "'.");
+ this.uri = uri;
+ }
+
+ /**
+ * Returns the {@link URI} that has no binding.
+ *
+ * @return The URI.
+ */
+ public URI uri() {
+ return uri;
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/BlockingContentWriter.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/BlockingContentWriter.java
new file mode 100644
index 00000000000..fc30ee11faf
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/BlockingContentWriter.java
@@ -0,0 +1,78 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.ExecutionException;
+import java.util.Objects;
+
+/**
+ * <p>This class provides a blocking <em>write</em>-interface to a {@link ContentChannel}. Both {@link
+ * #write(ByteBuffer)} and {@link #close()} methods provide an internal {@link CompletionHandler} to the decorated
+ * {@link ContentChannel} calls, and wait for these to be called before returning. If {@link
+ * CompletionHandler#failed(Throwable)} is called, the corresponding Throwable is thrown to the caller.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ * @see FastContentWriter
+ */
+public final class BlockingContentWriter {
+
+ private final ContentChannel channel;
+
+ /**
+ * <p>Creates a new BlockingContentWriter that encapsulates a given {@link ContentChannel}.</p>
+ *
+ * @param content The ContentChannel to encapsulate.
+ * @throws NullPointerException If the <em>content</em> argument is null.
+ */
+ public BlockingContentWriter(ContentChannel content) {
+ Objects.requireNonNull(content, "content");
+ this.channel = content;
+ }
+
+ /**
+ * <p>Writes to the underlying {@link ContentChannel} and waits for the operation to complete.</p>
+ *
+ * @param buf The ByteBuffer to write.
+ * @throws InterruptedException If the thread was interrupted while waiting.
+ * @throws RuntimeException If the operation failed to complete, see cause for details.
+ */
+ public void write(ByteBuffer buf) throws InterruptedException {
+ try {
+ FutureCompletion future = new FutureCompletion();
+ channel.write(buf, future);
+ future.get();
+ } catch (ExecutionException e) {
+ Throwable t = e.getCause();
+ if (t instanceof RuntimeException) {
+ throw (RuntimeException)t;
+ }
+ if (t instanceof Error) {
+ throw (Error)t;
+ }
+ throw new RuntimeException(t);
+ }
+ }
+
+ /**
+ * <p>Closes the underlying {@link ContentChannel} and waits for the operation to complete.</p>
+ *
+ * @throws InterruptedException If the thread was interrupted while waiting.
+ * @throws RuntimeException If the operation failed to complete, see cause for details.
+ */
+ public void close() throws InterruptedException {
+ try {
+ FutureCompletion future = new FutureCompletion();
+ channel.close(future);
+ future.get();
+ } catch (ExecutionException e) {
+ Throwable t = e.getCause();
+ if (t instanceof RuntimeException) {
+ throw (RuntimeException)t;
+ }
+ if (t instanceof Error) {
+ throw (Error)t;
+ }
+ throw new RuntimeException(t);
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/BufferedContentChannel.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/BufferedContentChannel.java
new file mode 100644
index 00000000000..79bd340df55
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/BufferedContentChannel.java
@@ -0,0 +1,156 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import java.nio.ByteBuffer;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * <p>This class implements an unlimited, non-blocking content queue. All {@link ContentChannel} methods are implemented
+ * by pushing to a thread-safe internal queue. All of the queued calls are forwarded to another ContentChannel when
+ * {@link #connectTo(ContentChannel)} is called. Once connected, this class becomes a non-buffering proxy for the
+ * connected ContentChannel.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class BufferedContentChannel implements ContentChannel {
+
+ private final Object lock = new Object();
+ private List<Entry> queue = new LinkedList<>();
+ private ContentChannel content = null;
+ private boolean closed = false;
+ private CompletionHandler closeCompletion = null;
+
+ /**
+ * <p>Connects this BufferedContentChannel to a ContentChannel. First, this method forwards all queued calls to the
+ * connected ContentChannel. Once this method has been called, all future calls to {@link #write(ByteBuffer,
+ * CompletionHandler)} and {@link #close(CompletionHandler)} are synchronously forwarded to the connected
+ * ContentChannel.</p>
+ *
+ * @param content The ContentChannel to connect to.
+ * @throws NullPointerException If the <em>content</em> argument is null.
+ * @throws IllegalStateException If another ContentChannel has already been connected.
+ */
+ public void connectTo(ContentChannel content) {
+ Objects.requireNonNull(content, "content");
+ boolean closed;
+ List<Entry> queue;
+ synchronized (lock) {
+ if (this.content != null || this.queue == null) {
+ throw new IllegalStateException();
+ }
+ closed = this.closed;
+ queue = this.queue;
+ this.queue = null;
+ }
+ for (Entry entry : queue) {
+ content.write(entry.buf, entry.handler);
+ }
+ if (closed) {
+ content.close(closeCompletion);
+ }
+ synchronized (lock) {
+ this.content = content;
+ lock.notifyAll();
+ }
+ }
+
+ /**
+ * <p>Returns whether or not {@link #connectTo(ContentChannel)} has been called. Even if this method returns false,
+ * calling {@link #connectTo(ContentChannel)} might still throw an IllegalStateException if there is a race.</p>
+ *
+ * @return True if {@link #connectTo(ContentChannel)} has been called.
+ */
+ public boolean isConnected() {
+ synchronized (lock) {
+ return content != null;
+ }
+ }
+
+ /**
+ * <p>Creates a {@link ReadableContentChannel} and {@link #connectTo(ContentChannel) connects} to it. </p>
+ *
+ * @return The new ReadableContentChannel that this connected to.
+ */
+ public ReadableContentChannel toReadable() {
+ ReadableContentChannel ret = new ReadableContentChannel();
+ connectTo(ret);
+ return ret;
+ }
+
+ /**
+ * <p>Creates a {@link ContentInputStream} and {@link #connectTo(ContentChannel) connects} to its internal
+ * ContentChannel.</p>
+ *
+ * @return The new ContentInputStream that this connected to.
+ */
+ public ContentInputStream toStream() {
+ return toReadable().toStream();
+ }
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ ContentChannel content;
+ synchronized (lock) {
+ if (closed) {
+ throw new IllegalStateException();
+ }
+ if (queue != null) {
+ queue.add(new Entry(buf, handler));
+ return;
+ }
+ try {
+ while (this.content == null) {
+ lock.wait(); // waiting for connecTo()
+ }
+ } catch (InterruptedException e) {
+ throw new IllegalStateException(e);
+ }
+ if (closed) {
+ throw new IllegalStateException();
+ }
+ content = this.content;
+ }
+ content.write(buf, handler);
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ ContentChannel content;
+ synchronized (lock) {
+ if (closed) {
+ throw new IllegalStateException();
+ }
+ if (queue != null) {
+ closed = true;
+ closeCompletion = handler;
+ return;
+ }
+ try {
+ while (this.content == null) {
+ lock.wait(); // waiting for connecTo()
+ }
+ } catch (InterruptedException e) {
+ throw new IllegalStateException(e);
+ }
+ if (closed) {
+ throw new IllegalStateException();
+ }
+ closed = true;
+ content = this.content;
+ }
+ content.close(handler);
+ }
+
+ private static class Entry {
+
+ final ByteBuffer buf;
+ final CompletionHandler handler;
+
+ Entry(ByteBuffer buf, CompletionHandler handler) {
+ this.handler = handler;
+ this.buf = buf;
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/CallableRequestDispatch.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/CallableRequestDispatch.java
new file mode 100644
index 00000000000..06421b2bfe2
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/CallableRequestDispatch.java
@@ -0,0 +1,22 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import com.yahoo.jdisc.Response;
+
+import java.util.concurrent.Callable;
+
+/**
+ * <p>This is a convenient subclass of {@link RequestDispatch} that implements the {@link Callable} interface. This
+ * should be used in place of {@link RequestDispatch} if you intend to schedule its execution. Because {@link #call()}
+ * does not return until a {@link Response} becomes available, you can use the <tt>Future</tt> return value of
+ * <tt>ExecutorService.submit(Callable)</tt> to wait for it.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public abstract class CallableRequestDispatch extends RequestDispatch implements Callable<Response> {
+
+ @Override
+ public final Response call() throws Exception {
+ return dispatch().get();
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/CallableResponseDispatch.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/CallableResponseDispatch.java
new file mode 100644
index 00000000000..9a22ec1c0e4
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/CallableResponseDispatch.java
@@ -0,0 +1,34 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import com.yahoo.jdisc.Response;
+
+import java.util.concurrent.Callable;
+
+/**
+ * <p>This is a convenient subclass of {@link ResponseDispatch} that implements the {@link Callable} interface. This
+ * should be used in place of {@link ResponseDispatch} if you intend to schedule its execution. Because {@link #call()}
+ * does not return until the entirety of the {@link Response} and its content have been consumed, you can use the
+ * <tt>Future</tt> return value of <tt>ExecutorService.submit(Callable)</tt> to wait for it to complete.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public abstract class CallableResponseDispatch extends ResponseDispatch implements Callable<Boolean> {
+
+ private final ResponseHandler handler;
+
+ /**
+ * <p>Constructs a new instances of this class over the given {@link ResponseHandler}. Invoking {@link #call()} will
+ * dispatch to this handler.</p>
+ *
+ * @param handler The ResponseHandler to dispatch to.
+ */
+ public CallableResponseDispatch(ResponseHandler handler) {
+ this.handler = handler;
+ }
+
+ @Override
+ public final Boolean call() throws Exception {
+ return dispatch(handler).get();
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/CompletionHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/CompletionHandler.java
new file mode 100644
index 00000000000..ca2e61fff52
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/CompletionHandler.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import com.yahoo.jdisc.Container;
+
+/**
+ * This interface defines a handler for consuming the result of an asynchronous I/O operation.
+ * <p>
+ * The asynchronous channels defined in this package allow a completion handler to be specified to consume the result of
+ * an asynchronous operation. The {@link #completed()} method is invoked when the I/O operation completes successfully.
+ * The {@link #failed(Throwable)} method is invoked if the I/O operations fails. The implementations of these methods
+ * should complete in a timely manner so as to avoid keeping the invoking thread from dispatching to other completion
+ * handlers.
+ * <p>
+ * Because a CompletionHandler might have a completely different lifespan than the originating ContentChannel objects,
+ * all instances of this interface are internally backed by a reference to the {@link Container} that was active when
+ * the initial Request was created. This ensures that the configured environment of the CompletionHandler is stable
+ * throughout its lifetime. This also means that the either {@link #completed()} or {@link #failed(Throwable)} MUST be
+ * called in order to release that reference. Failure to do so will prevent the Container from ever shutting down.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface CompletionHandler {
+
+ /**
+ * Invoked when an operation has completed. Notice that you MUST call either this or {@link #failed(Throwable)} to
+ * release the internal {@link Container} reference. Failure to do so will prevent the Container from ever shutting
+ * down.
+ */
+ public void completed();
+
+ /**
+ * Invoked when an operation fails. Notice that you MUST call either this or {@link #completed()} to release the
+ * internal {@link Container} reference. Failure to do so will prevent the Container from ever shutting down.
+ *
+ * @param t The exception to indicate why the I/O operation failed.
+ */
+ public void failed(Throwable t);
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ContentChannel.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ContentChannel.java
new file mode 100644
index 00000000000..7a4a6e46fe7
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ContentChannel.java
@@ -0,0 +1,49 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+
+import java.nio.ByteBuffer;
+
+/**
+ * This interface defines a callback for asynchronously writing the content of a {@link Request} or a {@link Response}
+ * to a recipient. It is the returned both by {@link RequestHandler#handleRequest(Request, ResponseHandler)} and {@link
+ * ResponseHandler#handleResponse(Response)}. Note that methods of this channel only <em>schedule</em> the appropriate
+ * action - if you need to act on the result you will need submit a {@link CompletionHandler} to the appropriate method.
+ * <p>
+ * Because a ContentChannel might have a different lifespan than the originating Request and Response
+ * objects, all instances of this interface are internally backed by a reference to the {@link Container} that was
+ * active when the initial Request was created. This ensures that the configured environment of the ContentChannel is
+ * stable throughout its lifetime. This also means that the {@link #close(CompletionHandler)} method MUST be called in
+ * order to release that reference. Failure to do so will prevent the Container from ever shutting down. This
+ * requirement is regardless of any errors that may occur while calling any of its other methods or its derived {@link
+ * CompletionHandler}s.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface ContentChannel {
+
+ /**
+ * Schedules the given {@link ByteBuffer} to be written to the content corresponding to this ContentChannel. This
+ * call <em>transfers ownership</em> of the given ByteBuffer to this ContentChannel, i.e. no further calls can be
+ * made to the buffer. The execution of writes happen in the same order as this method was invoked.
+ *
+ * @param buf The {@link ByteBuffer} to schedule for write. No further calls can be made to this buffer.
+ * @param handler The {@link CompletionHandler} to call after the write has been executed.
+ */
+ public void write(ByteBuffer buf, CompletionHandler handler);
+
+ /**
+ * Closes this ContentChannel. After a channel is closed, any further attempt to invoke {@link #write(ByteBuffer,
+ * CompletionHandler)} upon it will cause an {@link IllegalStateException} to be thrown. If this channel is already
+ * closed then invoking this method has no effect, but {@link CompletionHandler#completed()} will still be called.
+ *
+ * Notice that you MUST call this method, regardless of any exceptions that might have occurred while writing to this
+ * ContentChannel. Failure to do so will prevent the {@link Container} from ever shutting down.
+ *
+ * @param handler The {@link CompletionHandler} to call after the close has been executed.
+ */
+ public void close(CompletionHandler handler);
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ContentInputStream.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ContentInputStream.java
new file mode 100644
index 00000000000..d59bb893a2f
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ContentInputStream.java
@@ -0,0 +1,29 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+/**
+ * <p>This class extends {@link UnsafeContentInputStream} and adds a finalizer to it that calls {@link #close()}. This
+ * has a performance impact, but ensures that an unclosed stream does not prevent shutdown.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class ContentInputStream extends UnsafeContentInputStream {
+
+ /**
+ * <p>Constructs a new ContentInputStream that reads from the given {@link ReadableContentChannel}.</p>
+ *
+ * @param content The content to read the stream from.
+ */
+ public ContentInputStream(ReadableContentChannel content) {
+ super(content);
+ }
+
+ @Override
+ public void finalize() throws Throwable {
+ try {
+ close();
+ } finally {
+ super.finalize();
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FastContentOutputStream.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FastContentOutputStream.java
new file mode 100644
index 00000000000..eed3210f57e
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FastContentOutputStream.java
@@ -0,0 +1,85 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.nio.ByteBuffer;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * <p>This class extends the {@link AbstractContentOutputStream}, and forwards all write() and close() calls to a {@link
+ * FastContentWriter}. This means that once {@link #close()} has been called, the asynchronous completion of all pending
+ * operations can be awaited using the ListenableFuture interface of this class. Any asynchronous failure will be
+ * rethrown when calling either of the get() methods on this class.</p>
+ * <p>Please notice that the Future implementation of this class will NEVER complete unless {@link #close()} has been
+ * called.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class FastContentOutputStream extends AbstractContentOutputStream implements ListenableFuture<Boolean> {
+
+ private final FastContentWriter out;
+
+ /**
+ * <p>Constructs a new FastContentOutputStream that writes into the given {@link ContentChannel}.</p>
+ *
+ * @param out The ContentChannel to write the stream into.
+ */
+ public FastContentOutputStream(ContentChannel out) {
+ this(new FastContentWriter(out));
+ }
+
+ /**
+ * <p>Constructs a new FastContentOutputStream that writes into the given {@link FastContentWriter}.</p>
+ *
+ * @param out The ContentWriter to write the stream into.
+ */
+ public FastContentOutputStream(FastContentWriter out) {
+ Objects.requireNonNull(out, "out");
+ this.out = out;
+ }
+
+ @Override
+ protected void doFlush(ByteBuffer buf) {
+ out.write(buf);
+ }
+
+ @Override
+ protected void doClose() {
+ out.close();
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ return out.cancel(mayInterruptIfRunning);
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return out.isCancelled();
+ }
+
+ @Override
+ public boolean isDone() {
+ return out.isDone();
+ }
+
+ @Override
+ public Boolean get() throws InterruptedException, ExecutionException {
+ return out.get();
+ }
+
+ @Override
+ public Boolean get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+ return out.get(timeout, unit);
+ }
+
+ @Override
+ public void addListener(Runnable listener, Executor executor) {
+ out.addListener(listener, executor);
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FastContentWriter.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FastContentWriter.java
new file mode 100644
index 00000000000..5c6e8334891
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FastContentWriter.java
@@ -0,0 +1,156 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * <p>This class provides a non-blocking, awaitable <em>write</em>-interface to a {@link ContentChannel}.
+ * The ListenableFuture&lt;Boolean&gt; interface can be used to await
+ * the asynchronous completion of all pending operations. Any asynchronous
+ * failure will be rethrown when calling either of the get() methods on
+ * this class.</p>
+ * <p>Please notice that the Future implementation of this class will NEVER complete unless {@link #close()} has been
+ * called; please use try-with-resources to ensure that close() is called.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class FastContentWriter implements ListenableFuture<Boolean>, AutoCloseable {
+
+ private final AtomicBoolean closed = new AtomicBoolean(false);
+ private final AtomicInteger numPendingCompletions = new AtomicInteger();
+ private final CompletionHandler completionHandler = new SimpleCompletionHandler();
+ private final ContentChannel out;
+ private final SettableFuture<Boolean> future = SettableFuture.create();
+
+ /**
+ * <p>Creates a new FastContentWriter that encapsulates a given {@link ContentChannel}.</p>
+ *
+ * @param out The ContentChannel to encapsulate.
+ * @throws NullPointerException If the <em>content</em> argument is null.
+ */
+ public FastContentWriter(ContentChannel out) {
+ Objects.requireNonNull(out, "out");
+ this.out = out;
+ }
+
+ /**
+ * <p>This is a convenience method to convert the given string to a ByteBuffer of UTF8 bytes, and then passing that
+ * to {@link #write(ByteBuffer)}.</p>
+ *
+ * @param str The string to write.
+ */
+ public void write(String str) {
+ write(str.getBytes(StandardCharsets.UTF_8));
+ }
+
+ /**
+ * <p>This is a convenience method to convert the given byte array into a ByteBuffer object, and then passing that
+ * to {@link #write(java.nio.ByteBuffer)}.</p>
+ *
+ * @param buf The bytes to write.
+ */
+ public void write(byte[] buf) {
+ write(buf, 0, buf.length);
+ }
+
+ /**
+ * <p>This is a convenience method to convert a subarray of the given byte array into a ByteBuffer object, and then
+ * passing that to {@link #write(java.nio.ByteBuffer)}.</p>
+ *
+ * @param buf The bytes to write.
+ * @param offset The offset of the subarray to be used.
+ * @param length The length of the subarray to be used.
+ */
+ public void write(byte[] buf, int offset, int length) {
+ write(ByteBuffer.wrap(buf, offset, length));
+ }
+
+ /**
+ * <p>Writes to the underlying {@link ContentChannel}. If {@link CompletionHandler#failed(Throwable)} is called,
+ * either of the get() methods will rethrow that Throwable.</p>
+ *
+ * @param buf The ByteBuffer to write.
+ */
+ public void write(ByteBuffer buf) {
+ numPendingCompletions.incrementAndGet();
+ try {
+ out.write(buf, completionHandler);
+ } catch (Throwable t) {
+ future.setException(t);
+ throw t;
+ }
+ }
+
+ /**
+ * <p>Closes the underlying {@link ContentChannel}. If {@link CompletionHandler#failed(Throwable)} is called,
+ * either of the get() methods will rethrow that Throwable.</p>
+ */
+ @Override
+ public void close() {
+ numPendingCompletions.incrementAndGet();
+ closed.set(true);
+ try {
+ out.close(completionHandler);
+ } catch (Throwable t) {
+ future.setException(t);
+ throw t;
+ }
+ }
+
+ @Override
+ public void addListener(Runnable listener, Executor executor) {
+ future.addListener(listener, executor);
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public boolean isDone() {
+ return future.isDone();
+ }
+
+ @Override
+ public Boolean get() throws InterruptedException, ExecutionException {
+ return future.get();
+ }
+
+ @Override
+ public Boolean get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+ return future.get(timeout, unit);
+ }
+
+ private class SimpleCompletionHandler implements CompletionHandler {
+
+ @Override
+ public void completed() {
+ numPendingCompletions.decrementAndGet();
+ if (closed.get() && numPendingCompletions.get() == 0) {
+ future.set(true);
+ }
+ }
+
+ @Override
+ public void failed(Throwable t) {
+ future.setException(t);
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureCompletion.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureCompletion.java
new file mode 100644
index 00000000000..ed26678c7ac
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureCompletion.java
@@ -0,0 +1,37 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import com.google.common.util.concurrent.AbstractFuture;
+
+/**
+ * <p>This class provides an implementation of {@link CompletionHandler} that allows you to wait for either {@link
+ * #completed()} or {@link #failed(Throwable)} to be called. If failed() was called, the corresponding Throwable will
+ * be rethrown when calling either of the get() methods. Unless an exception is thrown, the get() methods will always
+ * return Boolean.TRUE.</p>
+ *
+ * <p>Notice that calling {@link #cancel(boolean)} throws an UnsupportedOperationException.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class FutureCompletion extends AbstractFuture<Boolean> implements CompletionHandler {
+
+ @Override
+ public void completed() {
+ set(true);
+ }
+
+ @Override
+ public void failed(Throwable t) {
+ setException(t);
+ }
+
+ @Override
+ public final boolean cancel(boolean mayInterruptIfRunning) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final boolean isCancelled() {
+ return false;
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureConjunction.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureConjunction.java
new file mode 100644
index 00000000000..eebb0ea266b
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureConjunction.java
@@ -0,0 +1,97 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.JdkFutureAdapters;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.*;
+
+/**
+ * <p>This class implements a Future&lt;Boolean&gt; that is conjunction of zero or more other Future&lt;Boolean&gt;s,
+ * i.e. it evaluates to <tt>true</tt> if, and only if, all its operands evaluate to <tt>true</tt>. To use this class,
+ * simply create an instance of it and add operands to it using the {@link #addOperand(ListenableFuture)} method.</p>
+ * TODO: consider rewriting usage of FutureConjunction to use CompletableFuture instead.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class FutureConjunction implements ListenableFuture<Boolean> {
+
+ private final List<ListenableFuture<Boolean>> operands = new LinkedList<>();
+
+ /**
+ * <p>Adds a ListenableFuture&lt;Boolean&gt; to this conjunction. This can be called at any time, even after having called
+ * {@link #get()} previously.</p>
+ *
+ * @param operand The operand to add to this conjunction.
+ */
+ public void addOperand(ListenableFuture<Boolean> operand) {
+ operands.add(operand);
+ }
+
+ @Override
+ public void addListener(Runnable listener, Executor executor) {
+ Futures.allAsList(operands).addListener(listener, executor);
+ }
+
+ @Override
+ public final boolean cancel(boolean mayInterruptIfRunning) {
+ boolean ret = true;
+ for (Future<Boolean> op : operands) {
+ if (!op.cancel(mayInterruptIfRunning)) {
+ ret = false;
+ }
+ }
+ return ret;
+ }
+
+ @Override
+ public final boolean isCancelled() {
+ for (Future<Boolean> op : operands) {
+ if (!op.isCancelled()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public final boolean isDone() {
+ for (Future<Boolean> op : operands) {
+ if (!op.isDone()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public final Boolean get() throws InterruptedException, ExecutionException {
+ Boolean ret = Boolean.TRUE;
+ for (Future<Boolean> op : operands) {
+ if (!op.get()) {
+ ret = Boolean.FALSE;
+ }
+ }
+ return ret;
+ }
+
+ @Override
+ public final Boolean get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException,
+ TimeoutException {
+ Boolean ret = Boolean.TRUE;
+ long nanos = unit.toNanos(timeout);
+ long lastTime = System.nanoTime();
+ for (Future<Boolean> op : operands) {
+ if (!op.get(nanos, TimeUnit.NANOSECONDS)) {
+ ret = Boolean.FALSE;
+ }
+ long now = System.nanoTime();
+ nanos -= now - lastTime;
+ lastTime = now;
+ }
+ return ret;
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureResponse.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureResponse.java
new file mode 100644
index 00000000000..ce772ff0340
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/FutureResponse.java
@@ -0,0 +1,66 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import com.google.common.util.concurrent.AbstractFuture;
+import com.yahoo.jdisc.Response;
+
+/**
+ * <p>This class provides an implementation of {@link ResponseHandler} that allows you to wait for a {@link Response} to
+ * be returned.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class FutureResponse extends AbstractFuture<Response> implements ResponseHandler {
+
+ private final ResponseHandler handler;
+
+ /**
+ * <p>Constructs a new FutureResponse that returns a {@link NullContent} when {@link #handleResponse(Response)} is
+ * invoked.</p>
+ */
+ public FutureResponse() {
+ this(NullContent.INSTANCE);
+ }
+
+ /**
+ * <p>Constructs a new FutureResponse that returns the given {@link ContentChannel} when {@link
+ * #handleResponse(Response)} is invoked.</p>
+ *
+ * @param content The content channel for the Response.
+ */
+ public FutureResponse(final ContentChannel content) {
+ this(new ResponseHandler() {
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ return content;
+ }
+ });
+ }
+
+ /**
+ * <p>Constructs a new FutureResponse that calls the given {@link ResponseHandler} when {@link
+ * #handleResponse(Response)} is invoked.</p>
+ *
+ * @param handler The ResponseHandler to invoke.
+ */
+ public FutureResponse(ResponseHandler handler) {
+ this.handler = handler;
+ }
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ set(response);
+ return handler.handleResponse(response);
+ }
+
+ @Override
+ public final boolean cancel(boolean mayInterruptIfRunning) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final boolean isCancelled() {
+ return false;
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/NullContent.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/NullContent.java
new file mode 100644
index 00000000000..e231674ad30
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/NullContent.java
@@ -0,0 +1,42 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import com.yahoo.jdisc.Request;
+
+import java.nio.ByteBuffer;
+
+/**
+ * <p>This class provides a convenient implementation of {@link ContentChannel} that does not support being written to.
+ * If {@link #write(ByteBuffer, CompletionHandler)} is called, it throws an UnsupportedOperationException. If {@link
+ * #close(CompletionHandler)} is called, it calls the given {@link CompletionHandler}.</p>
+ *
+ * <p>A {@link RequestHandler}s that does not expect content can simply return the {@link #INSTANCE} of this class for
+ * every invocation of its {@link RequestHandler#handleRequest(Request, ResponseHandler)}.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class NullContent implements ContentChannel {
+
+ public static final NullContent INSTANCE = new NullContent();
+
+ private NullContent() {
+ // hide
+ }
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ if (buf.hasRemaining()) {
+ throw new UnsupportedOperationException();
+ }
+ if (handler != null) {
+ handler.completed();
+ }
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ if (handler != null) {
+ handler.completed();
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/OverloadException.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/OverloadException.java
new file mode 100644
index 00000000000..22bd5cc14c7
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/OverloadException.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+/**
+ * An exception to signal abort current action, as the container is overloaded.
+ * Just unroll state as cheaply as possible.
+ *
+ * <p>
+ * The contract of OverloadException (for Jetty) is:
+ * </p>
+ *
+ * <ol>
+ * <li>You must set the response yourself first, or you'll get 500 internal
+ * server error.</li>
+ * <li>You must throw it from handleRequest synchronously.</li>
+ * </ol>
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class OverloadException extends RuntimeException {
+ public OverloadException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ReadableContentChannel.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ReadableContentChannel.java
new file mode 100644
index 00000000000..c887f4bfbab
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ReadableContentChannel.java
@@ -0,0 +1,181 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import java.nio.ByteBuffer;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Queue;
+
+/**
+ * <p>This class implements a {@link ContentChannel} that has a blocking <em>read</em> interface. Use this class if you
+ * intend to consume the content of the ContentChannel yourself. If you intend to forward the content to another
+ * ContentChannel, use {@link BufferedContentChannel} instead. If you <em>might</em> want to consume the content, return
+ * a {@link BufferedContentChannel} up front, and {@link BufferedContentChannel#connectTo(ContentChannel) connect} that
+ * to a ReadableContentChannel at the point where you decide to consume the data.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class ReadableContentChannel implements ContentChannel, Iterable<ByteBuffer> {
+
+ private final Object lock = new Object();
+ private Queue<Entry> queue = new LinkedList<>();
+ private boolean closed = false;
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ Objects.requireNonNull(buf, "buf");
+ synchronized (lock) {
+ if (closed || queue == null) {
+ throw new IllegalStateException(this + " is closed");
+ }
+ queue.add(new Entry(buf, handler));
+ lock.notifyAll();
+ }
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ synchronized (lock) {
+ if (closed || queue == null) {
+ throw new IllegalStateException(this + " is already closed");
+ }
+ closed = true;
+ queue.add(new Entry(null, handler));
+ lock.notifyAll();
+ }
+ }
+
+ @Override
+ public Iterator<ByteBuffer> iterator() {
+ return new MyIterator();
+ }
+
+ /**
+ * <p>Returns a lower-bound estimate on the number of bytes available to be {@link #read()} without blocking. If
+ * the returned number is larger than zero, the next call to {@link #read()} is guaranteed to not block.</p>
+ *
+ * @return The number of bytes available to be read without blocking.
+ */
+ public int available() {
+ Entry entry;
+ synchronized (lock) {
+ if (queue == null) {
+ return 0;
+ }
+ entry = queue.peek();
+ }
+ if (entry == null || entry.buf == null) {
+ return 0;
+ }
+ return entry.buf.remaining();
+ }
+
+ /**
+ * <p>Returns the next ByteBuffer in the internal queue. Before returning, this method calls {@link
+ * CompletionHandler#completed()} on the {@link CompletionHandler} that was submitted along with the ByteBuffer. If
+ * there are no ByteBuffers in the queue, this method waits indefinitely for either {@link
+ * #write(ByteBuffer, CompletionHandler)} or {@link #close(CompletionHandler)} to be called. Once closed and the
+ * internal queue drained, this method returns null.</p>
+ *
+ * @return The next ByteBuffer in queue, or null if this ReadableContentChannel is closed.
+ * @throws IllegalStateException If the current thread is interrupted while waiting.
+ */
+ public ByteBuffer read() {
+ Entry entry;
+ synchronized (lock) {
+ try {
+ while (queue != null && queue.isEmpty()) {
+ lock.wait();
+ }
+ } catch (InterruptedException e) {
+ throw new IllegalStateException(e);
+ }
+ if (queue == null) {
+ return null;
+ }
+ entry = queue.poll();
+ if (entry.buf == null) {
+ queue = null;
+ }
+ }
+ if (entry.handler != null) {
+ entry.handler.completed();
+ }
+ return entry.buf;
+ }
+
+ /**
+ * <p>This method calls {@link CompletionHandler#failed(Throwable)} on all pending {@link CompletionHandler}s, and
+ * blocks all future operations to this ContentChannel (i.e. calls to {@link #write(ByteBuffer, CompletionHandler)}
+ * and {@link #close(CompletionHandler)} throw IllegalStateExceptions).</p>
+ *
+ * <p>This method will also notify any thread waiting in {@link #read()}.</p>
+ *
+ * @param t The Throwable to pass to all pending CompletionHandlers.
+ * @throws IllegalStateException If this method is called more than once.
+ */
+ public void failed(Throwable t) {
+ Queue<Entry> queue;
+ synchronized (lock) {
+ if ((queue = this.queue) == null) {
+ throw new IllegalStateException();
+ }
+ this.queue = null;
+ lock.notifyAll();
+ }
+ for (Entry entry : queue) {
+ entry.handler.failed(t);
+ }
+ }
+
+ /**
+ * <p>Creates a {@link ContentInputStream} that wraps this ReadableContentChannel.</p>
+ *
+ * @return The new ContentInputStream that wraps this.
+ */
+ public ContentInputStream toStream() {
+ return new ContentInputStream(this);
+ }
+
+ private class MyIterator implements Iterator<ByteBuffer> {
+
+ ByteBuffer next;
+
+ @Override
+ public boolean hasNext() {
+ if (next != null) {
+ return true;
+ }
+ next = read();
+ return next != null;
+ }
+
+ @Override
+ public ByteBuffer next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ ByteBuffer ret = next;
+ next = null;
+ return ret;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private static class Entry {
+
+ final ByteBuffer buf;
+ final CompletionHandler handler;
+
+ Entry(ByteBuffer buf, CompletionHandler handler) {
+ this.handler = handler;
+ this.buf = buf;
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestDeniedException.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestDeniedException.java
new file mode 100644
index 00000000000..b46751d5a3c
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestDeniedException.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.service.ClientProvider;
+
+import java.net.URI;
+
+/**
+ * <p>This exception is used to signal that a {@link Request} was rejected by the corresponding {@link ClientProvider}
+ * or {@link RequestHandler}. There is no automation in throwing an instance of this class, but all RequestHandlers are
+ * encouraged to use this where appropriate.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class RequestDeniedException extends RuntimeException {
+
+ private final Request request;
+
+ /**
+ * <p>Constructs a new instance of this class with a detail message that contains the {@link URI} of the {@link
+ * Request} that was denied.</p>
+ *
+ * @param request The Request that was denied.
+ */
+ public RequestDeniedException(Request request) {
+ super("Request with URI '" + request.getUri() + "' denied.");
+ this.request = request;
+ }
+
+ /**
+ * <p>Returns the {@link Request} that was denied.</p>
+ *
+ * @return The Request that was denied.
+ */
+ public Request request() {
+ return request;
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestDispatch.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestDispatch.java
new file mode 100644
index 00000000000..02c752ceae9
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestDispatch.java
@@ -0,0 +1,156 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.ResourceReference;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.SharedResource;
+import com.yahoo.jdisc.References;
+
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.concurrent.*;
+
+/**
+ * <p>This class provides a convenient way of safely dispatching a {@link Request}. Using this class you do not have to
+ * worry about the exception safety surrounding the {@link SharedResource} logic. The internal mechanics of this class
+ * will ensure that anything that goes wrong during dispatch is safely handled according to jDISC contracts.</p>
+ *
+ * <p>It also provides a default implementation of the {@link ResponseHandler} interface that returns a {@link
+ * NullContent}. If you want to return a different {@link ContentChannel}, you need to override {@link
+ * #handleResponse(Response)}.</p>
+ *
+ * <p>The following is a simple example on how to use this class:</p>
+ * <pre>
+ * public void handleRequest(final Request parent, final ResponseHandler handler) {
+ * new RequestDispatch() {
+ * &#64;Override
+ * protected Request newRequest() {
+ * return new Request(parent, URI.create("http://remotehost/"));
+ * }
+ * &#64;Override
+ * protected Iterable&lt;ByteBuffer&gt; requestContent() {
+ * return Collections.singleton(ByteBuffer.wrap(new byte[] { 6, 9 }));
+ * }
+ * &#64;Override
+ * public ContentChannel handleResponse(Response response) {
+ * return handler.handleResponse(response);
+ * }
+ * }.dispatch();
+ * }
+ * </pre>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public abstract class RequestDispatch implements ListenableFuture<Response>, ResponseHandler {
+
+ private final FutureConjunction completions = new FutureConjunction();
+ private final FutureResponse futureResponse = new FutureResponse(this);
+
+ /**
+ * <p>Creates and returns the {@link Request} to dispatch. The internal code that calls this method takes care of
+ * the necessary exception safety of connecting the Request.</p>
+ *
+ * @return The Request to dispatch.
+ */
+ protected abstract Request newRequest();
+
+ /**
+ * <p>Returns an Iterable for the ByteBuffers that the {@link #dispatch()} method should write to the {@link
+ * Request} once it has {@link #connect() connected}. The default implementation returns an empty list. Because this
+ * method uses the Iterable interface, you can create the ByteBuffers lazily, or provide them as they become
+ * available.</p>
+ *
+ * @return The ByteBuffers to write to the Request's ContentChannel.
+ */
+ protected Iterable<ByteBuffer> requestContent() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * <p>This methods calls {@link #newRequest()} to create a new {@link Request}, and then calls {@link
+ * Request#connect(ResponseHandler)} on that. This method uses a <tt>finally</tt> block to make sure that the
+ * Request is always {@link Request#release() released}.</p>
+ *
+ * @return The ContentChannel to write the Request's content to.
+ */
+ public final ContentChannel connect() {
+ final Request request = newRequest();
+ try (final ResourceReference ref = References.fromResource(request)) {
+ return request.connect(futureResponse);
+ }
+ }
+
+ /**
+ * <p>This is a convenient method to construct a {@link FastContentWriter} over the {@link ContentChannel} returned by
+ * calling {@link #connect()}.</p>
+ *
+ * @return The ContentWriter for the connected Request.
+ */
+ public final FastContentWriter connectFastWriter() {
+ return new FastContentWriter(connect());
+ }
+
+ /**
+ * <p>This method calls {@link #connect()} to establish a {@link ContentChannel} for the {@link Request}, and then
+ * iterates through all the ByteBuffers returned by {@link #requestContent()} and writes them to that
+ * ContentChannel. This method uses a <tt>finally</tt> block to make sure that the ContentChannel is always {@link
+ * ContentChannel#close(CompletionHandler) closed}.</p>
+ *
+ * <p>The returned Future will wait for all CompletionHandlers associated with the Request have been completed, and
+ * a {@link Response} has been received.</p>
+ *
+ * @return A Future that can be waited for.
+ */
+ public final ListenableFuture<Response> dispatch() {
+ try (FastContentWriter writer = new FastContentWriter(connect())) {
+ for (ByteBuffer buf : requestContent()) {
+ writer.write(buf);
+ }
+ completions.addOperand(writer);
+ }
+ return this;
+ }
+
+ @Override
+ public void addListener(Runnable listener, Executor executor) {
+ Futures.allAsList(completions, futureResponse).addListener(listener, executor);
+ }
+
+ @Override
+ public final boolean cancel(boolean mayInterruptIfRunning) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public final boolean isDone() {
+ return completions.isDone() && futureResponse.isDone();
+ }
+
+ @Override
+ public final Response get() throws InterruptedException, ExecutionException {
+ completions.get();
+ return futureResponse.get();
+ }
+
+ @Override
+ public final Response get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException,
+ TimeoutException
+ {
+ long now = System.nanoTime();
+ completions.get(timeout, unit);
+ return futureResponse.get(unit.toNanos(timeout) - (System.nanoTime() - now), TimeUnit.NANOSECONDS);
+ }
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ return NullContent.INSTANCE;
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestHandler.java
new file mode 100644
index 00000000000..3fc3dbb8a82
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/RequestHandler.java
@@ -0,0 +1,62 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.SharedResource;
+import com.yahoo.jdisc.application.BindingRepository;
+import com.yahoo.jdisc.application.ContainerActivator;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.application.UriPattern;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * <p>This interface defines a component that is capable of acting as a handler for a {@link Request}. To activate a
+ * RequestHandler it must be {@link BindingRepository#bind(String, Object) bound} to a {@link UriPattern} within a
+ * {@link ContainerBuilder}, and that builder must be {@link ContainerActivator#activateContainer(ContainerBuilder)
+ * activated}.</p>
+*
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface RequestHandler extends SharedResource {
+
+ /**
+ * <p>This method will process the given {@link Request} and return a {@link ContentChannel} into which the caller
+ * can write the Request's content. For every call to this method, the implementation must call the provided {@link
+ * ResponseHandler} exactly once.</p>
+ *
+ * <p>Notice that unless this method throws an Exception, a reference to the currently active {@link Container}
+ * instance is kept internally until {@link ResponseHandler#handleResponse(Response)} has been called. This ensures
+ * that the configured environment of the Request is stable throughout its lifetime. Failure to call back with a
+ * Response will prevent the release of that reference, and therefore prevent the corresponding Container from ever
+ * shutting down. The requirement to call {@link ResponseHandler#handleResponse(Response)} is regardless of any
+ * subsequent errors that may occur while working with the returned ContentChannel.</p>
+ *
+ * @param request The Request to handle.
+ * @param handler The handler to pass the corresponding {@link Response} to.
+ * @return The ContentChannel to write the Request content to. Notice that the ContentChannel itself also holds a
+ * Container reference, so failure to close this will prevent the Container from ever shutting down.
+ */
+ public ContentChannel handleRequest(Request request, ResponseHandler handler);
+
+ /**
+ * <p>This method is called by the {@link Container} when a {@link Request} that was previously accepted by {@link
+ * #handleRequest(Request, ResponseHandler)} has timed out. If the Request has no timeout (i.e. {@link
+ * Request#getTimeout(TimeUnit)} returns <em>null</em>), then this method is never called.</p>
+ *
+ * <p>The given {@link ResponseHandler} is the same ResponseHandler that was initially passed to the {@link
+ * #handleRequest(Request, ResponseHandler)} method, and it is guarded by a volatile boolean so that only the first
+ * call to {@link ResponseHandler#handleResponse(Response)} is actually passed on. This means that you do NOT need
+ * to manage the ResponseHandlers yourself to prevent a late Response from calling the same ResponseHandler.</p>
+ *
+ * <p>Notice that you MUST call {@link ResponseHandler#handleResponse(Response)} as a reaction to having this method
+ * invoked. Failure to do so will prevent the Container from ever shutting down.</p>
+ *
+ * @param request The Request that has timed out.
+ * @param handler The handler to pass the timeout {@link Response} to.
+ * @see Response#dispatchTimeout(ResponseHandler)
+ */
+ public void handleTimeout(Request request, ResponseHandler handler);
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ResponseDispatch.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ResponseDispatch.java
new file mode 100644
index 00000000000..dfcda9ee85d
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ResponseDispatch.java
@@ -0,0 +1,179 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import com.google.common.util.concurrent.ForwardingListenableFuture;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.SharedResource;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.Future;
+
+/**
+ * <p>This class provides a convenient way of safely dispatching a {@link Response}. It is similar in use to {@link
+ * RequestDispatch}, where you need to subclass and implement and override the appropriate methods. Because a Response
+ * is not a {@link SharedResource}, its construction is less strenuous, and this class is able to provide a handful of
+ * convenient factory methods to dispatch the simplest of Responses.</p>
+ * <p>The following is a simple example on how to use this class without the factories:</p>
+ * <pre>
+ * public void signalInternalError(ResponseHandler handler) {
+ * new ResponseDispatch() {
+ * &#64;Override
+ * protected Response newResponse() {
+ * return new Response(Response.Status.INTERNAL_SERVER_ERROR);
+ * }
+ * &#64;Override
+ * protected Iterable&lt;ByteBuffer&gt; responseContent() {
+ * return Collections.singleton(ByteBuffer.wrap(new byte[] { 6, 9 }));
+ * }
+ * }.dispatch(handler);
+ * }
+ * </pre>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public abstract class ResponseDispatch extends ForwardingListenableFuture<Boolean> {
+
+ private final FutureConjunction completions = new FutureConjunction();
+
+ /**
+ * <p>Creates and returns the {@link Response} to dispatch.</p>
+ *
+ * @return The Response to dispatch.
+ */
+ protected abstract Response newResponse();
+
+ /**
+ * <p>Returns an Iterable for the ByteBuffers that the {@link #dispatch(ResponseHandler)} method should write to the
+ * {@link Response} once it has {@link ResponseHandler#handleResponse(Response) connected}. The default
+ * implementation returns an empty list. Because this method uses the Iterable interface, you can provide the
+ * ByteBuffers lazily, or as they become available.</p>
+ *
+ * @return The ByteBuffers to write to the Response's ContentChannel.
+ */
+ protected Iterable<ByteBuffer> responseContent() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * <p>This methods calls {@link #newResponse()} to create a new {@link Response}, and then calls {@link
+ * ResponseHandler#handleResponse(Response)} with that.</p>
+ *
+ * @param responseHandler The ResponseHandler to connect to.
+ * @return The ContentChannel to write the Response's content to.
+ */
+ public final ContentChannel connect(ResponseHandler responseHandler) {
+ return responseHandler.handleResponse(newResponse());
+ }
+
+ /**
+ * <p>Convenience method for constructing a {@link FastContentWriter} over the {@link ContentChannel} returned by
+ * calling {@link #connect(ResponseHandler)}.</p>
+ *
+ * @param responseHandler The ResponseHandler to connect to.
+ * @return The FastContentWriter for the connected Response.
+ */
+ public final FastContentWriter connectFastWriter(ResponseHandler responseHandler) {
+ return new FastContentWriter(connect(responseHandler));
+ }
+
+ /**
+ * <p>This method calls {@link #connect(ResponseHandler)} to establish a {@link ContentChannel} for the {@link
+ * Response}, and then iterates through all the ByteBuffers returned by {@link #responseContent()} and writes them
+ * to that ContentChannel. This method uses a <tt>finally</tt> block to make sure that the ContentChannel is always
+ * {@link ContentChannel#close(CompletionHandler) closed}.</p>
+ * <p>The returned Future will wait for all CompletionHandlers associated with the Response have been
+ * completed.</p>
+ *
+ * @param responseHandler The ResponseHandler to dispatch to.
+ * @return A Future that can be waited for.
+ */
+ public final ListenableFuture<Boolean> dispatch(ResponseHandler responseHandler) {
+ try (FastContentWriter writer = new FastContentWriter(connect(responseHandler))) {
+ for (ByteBuffer buf : responseContent()) {
+ writer.write(buf);
+ }
+ completions.addOperand(writer);
+ }
+ return this;
+ }
+
+ @Override
+ protected final ListenableFuture<Boolean> delegate() {
+ return completions;
+ }
+
+ @Override
+ public final boolean cancel(boolean mayInterruptIfRunning) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final boolean isCancelled() {
+ return false;
+ }
+
+ /**
+ * <p>Factory method for creating a ResponseDispatch with a {@link Response} that has the given status code, and
+ * ByteBuffer content.</p>
+ *
+ * @param responseStatus The status code of the Response to dispatch.
+ * @param content The ByteBuffer content of the Response, may be empty.
+ * @return The created ResponseDispatch.
+ */
+ public static ResponseDispatch newInstance(int responseStatus, ByteBuffer... content) {
+ return newInstance(new Response(responseStatus), Arrays.asList(content));
+ }
+
+ /**
+ * <p>Factory method for creating a ResponseDispatch with a {@link Response} that has the given status code, and
+ * collection of ByteBuffer content.
+ * Because this method uses the Iterable interface, you can create the ByteBuffers lazily, or
+ * provide them as they become available.</p>
+ *
+ * @param responseStatus The status code of the Response to dispatch.
+ * @param content The provider of the Response's ByteBuffer content.
+ * @return The created ResponseDispatch.
+ */
+ public static ResponseDispatch newInstance(int responseStatus, Iterable<ByteBuffer> content) {
+ return newInstance(new Response(responseStatus), content);
+ }
+
+ /**
+ * <p>Factory method for creating a ResponseDispatch over a given {@link Response} and ByteBuffer content.</p>
+ *
+ * @param response The Response to dispatch.
+ * @param content The ByteBuffer content of the Response, may be empty.
+ * @return The created ResponseDispatch.
+ */
+ public static ResponseDispatch newInstance(Response response, ByteBuffer... content) {
+ return newInstance(response, Arrays.asList(content));
+ }
+
+ /**
+ * <p>Factory method for creating a ResponseDispatch over a given {@link Response} and ByteBuffer content.
+ * Because this method uses the Iterable interface, you can create the ByteBuffers lazily, or provide them as they
+ * become available.</p>
+ *
+ * @param response The Response to dispatch.
+ * @param content The provider of the Response's ByteBuffer content.
+ * @return The created ResponseDispatch.
+ */
+ public static ResponseDispatch newInstance(final Response response, final Iterable<ByteBuffer> content) {
+ return new ResponseDispatch() {
+
+ @Override
+ protected Response newResponse() {
+ return response;
+ }
+
+ @Override
+ public Iterable<ByteBuffer> responseContent() {
+ return content;
+ }
+ };
+ }
+
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ResponseHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ResponseHandler.java
new file mode 100644
index 00000000000..5c6abf64013
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ResponseHandler.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.service.ClientProvider;
+
+/**
+ * <p>This interface defines a component that is capable of acting as a handler for a {@link Response}. An
+ * implementation of this interface is required to be passed alongside every {@link Request} as part of the API (see
+ * {@link ClientProvider#handleRequest(Request, ResponseHandler)} and {@link RequestHandler#handleRequest(Request,
+ * ResponseHandler)}).</p>
+ *
+ * <p>The jDISC API has intentionally been designed as not to provide a implicit reference from Response to
+ * corresponding Request, but rather leave that to the implementation of context-aware ResponseHandlers. By creating
+ * light-weight ResponseHandlers on a per-Request basis, any necessary reference can be embedded within.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface ResponseHandler {
+
+ /**
+ * <p>This method will process the given {@link Response} and return a {@link ContentChannel} into which the caller
+ * can write the Response's content.</p>
+ *
+ * @param response The Response to handle.
+ * @return The ContentChannel to write the Response content to. Notice that the ContentChannel holds a Container
+ * reference, so failure to close this will prevent the Container from ever shutting down.
+ */
+ ContentChannel handleResponse(Response response);
+
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ThreadedRequestHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ThreadedRequestHandler.java
new file mode 100644
index 00000000000..64bcf91edbd
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ThreadedRequestHandler.java
@@ -0,0 +1,156 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.ResourceReference;
+import com.yahoo.jdisc.Response;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * <p>This class implements a {@link RequestHandler} with a synchronous {@link #handleRequest(Request,
+ * BufferedContentChannel, ResponseHandler)} API for handling {@link Request}s. An Executor is provided at construction
+ * time, and all Requests are automatically scheduled for processing on that Executor.</p>
+ *
+ * <p>A very simple echo handler could be implemented like this:</p>
+ * <pre>
+ * class MyRequestHandler extends ThreadedRequestHandler {
+ *
+ * &#64;Inject
+ * MyRequestHandler(Executor executor) {
+ * super(executor);
+ * }
+ *
+ * &#64;Override
+ * protected void handleRequest(Request request, ReadableContentChannel requestContent, ResponseHandler handler) {
+ * ContentWriter responseContent = ResponseDispatch.newInstance(Response.Status.OK).connectWriter(handler);
+ * try {
+ * for (ByteBuffer buf : requestContent) {
+ * responseContent.write(buf);
+ * }
+ * } catch (RuntimeException e) {
+ * requestContent.failed(e);
+ * throw e;
+ * } finally {
+ * responseContent.close();
+ * }
+ * }
+ * }
+ * </pre>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public abstract class ThreadedRequestHandler extends AbstractRequestHandler {
+
+ private final Executor executor;
+ private volatile long timeout = 0;
+
+ protected ThreadedRequestHandler(Executor executor) {
+ Objects.requireNonNull(executor, "executor");
+ this.executor = executor;
+ }
+
+ /**
+ * <p>Sets the timeout that this ThreadedRequestHandler sets on all handled {@link Request}s. If the
+ * <em>timeout</em> value is less than or equal to zero, no timeout will be applied.</p>
+ *
+ * @param timeout The allocated amount of time.
+ * @param unit The time unit of the <em>timeout</em> argument.
+ */
+ public final void setTimeout(long timeout, TimeUnit unit) {
+ this.timeout = unit.toMillis(timeout);
+ }
+
+ /**
+ * <p>Returns the timeout that this ThreadedRequestHandler sets on all handled {@link Request}s.</p>
+ *
+ * @param unit The unit to use for the return value.
+ * @return The timeout in the appropriate unit.
+ */
+ public final long getTimeout(TimeUnit unit) {
+ return unit.convert(timeout, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public final ContentChannel handleRequest(Request request, ResponseHandler responseHandler) {
+ if (timeout > 0) {
+ request.setTimeout(timeout, TimeUnit.MILLISECONDS);
+ }
+ BufferedContentChannel content = new BufferedContentChannel();
+ executor.execute(new RequestTask(request, content, responseHandler));
+ return content;
+ }
+
+ /**
+ * <p>Override this method if you want to access the {@link Request}'s content using a {@link
+ * BufferedContentChannel}. If you do not override this method, it will call {@link #handleRequest(Request,
+ * ReadableContentChannel, ResponseHandler)}.</p>
+ *
+ * @param request The Request to handle.
+ * @param responseHandler The handler to pass the corresponding {@link Response} to.
+ * @param requestContent The content of the Request.
+ */
+ protected void handleRequest(Request request, BufferedContentChannel requestContent,
+ ResponseHandler responseHandler)
+ {
+ handleRequest(request, requestContent.toReadable(), responseHandler);
+ }
+
+ /**
+ * <p>Implement this method if you want to access the {@link Request}'s content using a {@link
+ * ReadableContentChannel}. If you do not override this method, it will call {@link #handleRequest(Request,
+ * ContentInputStream, ResponseHandler)}.</p>
+ *
+ * @param request The Request to handle.
+ * @param responseHandler The handler to pass the corresponding {@link Response} to.
+ * @param requestContent The content of the Request.
+ */
+ protected void handleRequest(Request request, ReadableContentChannel requestContent,
+ ResponseHandler responseHandler)
+ {
+ handleRequest(request, requestContent.toStream(), responseHandler);
+ }
+
+ /**
+ * <p>Implement this method if you want to access the {@link Request}'s content using a {@link ContentInputStream}.
+ * If you do not override this method, it will dispatch a {@link Response} to the {@link ResponseHandler} with a
+ * <tt>Response.Status.NOT_IMPLEMENTED</tt> status.</p>
+ *
+ * @param request The Request to handle.
+ * @param responseHandler The handler to pass the corresponding {@link Response} to.
+ * @param requestContent The content of the Request.
+ */
+ @SuppressWarnings("UnusedParameters")
+ protected void handleRequest(Request request, ContentInputStream requestContent,
+ ResponseHandler responseHandler)
+ {
+ while (requestContent.read() >= 0) {
+ // drain content stream
+ }
+ ResponseDispatch.newInstance(Response.Status.NOT_IMPLEMENTED).dispatch(responseHandler);
+ }
+
+ private class RequestTask implements Runnable {
+
+ final Request request;
+ final BufferedContentChannel content;
+ final ResponseHandler responseHandler;
+ private final ResourceReference requestReference;
+
+ RequestTask(Request request, BufferedContentChannel content, ResponseHandler responseHandler) {
+ this.request = request;
+ this.content = content;
+ this.responseHandler = responseHandler;
+ this.requestReference = request.refer();
+ }
+
+ @Override
+ public void run() {
+ try (final ResourceReference ref = requestReference) {
+ ThreadedRequestHandler.this.handleRequest(request, content, responseHandler);
+ }
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/UnsafeContentInputStream.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/UnsafeContentInputStream.java
new file mode 100644
index 00000000000..115b5383302
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/UnsafeContentInputStream.java
@@ -0,0 +1,82 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.handler;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+/**
+ * <p>This class provides an adapter from a {@link ReadableContentChannel} to an InputStream. This class supports all
+ * regular InputStream operations, and can be combined with any other InputStream API.</p>
+ *
+ * <p>Because this class encapsulates the reference-counted {@link ContentChannel} operations, one must be sure to
+ * always call {@link #close()} before discarding it. Failure to do so will prevent the Container from ever shutting
+ * down.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class UnsafeContentInputStream extends InputStream {
+
+ private final ReadableContentChannel content;
+ private ByteBuffer buf = ByteBuffer.allocate(0);
+
+ /**
+ * <p>Constructs a new ContentInputStream that reads from the given {@link ReadableContentChannel}.</p>
+ *
+ * @param content The content to read the stream from.
+ */
+ public UnsafeContentInputStream(ReadableContentChannel content) {
+ this.content = content;
+ }
+
+ @Override
+ public int read() {
+ while (buf != null && buf.remaining() == 0) {
+ buf = content.read();
+ }
+ if (buf == null) {
+ return -1;
+ }
+ return ((int)buf.get()) & 0xFF;
+ }
+
+ @Override
+ public int read(byte buf[], int off, int len) {
+ Objects.requireNonNull(buf, "buf");
+ if (off < 0 || len < 0 || len > buf.length - off) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (len == 0) {
+ return 0;
+ }
+ int c = read();
+ if (c == -1) {
+ return -1;
+ }
+ buf[off] = (byte)c;
+ int cnt = 1;
+ for (; cnt < len && available() > 0; ++cnt) {
+ if ((c = read()) == -1) {
+ break;
+ }
+ buf[off + cnt] = (byte)c;
+ }
+ return cnt;
+ }
+
+ @Override
+ public int available() {
+ if (buf != null && buf.remaining() > 0) {
+ return buf.remaining();
+ }
+ return content.available();
+ }
+
+ @Override
+ public void close() {
+ // noinspection StatementWithEmptyBody
+ while (content.read() != null) {
+
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/package-info.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/package-info.java
new file mode 100644
index 00000000000..8f44495222b
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/package-info.java
@@ -0,0 +1,68 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * <p>Provides classes and interfaces for implementing a {@link com.yahoo.jdisc.handler.RequestHandler
+ * RequestHandler}.</p>
+ *
+ * <h3>RequestHandler</h3>
+ * <p>All {@link com.yahoo.jdisc.Request Requests} in a jDISC application are processed by RequestHandlers. These are
+ * components created by the {@link com.yahoo.jdisc.application.Application Application}, and bound to one or more URI
+ * patterns through the {@link com.yahoo.jdisc.application.ContainerBuilder ContainerBuilder} API. Upon receiving a
+ * Request, a RequestHandler must return a {@link com.yahoo.jdisc.handler.ContentChannel ContentChannel} into which the
+ * caller can asynchronously write the Request's payload. The ContentChannel is an asynchronous API for ByteBuffer
+ * hand-over, with support for asynchronous completion-notification (through the {@link
+ * com.yahoo.jdisc.handler.CompletionHandler CompletionHandler} interface). Once the Request has been processed (which
+ * may or may not involve dispatching one or more child-Requests), the RequestHandler must prepare a {@link
+ * com.yahoo.jdisc.Response Response} object and asynchronously pass that to the corresponding {@link
+ * com.yahoo.jdisc.handler.ResponseHandler ResponseHandler}. One of the most vital parts of the RequestHandler definition
+ * is that it must provide exactly one Response for every Request. This guarantee simplifies the usage pattern of
+ * RequestHandlers, and allows other components to skip a lot of bookkeeping. If a RequestHandler decides to create and
+ * dispatch a child-Request, it is done through the same {@link com.yahoo.jdisc.application.BindingSet BindingSet}
+ * mechanics that was used to resolve the current RequestHandler. Because all {@link
+ * com.yahoo.jdisc.service.ServerProvider ServerProviders} use "localhost" for Request URI hostname, most RequestHandlers
+ * are also bound to "localhost". Those that are not typically provide a specific service for one or more remote hosts
+ * (these are {@link com.yahoo.jdisc.service.ClientProvider ClientProviders}).</p>
+ *
+<pre>
+&#64;Inject
+MyApplication(ContainerActivator activator, CurrentContainer container) {
+ ContainerBuilder builder = activator.newContainerBuilder();
+ builder.serverBindings().bind("http://localhost/*", new MyRequestHandler());
+ activator.activateContainer(builder);
+}
+</pre>
+ *
+ * <p>Because the entirety of the RequestHandler stack (RequestHandler, ResponseHandler, ContentChannel and
+ * CompletionHandler) is asynchronous, an active {@link com.yahoo.jdisc.Container Container} can handle as many
+ * concurrent Requests as the sum capacity of all installed ServerProviders. Furthermore, the APIs have been designed in
+ * such a way that the ContentChannel returned back to the initial call to a RequestHandler can be the very same
+ * ContentChannel as is returned by the final destination of a Request. This means that, unless explicitly implemented
+ * otherwise, a jDISC application that is intended to forward large streams of data can do so without having to make any
+ * copies of that data as it is passing through.</p>
+ *
+ * <h3>ResponseHandler</h3>
+ * <p>The complement of the Request is the Response. A Response is a numeric status code and a set of header fields.
+ * Just as Requests are processed by RequestHandlers, Responses are processed by ResponseHandlers. The ResponseHandler
+ * interface is fully asynchronous, and uses the ContentChannel class to encapsulate the asynchronous passing of
+ * Response content. Where the RequestHandler is part of the Container and it's BindingSets, the ResponseHandler is part
+ * of the Request context. With every call to a RequestHandler you must also provide a ResponseHandler. Because the
+ * Request itself is not part of the ResponseHandler API, there is no built-in feature to tell a ResponseHandler which
+ * Request the Response corresponds to. Instead, one should create per-Request light-weight ResponseHandler objects that
+ * encapsulate the necessary context for Response processing. This was a deliberate design choice based on observed
+ * usage patterns of a different but similar architecture (the messaging layer of the Vespa platform).</p>
+ *
+ * <p>A Request may or may not have an assigned timeout. Both a ServerProvider and a RequestHandler may choose to assign
+ * a timeout to a Request, but only the first to assign it has an effect. The timeout is the maximum allowed time for a
+ * RequestHandler to wait before calling the ResponseHandler. There is no monitoring of the associated ContentChannels
+ * of either Request or Response, so once a Response has been dispatched a ContentChannel can stay open indefinetly.
+ * Timeouts are managed by a jDISC core component, but a RequestHandler may ask a Request at any time whether or not it
+ * has timed out. This allows RequestHandlers to terminate CPU-intensive processing of Requests whose Response will be
+ * discarded anyway. Once timeout occurs, the timeout manager calls the appropriate {@link
+ * com.yahoo.jdisc.handler.RequestHandler#handleTimeout(Request, ResponseHandler)} method. All future calls to that
+ * ResponseHandler is blocked, as to uphold the guarantee that a Request should have exactly one Response.</p>
+ *
+ * @see com.yahoo.jdisc
+ * @see com.yahoo.jdisc.application
+ * @see com.yahoo.jdisc.service
+ */
+@com.yahoo.api.annotations.PublicApi
+package com.yahoo.jdisc.handler;
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/package-info.java b/jdisc_core/src/main/java/com/yahoo/jdisc/package-info.java
new file mode 100644
index 00000000000..2ad31099e07
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/package-info.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * <p>Provides the common classes and interfaces of the jDISC core.</p>
+ *
+ * <p>jDISC is a single-process, multi-threaded application container that consists of exactly one {@link
+ * com.yahoo.jdisc.application.Application Application} with an optional {@link com.yahoo.jdisc.Metric Metric}
+ * configuration, one or more {@link com.yahoo.jdisc.handler.RequestHandler RequestHandlers}, one or more {@link
+ * com.yahoo.jdisc.service.ServerProvider ServerProviders}, and one or more named {@link
+ * com.yahoo.jdisc.application.BindingSet BindingSets}. When starting an Application, and whenever else the current
+ * configuration changes, it is the responsibility of the Application to create and activate a new {@link
+ * com.yahoo.jdisc.Container Container} that matches the most recent configuration. The Container itself is an immutable
+ * object, ensuring that the context of a {@link com.yahoo.jdisc.Request Request} never changes during its execution.
+ * When a new Container is activated, the previous is deactivated and scheduled for shutdown as soon as it finishes
+ * processing all previously accepted Requests. At any time, a jDISC process will therefore have zero (typically during
+ * application startup and shutdown) or one active Container, and zero or more deactivated Containers. The currently
+ * active Container is available to ServerProviders through an application-scoped singleton, making sure that no new
+ * Request is ever passed to a deactivated Container.</p>
+ *
+ * <p>A Request is created when either a) a ServerProvider accepts an incoming connection, or b) a RequestHandler
+ * creates a child Request of another. In the case of the ServerProvider, the {@link
+ * com.yahoo.jdisc.service.CurrentContainer CurrentContainer} interface provides a reference to the currently active
+ * Container, and the Application's {@link com.yahoo.jdisc.application.BindingSetSelector BindingSetSelector} (provided
+ * during configuration) selects a BindingSet based on the Request's URI. The BindingSet is what the Container uses to
+ * match a Request's URI to an appropriate RequestHandler. Together, the Container reference and the selected BindingSet
+ * make up the context of the Request. When a RequestHandler chooses to create a child Request, it reuses both the
+ * Container reference and the BindingSet of the original Request, ensuring that all processing of a single connection
+ * happens within the same Container instance. For every dispatched Request there is always exactly one {@link
+ * com.yahoo.jdisc.Response Response}. The Response is never routed, it simply follows the call stack of the
+ * corresponding Request.</p>
+ *
+ * <p>Because BindingSets decide on the RequestHandler which is to process a Request, using multiple BindingSets and a
+ * property-specific BindingSetSelector, one is able to create a Container capable of rewiring itself on a per-Request
+ * basis. This can be used for running production code in a mock-up environment for offline regression tests, and also
+ * for features such as Request bucketing (selecting a bucket BindingSet for n percent of the URIs) and rate-limiting
+ * (selecting a rejecting-type RequestHandler if the system is in some specific state).</p>
+ *
+ * <p>Finally, the Container provides a minimal Metric API that consists of a {@link com.yahoo.jdisc.Metric Metric}
+ * producer and a {@link com.yahoo.jdisc.application.MetricConsumer MetricConsumer}. Any component may choose to inject
+ * and use the Metric API, but all its calls are ignored unless the Application has chosen to inject a MetricConsumer
+ * provider during configuration. For efficiency reasons, the Container provides the {@link
+ * com.yahoo.jdisc.application.ContainerThread ContainerThread} which offers thread local access to the Metric API. This
+ * is a class that needs to be explicitly used in whatever Executor or ThreadFactory the Application chooses to inject
+ * into the Container.</p>
+ *
+ * <p>For unit testing purposes, the {@link com.yahoo.jdisc.test} package provides classes and interfaces to help setup
+ * and run a jDISC application in a test environment with as little effort as possible.</p>
+ *
+ * @see com.yahoo.jdisc.application
+ * @see com.yahoo.jdisc.handler
+ * @see com.yahoo.jdisc.service
+ * @see com.yahoo.jdisc.test
+ */
+@com.yahoo.api.annotations.PublicApi
+package com.yahoo.jdisc;
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/service/AbstractClientProvider.java b/jdisc_core/src/main/java/com/yahoo/jdisc/service/AbstractClientProvider.java
new file mode 100644
index 00000000000..3f2ebc67aa5
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/service/AbstractClientProvider.java
@@ -0,0 +1,20 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.service;
+
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.handler.AbstractRequestHandler;
+import com.yahoo.jdisc.handler.ResponseHandler;
+
+/**
+ * <p>This is a convenient parent class for {@link ClientProvider} with default implementations for all but the
+ * essential {@link #handleRequest(Request, ResponseHandler)} method.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public abstract class AbstractClientProvider extends AbstractRequestHandler implements ClientProvider {
+
+ @Override
+ public void start() {
+
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/service/AbstractServerProvider.java b/jdisc_core/src/main/java/com/yahoo/jdisc/service/AbstractServerProvider.java
new file mode 100644
index 00000000000..15363ded3e0
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/service/AbstractServerProvider.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.service;
+
+import com.google.inject.Inject;
+import com.yahoo.jdisc.AbstractResource;
+import com.yahoo.jdisc.Request;
+
+import java.util.Objects;
+
+/**
+ * <p>This is a convenient parent class for {@link ServerProvider} with default implementations for all but the
+ * essential {@link #start()} and {@link #close()} methods. It requires that the {@link CurrentContainer} is injected in
+ * the constructor, since that interface is needed to dispatch {@link Request}s.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public abstract class AbstractServerProvider extends AbstractResource implements ServerProvider {
+
+ private final CurrentContainer container;
+
+ @Inject
+ protected AbstractServerProvider(CurrentContainer container) {
+ Objects.requireNonNull(container, "container");
+ this.container = container;
+ }
+
+ public final CurrentContainer container() {
+ return container;
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/service/BindingSetNotFoundException.java b/jdisc_core/src/main/java/com/yahoo/jdisc/service/BindingSetNotFoundException.java
new file mode 100644
index 00000000000..b02fab2eba8
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/service/BindingSetNotFoundException.java
@@ -0,0 +1,38 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.service;
+
+import com.yahoo.jdisc.application.BindingSet;
+
+import java.net.URI;
+
+/**
+ * This exception is used to signal that a named {@link BindingSet} was not found. An instance of this class will be
+ * thrown by the {@link CurrentContainer#newReference(URI)} method when a BindingSet with the specified name does not
+ * exist.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class BindingSetNotFoundException extends RuntimeException {
+
+ private final String bindingSet;
+
+ /**
+ * Constructs a new instance of this class with a detail message that contains the name of the {@link BindingSet}
+ * that was not found.
+ *
+ * @param bindingSet The name of the {@link BindingSet} that was not found.
+ */
+ public BindingSetNotFoundException(String bindingSet) {
+ super("No binding set named '" + bindingSet + "'.");
+ this.bindingSet = bindingSet;
+ }
+
+ /**
+ * Returns the name of the {@link BindingSet} that was not found.
+ *
+ * @return The name of the BindingSet.
+ */
+ public String bindingSet() {
+ return bindingSet;
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/service/ClientProvider.java b/jdisc_core/src/main/java/com/yahoo/jdisc/service/ClientProvider.java
new file mode 100644
index 00000000000..96583217721
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/service/ClientProvider.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.service;
+
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.application.*;
+import com.yahoo.jdisc.handler.RequestHandler;
+
+/**
+ * <p>This interface defines a component that is capable of acting as a client to an external server. To activate a
+ * ClientProvider it must be {@link BindingRepository#bind(String, Object) bound} to a {@link UriPattern} within a
+ * {@link ContainerBuilder}, and that builder must be {@link ContainerActivator#activateContainer(ContainerBuilder)
+ * activated}.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface ClientProvider extends RequestHandler {
+
+ /**
+ * <p>This is a synchronous method to configure this ClientProvider. The {@link Container} does <em>not</em> call
+ * this method, instead it is a required step in the {@link Application} initialization code.</p>
+ */
+ void start();
+
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/service/ContainerNotReadyException.java b/jdisc_core/src/main/java/com/yahoo/jdisc/service/ContainerNotReadyException.java
new file mode 100644
index 00000000000..fa9dfd3b6f6
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/service/ContainerNotReadyException.java
@@ -0,0 +1,26 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.service;
+
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.application.ContainerActivator;
+import com.yahoo.jdisc.application.ContainerBuilder;
+
+import java.net.URI;
+
+/**
+ * This exception is used to signal that no {@link Container} is ready to serve {@link Request}s. An instance of this
+ * class will be thrown by the {@link CurrentContainer#newReference(URI)} method if it is called before a Container has
+ * been activated, or after a <em>null</em> argument has been passed to {@link ContainerActivator#activateContainer(ContainerBuilder)}.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class ContainerNotReadyException extends RuntimeException {
+
+ /**
+ * Constructs a new instance of this class with a detail message.
+ */
+ public ContainerNotReadyException() {
+ super("Container not ready.");
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/service/CurrentContainer.java b/jdisc_core/src/main/java/com/yahoo/jdisc/service/CurrentContainer.java
new file mode 100644
index 00000000000..7e4625277be
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/service/CurrentContainer.java
@@ -0,0 +1,37 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.service;
+
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.application.BindingSet;
+import com.yahoo.jdisc.application.BindingSetSelector;
+import com.yahoo.jdisc.handler.RequestHandler;
+
+import java.net.URI;
+
+/**
+ * This interface declares a method to retrieve a reference to the current {@link Container}. Note that a {@link
+ * Container} which has <em>not</em> been {@link Container#release() closed} will actively keep it alive, preventing it
+ * from shutting down when expired. Failure to call close() will eventually lead to an {@link OutOfMemoryError}. A
+ * {@link ServerProvider} should have an instance of this class injected in its constructor, and simply use the {@link
+ * Request#Request(CurrentContainer, URI) appropriate Request constructor} to avoid having to worry about the keep-alive
+ * issue.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface CurrentContainer {
+
+ /**
+ * Returns a reference to the currently active {@link Container}. Until {@link Container#release()} has been called,
+ * the Container can not shut down.
+ *
+ * @param uri The identifier used to match this Request to an appropriate {@link ClientProvider} or {@link
+ * RequestHandler}. The hostname must be "localhost" or a fully qualified domain name.
+ * @return A reference to the current Container.
+ * @throws NoBindingSetSelectedException If no {@link BindingSet} was selected by the {@link BindingSetSelector}.
+ * @throws BindingSetNotFoundException If the named BindingSet was not found.
+ * @throws ContainerNotReadyException If no active Container was found, this can only happen during initial
+ * setup.
+ */
+ public Container newReference(URI uri);
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/service/NoBindingSetSelectedException.java b/jdisc_core/src/main/java/com/yahoo/jdisc/service/NoBindingSetSelectedException.java
new file mode 100644
index 00000000000..382262e52cd
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/service/NoBindingSetSelectedException.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.service;
+
+import com.yahoo.jdisc.application.BindingSet;
+import com.yahoo.jdisc.application.BindingSetSelector;
+
+import java.net.URI;
+
+/**
+ * This exception is used to signal that no {@link BindingSet} was selected for a given {@link URI}. An instance of this
+ * class will be thrown by the {@link CurrentContainer#newReference(URI)} method if {@link
+ * BindingSetSelector#select(URI)} returned <em>null</em>.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class NoBindingSetSelectedException extends RuntimeException {
+
+ private final URI uri;
+
+ /**
+ * Constructs a new instance of this class with a detail message that contains the {@link URI} for which there was
+ * no {@link BindingSet} selected.
+ *
+ * @param uri The URI for which there was no BindingSet selected.
+ */
+ public NoBindingSetSelectedException(URI uri) {
+ super("No binding set selected for URI '" + uri + "'.");
+ this.uri = uri;
+ }
+
+ /**
+ * Returns the {@link URI} for which there was no {@link BindingSet} selected.
+ *
+ * @return The URI.
+ */
+ public URI uri() {
+ return uri;
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/service/ServerProvider.java b/jdisc_core/src/main/java/com/yahoo/jdisc/service/ServerProvider.java
new file mode 100644
index 00000000000..ea895088492
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/service/ServerProvider.java
@@ -0,0 +1,52 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.service;
+
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.SharedResource;
+import com.yahoo.jdisc.application.Application;
+import com.yahoo.jdisc.application.ContainerActivator;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.application.ServerRepository;
+
+import java.net.URI;
+
+/**
+ * <p>This interface defines a component that is capable of acting as a server for an external client. To activate a
+ * ServerProvider it must be {@link ServerRepository#install(ServerProvider) installed} in a {@link ContainerBuilder},
+ * and that builder must be {@link ContainerActivator#activateContainer(ContainerBuilder) activated}.</p>
+ *
+ * <p>If a ServerProvider is to expire due to {@link Application} reconfiguration, it is necessary to close() that
+ * ServerProvider before deactivating the owning {@link Container}. Typically:</p>
+ *
+ * <pre>
+ * myExpiredServers.close();
+ * reconfiguredContainerBuilder.servers().install(myRetainedServers);
+ * containerActivator.activateContainer(reconfiguredContainerBuilder);
+ * </pre>
+ *
+ * <p>All implementations of this interface will need to have a {@link CurrentContainer} injected into its constructor
+ * so that it is able to create and dispatch new {@link Request}s.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface ServerProvider extends SharedResource {
+
+ /**
+ * <p>This is a synchronous method to configure this ServerProvider and bind the listen port (or equivalent). The
+ * {@link Container} does <em>not</em> call this method, instead it is a required step in the {@link Application}
+ * initialization code.</p>
+ */
+ public void start();
+
+ /**
+ * <p>This is a synchronous method to close the listen port (or equivalent) of this ServerProvider and flush any
+ * input buffers that will cause calls to {@link CurrentContainer#newReference(URI)}. This method <em>must not</em>
+ * return until the implementation can guarantee that there will be no further calls to CurrentContainer. All
+ * previously dispatched {@link Request}s are processed as before.</p>
+ *
+ * <p>The {@link Container} does <em>not</em> call this method, instead it is a required step in the {@link
+ * Application} shutdown code.</p>
+ */
+ public void close();
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/service/package-info.java b/jdisc_core/src/main/java/com/yahoo/jdisc/service/package-info.java
new file mode 100644
index 00000000000..445ddd9c726
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/service/package-info.java
@@ -0,0 +1,77 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * <p>Provides classes and interfaces for implementing a {@link com.yahoo.jdisc.service.ClientProvider ClientProvider} or
+ * a {@link com.yahoo.jdisc.service.ServerProvider ServerProvider}.</p>
+ *
+ * <h3>ServerProvider</h3>
+ * <p>All {@link com.yahoo.jdisc.Request Requests} that are processed in a jDISC application are created by
+ * ServerProviders. These are components created by the {@link com.yahoo.jdisc.application.Application Application}, and
+ * they are the parts of jDISC that accept incoming connections. The ServerProvider creates and dispatches Request
+ * instances to the {@link com.yahoo.jdisc.service.CurrentContainer CurrentContainer}. No Request is ever dispatched to a
+ * ServerProvider, so a ServerProvider is considered part of the Application and not part of a Container (as opposed to
+ * {@link com.yahoo.jdisc.handler.RequestHandler RequestHandlers} and ClientProviders). To create a Request the
+ * ServerProvider first composes a URI on the form <code>&lt;scheme&gt;://localhost[:&lt;port&gt;]/&lt;path&gt;</code>
+ * that matches the content of the accepted connection, and passes that URI to the CurrentContainer interface. This
+ * creates a com.yahoo.jdisc.core.ContainerSnapshot that holds a reference to the {@link
+ * com.yahoo.jdisc.Container Container} that is currently active, and resolves the appropriate {@link
+ * com.yahoo.jdisc.application.BindingSet BindingSet} for the given URI through the Application's {@link
+ * com.yahoo.jdisc.application.BindingSetSelector BindingSetSelector}. This snapshot becomes the context of the new
+ * Request to ensure that all further processing of that Request happens within the same Container instace. Finally, the
+ * appropriate RequestHandler is resolved by the selected BindingSet, and the Request is dispatched.</p>
+ *
+<pre>
+private final ServerProvider server;
+
+&#64;Inject
+MyApplication(CurrentContainer container) {
+ server = new MyServerProvider(container);
+ server.start();
+}
+</pre>
+ *
+ * <h3>ClientProvider</h3>
+ * <p>A ClientProvider extends the RequestHandler interface, adding a method for initiating the startup of the provider.
+ * This is to allow an Application to develop a common ClientProvider install path. As opposed to RequestHandlers that
+ * are bound to URIs with the "localhost" hostname that the ServerProviders use when creating a Request, a
+ * ClientProvider is typically bound using a hostname wildcard (the '*' character). Because BindingSet considers a
+ * wildcard match to be weaker than a verbatim match, only Requests with URIs that are not bound to a local
+ * RequestHandler are passed to the ClientProvider.</p>
+ *
+<pre>
+private final ClientProvider client;
+
+&#64;Inject
+MyApplication(ContainerActivator activator, CurrentContainer container) {
+ client = new MyClientProvider();
+ client.start();
+
+ ContainerBuilder builder = activator.newContainerBuilder();
+ builder.serverBindings().bind("http://localhost/*", new MyRequestHandler());
+ builder.clientBindings().bind("http://&#42;/*", client);
+ activator.activateContainer(builder);
+}
+</pre>
+ *
+ * <p>Because the dispatch to a ClientProvider uses the same mechanics as the dispatch to an ordinary RequestHandler
+ * (i.e. the BindingSet), it is possible to create a test-mode BindingSet and a test-aware BindingSetSelector which
+ * dispatches to mock-up RequestHandlers instead of remote servers. The immediate benefit of this is that regression
+ * tests can be run on an Application otherwise configured for production traffic, allowing you to stress actual
+ * production code instead of targeted-only unit tests. This is how you would install a custom BindingSetSelector:</p>
+ *
+<pre>
+&#64;Inject
+MyApplication(ContainerActivator activator, CurrentContainer container) {
+ ContainerBuilder builder = activator.newContainerBuilder();
+ builder.clientBindings().bind("http://bing.com/*", new BingClientProvider());
+ builder.clientBindings("test").bind("http://bing.com/*", new BingMockupProvider());
+ builder.guiceModules().install(new MyBindingSetSelector());
+ activator.activateContainer(builder);
+}
+</pre>
+ *
+ * @see com.yahoo.jdisc
+ * @see com.yahoo.jdisc.application
+ * @see com.yahoo.jdisc.handler
+ */
+@com.yahoo.api.annotations.PublicApi
+package com.yahoo.jdisc.service;
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingClientProvider.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingClientProvider.java
new file mode 100644
index 00000000000..5c384fd2ddf
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingClientProvider.java
@@ -0,0 +1,29 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.test;
+
+import com.yahoo.jdisc.NoopSharedResource;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.service.ClientProvider;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class NonWorkingClientProvider extends NoopSharedResource implements ClientProvider {
+
+ @Override
+ public void start() {
+
+ }
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void handleTimeout(Request request, ResponseHandler handler) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingCompletionHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingCompletionHandler.java
new file mode 100644
index 00000000000..406e8a0235f
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingCompletionHandler.java
@@ -0,0 +1,20 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.test;
+
+import com.yahoo.jdisc.handler.CompletionHandler;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class NonWorkingCompletionHandler implements CompletionHandler {
+
+ @Override
+ public void completed() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void failed(Throwable t) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingContentChannel.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingContentChannel.java
new file mode 100644
index 00000000000..c8019eb513b
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingContentChannel.java
@@ -0,0 +1,23 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.test;
+
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+
+import java.nio.ByteBuffer;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class NonWorkingContentChannel implements ContentChannel {
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingOsgiFramework.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingOsgiFramework.java
new file mode 100644
index 00000000000..1e7d46bda4d
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingOsgiFramework.java
@@ -0,0 +1,50 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.test;
+
+import com.yahoo.jdisc.application.OsgiFramework;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class NonWorkingOsgiFramework implements OsgiFramework {
+
+ @Override
+ public List<Bundle> installBundle(String bundleLocation) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void startBundles(List<Bundle> bundles, boolean privileged) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void refreshPackages() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public BundleContext bundleContext() {
+ return null;
+ }
+
+ @Override
+ public List<Bundle> bundles() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public void start() {
+
+ }
+
+ @Override
+ public void stop() {
+
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingRequest.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingRequest.java
new file mode 100644
index 00000000000..1a285f3bf22
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingRequest.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.test;
+
+import com.google.inject.Module;
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.application.Application;
+
+import java.net.URI;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class NonWorkingRequest {
+
+ private NonWorkingRequest() {
+ // hide
+ }
+
+ /**
+ * <p>Factory method to create a {@link Request} without an associated {@link Container}. The design of jDISC does
+ * not allow this, so this method internally creates TestDriver, activates a Container, and creates a new Request
+ * from that Container. Before returning, this method {@link Request#release() closes} the Request, and calls {@link
+ * TestDriver#close()} on the TestDriver. This means that you MUST NOT attempt to access any Container features
+ * through the created Request. This factory is only for directed feature tests that require a non-null
+ * Request.</p>
+ *
+ * @param uri The URI string to assign to the Request.
+ * @param guiceModules The guice modules to inject into the {@link Application}.
+ * @return A non-working Request.
+ */
+ public static Request newInstance(String uri, Module... guiceModules) {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(guiceModules);
+ driver.activateContainer(driver.newContainerBuilder());
+ Request request = new Request(driver, URI.create(uri));
+ request.release();
+ driver.close();
+ return request;
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingRequestHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingRequestHandler.java
new file mode 100644
index 00000000000..d95b62186b2
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingRequestHandler.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.test;
+
+import com.yahoo.jdisc.NoopSharedResource;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.handler.ResponseHandler;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class NonWorkingRequestHandler extends NoopSharedResource implements RequestHandler {
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void handleTimeout(Request request, ResponseHandler handler) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingResponseHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingResponseHandler.java
new file mode 100644
index 00000000000..4f82df1c3e7
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingResponseHandler.java
@@ -0,0 +1,17 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.test;
+
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class NonWorkingResponseHandler implements ResponseHandler {
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingServerProvider.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingServerProvider.java
new file mode 100644
index 00000000000..79d57024359
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/NonWorkingServerProvider.java
@@ -0,0 +1,21 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.test;
+
+import com.yahoo.jdisc.NoopSharedResource;
+import com.yahoo.jdisc.service.ServerProvider;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class NonWorkingServerProvider extends NoopSharedResource implements ServerProvider {
+
+ @Override
+ public void start() {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/ServerProviderConformanceTest.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/ServerProviderConformanceTest.java
new file mode 100644
index 00000000000..ca52f3ab95b
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/ServerProviderConformanceTest.java
@@ -0,0 +1,3143 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.test;
+
+import com.google.common.annotations.Beta;
+import com.google.inject.AbstractModule;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.name.Names;
+import com.google.inject.util.Modules;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.BindingSetSelector;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.handler.AbstractRequestHandler;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.service.ServerProvider;
+
+import javax.annotation.CheckReturnValue;
+import java.io.Closeable;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Stream;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+@SuppressWarnings("UnusedDeclaration")
+@Beta
+public abstract class ServerProviderConformanceTest {
+ private static final int NUM_RUNS_EACH_TEST = 10;
+
+ /**
+ * <p>This interface declares the adapter between the general conformance test and an actual <tt>ServerProvider</tt>
+ * implementation. Every test runs as follows:</p>
+ * <ol>
+ * <li>{@link #newConfigModule()} is called to bind server-specific configuration.</li>
+ * <li>{@link #getServerProviderClass()} is called, and guice is asked to construct an instance of that class.</li>
+ * <li>{@link #newClient(ServerProvider)} is called one or more times as required by the test case.</li>
+ * <li>{@link #executeRequest(Object, boolean)} is called one or more times per client, as required by the test case.</li>
+ * <li>{@link #validateResponse(Object)} is called once per call to {@link #executeRequest(Object, boolean)}.</li>
+ * </ol>
+ *
+ * @param <T> The <tt>ServerProvider</tt> under test.
+ * @param <U> An object that represents a remote client that can connect to the server.
+ * @param <V> An object that holds the response generated by the client when executing a request.
+ */
+ public interface Adapter<T extends ServerProvider, U, V> {
+
+ Module newConfigModule();
+
+ Class<T> getServerProviderClass();
+
+ U newClient(T server) throws Throwable;
+
+ V executeRequest(U client, boolean withRequestContent) throws Throwable;
+
+ Iterable<ByteBuffer> newResponseContent();
+
+ void validateResponse(V response) throws Throwable;
+ }
+
+ /**
+ * <p>An instance of this exception is thrown within the conformance tests that imply that they will throw an
+ * exception. If your <tt>ServerProvider</tt> is capable of exposing such information, then this class is what you
+ * need to look for in the output.</p>
+ */
+ public static class ConformanceException extends RuntimeException {
+ private final Event peekEvent;
+
+ public ConformanceException() {
+ peekEvent = null;
+ }
+
+ /**
+ * In some tests, we want to ensure that a thrown exception has been handled by the framework before
+ * we do something else. There is no official hook to receive notification that the framework has
+ * handled an exception, but we assume (actually know) that the message of the exception will be
+ * accessed to create an error message. The provided event will signal that the exception
+ * has been _looked at_ by the framework, which we treat as synonymous with "handled" (due to
+ * synchronization in the framework, it is).
+ */
+ public ConformanceException(final Event peekEvent) {
+ this.peekEvent = peekEvent;
+ }
+
+ @Override
+ public String getMessage() {
+ if (peekEvent != null) {
+ peekEvent.happened();
+ }
+ return super.getMessage();
+ }
+ }
+
+ /* The following section declares and implements all test cases for the ServerProvider conformance test. When
+ * subclassing this test, you must implement these methods, annotate them as test methods and call runTest()
+ * from within each of them with an appropriate adapter instance.
+ *
+ * The test set up various scenarios with successes, failures and exceptions in different places and with
+ * different timing. There are many dimensions to test across, hence some really long method names. Some
+ * notes about the naming "scheme":
+ * - "testRequest<Something>" means the funky stuff happens in the handleRequest() method.
+ * - "testRequestContent<Something>" indicates that the funky stuff happens in the request content channel's code.
+ * - "testResponse<Something>" indicates that the funky stuff happens with the response content channel.
+ * - "Failure" means that failed() is called on some completion handler (the method name should indicate which).
+ * - "Nondeterministic" exception/failure means that it can occur before, during or after writing response content.
+ * The reason we include non-deterministic tests is that the deterministic ones involve synchronization, which
+ * may hide race conditions in the underlying processing. So we want some tests that "run free" as well.
+ * - "WithSync<Something>" means that anything NOT mentioned happens asynchronously, in a different thread.
+ * - "Before"/"After" is significant in some protocols; e.g. in http, status and headers are committed at one point.
+ * - "NoContent" refers to response content.
+ * There are quite likely possible scenarios that are not tested, but this is a good portion.
+ */
+
+ public abstract void testContainerNotReadyException() throws Throwable;
+ private <T extends ServerProvider, U, V> void testContainerNotReadyException(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.override(Modules.combine(config)).with(newActivateContainer(false)),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(Request request, ResponseHandler handler) {
+ throw new AssertionError();
+ }
+ });
+ }
+
+ public abstract void testBindingSetNotFoundException() throws Throwable;
+ private <T extends ServerProvider, U, V> void testBindingSetNotFoundException(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.override(Modules.combine()).with(newBindingSetSelector("unknown")),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(Request request, ResponseHandler handler) {
+ throw new AssertionError();
+ }
+ });
+ }
+
+ public abstract void testNoBindingSetSelectedException() throws Throwable;
+ private <T extends ServerProvider, U, V> void testNoBindingSetSelectedException(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.override(Modules.combine()).with(newBindingSetSelector(null)),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(Request request, ResponseHandler handler) {
+ throw new AssertionError();
+ }
+ });
+ }
+
+ public abstract void testBindingNotFoundException() throws Throwable;
+ private <T extends ServerProvider, U, V> void testBindingNotFoundException(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.override(Modules.combine(config)).with(newServerBinding("not://found/")),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(Request request, ResponseHandler handler) {
+ throw new AssertionError();
+ }
+ });
+ }
+
+ public abstract void testRequestHandlerWithSyncCloseResponse() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestHandlerWithSyncCloseResponse(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ }
+ });
+ }
+
+ public abstract void testRequestHandlerWithSyncWriteResponse() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestHandlerWithSyncWriteResponse(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ writeResponse(out);
+ closeResponseInOtherThread(out);
+ return null;
+ }
+ });
+ }
+
+ public abstract void testRequestHandlerWithSyncHandleResponse() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestHandlerWithSyncHandleResponse(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+ return null;
+ }
+ });
+ }
+
+ public abstract void testRequestHandlerWithAsyncHandleResponse() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestHandlerWithAsyncHandleResponse(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ respondWithContentInOtherThread(handler);
+ return null;
+ }
+ });
+ }
+
+ public abstract void testRequestException() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestException(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(Request request, ResponseHandler handler) {
+ throw new ConformanceException();
+ }
+ });
+ }
+
+ public abstract void testRequestExceptionWithSyncCloseResponse() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestExceptionWithSyncCloseResponse(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ writeResponse(out);
+ closeResponse(out);
+ throw new ConformanceException();
+ }
+ });
+ }
+
+ public abstract void testRequestExceptionWithSyncWriteResponse() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestExceptionWithSyncWriteResponse(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ writeResponse(out);
+ closeResponseInOtherThread(out);
+ throw new ConformanceException();
+ }
+ });
+ }
+
+ public abstract void testRequestNondeterministicExceptionWithSyncHandleResponse() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestNondeterministicExceptionWithSyncHandleResponse(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+ throw new ConformanceException();
+ }
+ });
+ }
+
+ public abstract void testRequestExceptionBeforeResponseWriteWithSyncHandleResponse() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestExceptionBeforeResponseWriteWithSyncHandleResponse(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ final Event exceptionHandledByFramework = new Event();
+ callInOtherThread(() -> {
+ exceptionHandledByFramework.await();
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+ throw new ConformanceException(exceptionHandledByFramework);
+ }
+ });
+ }
+
+ public abstract void testRequestExceptionAfterResponseWriteWithSyncHandleResponse() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestExceptionAfterResponseWriteWithSyncHandleResponse(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+ responseWritten.await();
+ throw new ConformanceException();
+ }
+ });
+ }
+
+ public abstract void testRequestNondeterministicExceptionWithAsyncHandleResponse() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestNondeterministicExceptionWithAsyncHandleResponse(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ callInOtherThread(new Callable<Void>() {
+
+ @Override
+ public Void call() throws Exception {
+ try {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ closeResponse(out);
+ } catch (Throwable ignored) {
+
+ }
+ return null;
+ }
+ });
+ throw new ConformanceException();
+ }
+ });
+ }
+
+ public abstract void testRequestExceptionBeforeResponseWriteWithAsyncHandleResponse() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestExceptionBeforeResponseWriteWithAsyncHandleResponse(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final Event exceptionHandledByFramework = new Event();
+ callInOtherThread(new Callable<Void>() {
+
+ @Override
+ public Void call() throws Exception {
+ exceptionHandledByFramework.await();
+ try {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ exceptionHandledByFramework.await();
+ writeResponse(out);
+ closeResponse(out);
+ } catch (Throwable ignored) {
+
+ }
+ return null;
+ }
+ });
+ throw new ConformanceException(exceptionHandledByFramework);
+ }
+ });
+ }
+
+ public abstract void testRequestExceptionAfterResponseCloseNoContentWithAsyncHandleResponse() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestExceptionAfterResponseCloseNoContentWithAsyncHandleResponse(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ callInOtherThread(() -> {
+ try {
+ respondNoContent(handler);
+ } catch (Throwable ignored) {
+
+ }
+ return null;
+ });
+ responseClosed.await();
+ throw new ConformanceException();
+ }
+ });
+ }
+
+ public abstract void testRequestExceptionAfterResponseWriteWithAsyncHandleResponse() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestExceptionAfterResponseWriteWithAsyncHandleResponse(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ callInOtherThread(new Callable<Void>() {
+
+ @Override
+ public Void call() throws Exception {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ }
+ });
+ responseWritten.await();
+ throw new ConformanceException();
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteWithSyncCompletion() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteWithSyncCompletion(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ respondWithContentInOtherThread(handler);
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteWithAsyncCompletion() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteWithAsyncCompletion(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ respondWithContentInOtherThread(handler);
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ completeInOtherThread(handler);
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteWithNondeterministicSyncFailure() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteWithNondeterministicSyncFailure(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.failed(new ConformanceException());
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteWithSyncFailureBeforeResponseWrite() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteWithSyncFailureBeforeResponseWrite(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ final Event failDone = new Event();
+ callInOtherThread(() -> {
+ failDone.await();
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ try {
+ handler.failed(new ConformanceException());
+ } finally {
+ failDone.happened();
+ }
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteWithSyncFailureAfterResponseWrite() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteWithSyncFailureAfterResponseWrite(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ responseWritten.await();
+ handler.failed(new ConformanceException());
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteWithNondeterministicAsyncFailure() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteWithNondeterministicAsyncFailure(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ callInOtherThread(() -> {
+ handler.failed(new ConformanceException());
+ return null;
+ });
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteWithAsyncFailureBeforeResponseWrite() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteWithAsyncFailureBeforeResponseWrite(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ final Event failDone = new Event();
+ callInOtherThread(() -> {
+ failDone.await();
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ callInOtherThread(() -> {
+ try {
+ handler.failed(new ConformanceException());
+ } finally {
+ failDone.happened();
+ }
+ return null;
+ });
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteWithAsyncFailureAfterResponseWrite() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteWithAsyncFailureAfterResponseWrite(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ callInOtherThread(() -> {
+ responseWritten.await();
+ handler.failed(new ConformanceException());
+ return null;
+ });
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteWithAsyncFailureAfterResponseCloseNoContent() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteWithAsyncFailureAfterResponseCloseNoContent(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ callInOtherThread(() -> {
+ responseClosed.await();
+ handler.failed(new ConformanceException());
+ return null;
+ });
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteNondeterministicException() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteNondeterministicException(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ throw new ConformanceException();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteExceptionBeforeResponseWrite() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionBeforeResponseWrite(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ final Event exceptionHandledByFramework = new Event();
+ callInOtherThread(() -> {
+ exceptionHandledByFramework.await();
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ throw new ConformanceException(exceptionHandledByFramework);
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteExceptionAfterResponseWrite() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionAfterResponseWrite(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ responseWritten.await();
+ throw new ConformanceException();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteExceptionAfterResponseCloseNoContent() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionAfterResponseCloseNoContent(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ responseClosed.await();
+ throw new ConformanceException();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteNondeterministicExceptionWithSyncCompletion() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteNondeterministicExceptionWithSyncCompletion(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ throw new ConformanceException();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteExceptionBeforeResponseWriteWithSyncCompletion() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionBeforeResponseWriteWithSyncCompletion(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ final Event exceptionHandledByFramework = new Event();
+ callInOtherThread(() -> {
+ exceptionHandledByFramework.await();
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ throw new ConformanceException(exceptionHandledByFramework);
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteExceptionAfterResponseWriteWithSyncCompletion() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionAfterResponseWriteWithSyncCompletion(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ responseWritten.await();
+ throw new ConformanceException();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteExceptionAfterResponseCloseNoContentWithSyncCompletion() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionAfterResponseCloseNoContentWithSyncCompletion(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ responseClosed.await();
+ throw new ConformanceException();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteNondeterministicExceptionWithAsyncCompletion() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteNondeterministicExceptionWithAsyncCompletion(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ completeInOtherThread(handler, IllegalStateException.class);
+ throw new ConformanceException();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteExceptionBeforeResponseWriteWithAsyncCompletion() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionBeforeResponseWriteWithAsyncCompletion(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ final Event exceptionHandledByFramework = new Event();
+ callInOtherThread(() -> {
+ exceptionHandledByFramework.await();
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ completeInOtherThread(handler, IllegalStateException.class);
+ throw new ConformanceException(exceptionHandledByFramework);
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteExceptionAfterResponseWriteWithAsyncCompletion() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionAfterResponseWriteWithAsyncCompletion(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ completeInOtherThread(handler, IllegalStateException.class);
+ responseWritten.await();
+ throw new ConformanceException();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteExceptionAfterResponseCloseNoContentWithAsyncCompletion() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionAfterResponseCloseNoContentWithAsyncCompletion(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ completeInOtherThread(handler, IllegalStateException.class);
+ responseClosed.await();
+ throw new ConformanceException();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteExceptionWithNondeterministicSyncFailure() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionWithNondeterministicSyncFailure(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.failed(new ConformanceException());
+ throw new ConformanceException();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteExceptionWithSyncFailureBeforeResponseWrite() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionWithSyncFailureBeforeResponseWrite(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ final Event failDone = new Event();
+ callInOtherThread(() -> {
+ failDone.await();
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ try {
+ handler.failed(new ConformanceException());
+ } finally {
+ failDone.happened();
+ }
+ throw new ConformanceException();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteExceptionWithSyncFailureAfterResponseWrite() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionWithSyncFailureAfterResponseWrite(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ responseWritten.await();
+ handler.failed(new ConformanceException());
+ throw new ConformanceException();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteExceptionWithSyncFailureAfterResponseCloseNoContent() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionWithSyncFailureAfterResponseCloseNoContent(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ responseClosed.await();
+ handler.failed(new ConformanceException());
+ throw new ConformanceException();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteExceptionWithNondeterministicAsyncFailure() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionWithNondeterministicAsyncFailure(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ callInOtherThread(() -> {
+ fail(handler, new ConformanceException(), IllegalStateException.class);
+ return null;
+ });
+ throw new ConformanceException();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteExceptionWithAsyncFailureBeforeResponseWrite() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionWithAsyncFailureBeforeResponseWrite(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ final Event exceptionHandled = new Event();
+
+ callInOtherThread(() -> {
+ exceptionHandled.await();
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ callInOtherThread(() -> {
+ fail(handler, new ConformanceException(exceptionHandled), IllegalStateException.class);
+ return null;
+ });
+ throw new ConformanceException(exceptionHandled);
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteExceptionWithAsyncFailureAfterResponseWrite() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionWithAsyncFailureAfterResponseWrite(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ callInOtherThread(() -> {
+ responseWritten.await();
+ fail(handler, new ConformanceException(), IllegalStateException.class);
+ return null;
+ });
+ responseWritten.await();
+ throw new ConformanceException();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentWriteExceptionWithAsyncFailureAfterResponseCloseNoContent() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentWriteExceptionWithAsyncFailureAfterResponseCloseNoContent(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITH_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ callInOtherThread(() -> {
+ responseClosed.await();
+ fail(handler, new ConformanceException(), IllegalStateException.class);
+ return null;
+ });
+ responseClosed.await();
+ throw new ConformanceException();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseWithSyncCompletion() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseWithSyncCompletion(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ respondWithContentInOtherThread(handler);
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseWithAsyncCompletion() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseWithAsyncCompletion(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ respondWithContentInOtherThread(handler);
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ completeInOtherThread(handler);
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseWithNondeterministicSyncFailure() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseWithNondeterministicSyncFailure(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.failed(new ConformanceException());
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseWithSyncFailureBeforeResponseWrite() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseWithSyncFailureBeforeResponseWrite(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ final Event failDone = new Event();
+ callInOtherThread(() -> {
+ failDone.await();
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ try {
+ handler.failed(new ConformanceException());
+ } finally {
+ failDone.happened();
+ }
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseWithSyncFailureAfterResponseWrite() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseWithSyncFailureAfterResponseWrite(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ responseWritten.await();
+ handler.failed(new ConformanceException());
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseWithSyncFailureAfterResponseCloseNoContent() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseWithSyncFailureAfterResponseCloseNoContent(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ responseClosed.await();
+ handler.failed(new ConformanceException());
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseWithNondeterministicAsyncFailure() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseWithNondeterministicAsyncFailure(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ respondWithContentInOtherThread(handler);
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ failInOtherThread(handler, new ConformanceException());
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseWithAsyncFailureBeforeResponseWrite() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseWithAsyncFailureBeforeResponseWrite(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final Event failDone = new Event();
+ callInOtherThread(() -> {
+ failDone.await();
+ respondWithContent(handler);
+ return null;
+ });
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ callInOtherThread(() -> {
+ try {
+ fail(handler, new ConformanceException());
+ } finally {
+ failDone.happened();
+ }
+ return null;
+ });
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseWithAsyncFailureAfterResponseWrite() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseWithAsyncFailureAfterResponseWrite(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ callInOtherThread(() -> {
+ respondWithContent(handler);
+ return null;
+ });
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ callInOtherThread(() -> {
+ responseWritten.await();
+ fail(handler, new ConformanceException());
+ return null;
+ });
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseWithAsyncFailureAfterResponseCloseNoContent() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseWithAsyncFailureAfterResponseCloseNoContent(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ callInOtherThread(() -> {
+ respondNoContent(handler);
+ return null;
+ });
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ callInOtherThread(() -> {
+ responseClosed.await();
+ fail(handler, new ConformanceException());
+ return null;
+ });
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseNondeterministicException() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseNondeterministicException(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ throw new ConformanceException();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseExceptionBeforeResponseWrite() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionBeforeResponseWrite(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ final Event exceptionHandledByFramework = new Event();
+ callInOtherThread(() -> {
+ exceptionHandledByFramework.await();
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ throw new ConformanceException(exceptionHandledByFramework);
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseExceptionAfterResponseWrite() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionAfterResponseWrite(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ responseWritten.await();
+ throw new ConformanceException();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseExceptionAfterResponseCloseNoContent() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionAfterResponseCloseNoContent(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ responseClosed.await();
+ throw new ConformanceException();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseNondeterministicExceptionWithSyncCompletion() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseNondeterministicExceptionWithSyncCompletion(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ respondWithContentInOtherThread(handler);
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ throw new ConformanceException();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseExceptionBeforeResponseWriteWithSyncCompletion() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionBeforeResponseWriteWithSyncCompletion(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final Event exceptionHandledByFramework = new Event();
+ callInOtherThread(() -> {
+ exceptionHandledByFramework.await();
+ respondWithContent(handler);
+ return null;
+ });
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ throw new ConformanceException(exceptionHandledByFramework);
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseExceptionAfterResponseWriteWithSyncCompletion() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionAfterResponseWriteWithSyncCompletion(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ callInOtherThread(() -> {
+ respondWithContent(handler);
+ return null;
+ });
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ responseWritten.await();
+ throw new ConformanceException();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncCompletion() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncCompletion(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ callInOtherThread(() -> {
+ respondNoContent(handler);
+ return null;
+ });
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.completed();
+ responseClosed.await();
+ throw new ConformanceException();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseNondeterministicExceptionWithAsyncCompletion() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseNondeterministicExceptionWithAsyncCompletion(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ respondWithContentInOtherThread(handler);
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ completeInOtherThread(handler, IllegalStateException.class);
+ throw new ConformanceException();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncCompletion() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncCompletion(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final Event exceptionHandledByFramework = new Event();
+ callInOtherThread(() -> {
+ exceptionHandledByFramework.await();
+ respondWithContent(handler);
+ return null;
+ });
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ completeInOtherThread(handler, IllegalStateException.class);
+ throw new ConformanceException(exceptionHandledByFramework);
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseExceptionAfterResponseWriteWithAsyncCompletion() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionAfterResponseWriteWithAsyncCompletion(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ respondWithContentInOtherThread(handler);
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ completeInOtherThread(handler, IllegalStateException.class);
+ responseWritten.await();
+ throw new ConformanceException();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncCompletion() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncCompletion(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ respondNoContentInOtherThread(handler);
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ completeInOtherThread(handler, IllegalStateException.class);
+ responseClosed.await();
+ throw new ConformanceException();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseNondeterministicExceptionWithSyncFailure() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseNondeterministicExceptionWithSyncFailure(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ handler.failed(new ConformanceException());
+ throw new ConformanceException();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseExceptionBeforeResponseWriteWithSyncFailure() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionBeforeResponseWriteWithSyncFailure(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ final Event failDone = new Event();
+ callInOtherThread(() -> {
+ failDone.await();
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ try {
+ handler.failed(new ConformanceException());
+ } finally {
+ failDone.happened();
+ }
+ throw new ConformanceException();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseExceptionAfterResponseWriteWithSyncFailure() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionAfterResponseWriteWithSyncFailure(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ responseWritten.await();
+ handler.failed(new ConformanceException());
+ throw new ConformanceException();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncFailure() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncFailure(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ responseClosed.await();
+ handler.failed(new ConformanceException());
+ throw new ConformanceException();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseNondeterministicExceptionWithAsyncFailure() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseNondeterministicExceptionWithAsyncFailure(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ callInOtherThread(() -> {
+ fail(handler, new ConformanceException(), IllegalStateException.class);
+ return null;
+ });
+ throw new ConformanceException();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncFailure() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncFailure(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ final Event exceptionHandled = new Event();
+
+ callInOtherThread(() -> {
+ exceptionHandled.await();
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+
+ callInOtherThread(() -> {
+ fail(handler, new ConformanceException(exceptionHandled), IllegalStateException.class);
+ return null;
+ });
+
+ throw new ConformanceException(exceptionHandled);
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseExceptionAfterResponseWriteWithAsyncFailure() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionAfterResponseWriteWithAsyncFailure(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ writeResponse(out);
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ callInOtherThread(() -> {
+ responseWritten.await();
+ fail(handler, new ConformanceException(), IllegalStateException.class);
+ return null;
+ });
+ responseWritten.await();
+ throw new ConformanceException();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncFailure() throws Throwable;
+ private <T extends ServerProvider, U, V> void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncFailure(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(final Request request, final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ callInOtherThread(() -> {
+ closeResponse(out);
+ return null;
+ });
+
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ callInOtherThread(() -> {
+ responseClosed.await();
+ fail(handler, new ConformanceException(), IllegalStateException.class);
+ return null;
+ });
+ responseClosed.await();
+ throw new ConformanceException();
+ }
+ };
+ }
+ });
+ }
+
+ public abstract void testResponseWriteCompletionException() throws Throwable;
+ private <T extends ServerProvider, U, V> void testResponseWriteCompletionException(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(Request request, ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ for (ByteBuffer buf : adapter.newResponseContent()) {
+ out.write(buf, EXCEPTION_COMPLETION_HANDLER);
+ }
+ closeResponse(out);
+ return null;
+ }
+ });
+ }
+
+ public abstract void testResponseCloseCompletionException() throws Throwable;
+ private <T extends ServerProvider, U, V> void testResponseCloseCompletionException(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(Request request, ResponseHandler handler) {
+ ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ writeResponse(out);
+ out.close(EXCEPTION_COMPLETION_HANDLER);
+ return null;
+ }
+ });
+ }
+
+ public abstract void testResponseCloseCompletionExceptionNoContent() throws Throwable;
+ private <T extends ServerProvider, U, V> void testResponseCloseCompletionExceptionNoContent(
+ final Adapter<T, U, V> adapter,
+ final Module... config)
+ throws Throwable {
+ runTest(adapter,
+ Modules.combine(config),
+ RequestType.WITHOUT_CONTENT,
+ new TestRequestHandler() {
+
+ @Override
+ public ContentChannel handle(Request request, ResponseHandler handler) {
+ ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ out.close(EXCEPTION_COMPLETION_HANDLER);
+ return null;
+ }
+ });
+ }
+
+ // -------------------------------------------------------------------------------------------------------------- //
+ // //
+ // The following section is implementation details that are not necessary to understand in order to implement a //
+ // conformance test for a ServerProvider. //
+ // //
+ // -------------------------------------------------------------------------------------------------------------- //
+
+ protected <T extends ServerProvider, U, V> void runTest(
+ final Adapter<T, U, V> adapter,
+ final Module... guiceModules)
+ throws Throwable {
+ Class<ServerProviderConformanceTest> clazz = ServerProviderConformanceTest.class;
+ StackTraceElement[] stack = Thread.currentThread().getStackTrace();
+ for (StackTraceElement element : stack) {
+ Method method;
+ final String methodName = element.getMethodName();
+ try {
+ method = clazz.getDeclaredMethod(methodName);
+ } catch (NoSuchMethodException e) {
+ continue;
+ }
+ if (!Modifier.isAbstract(method.getModifiers())) {
+ continue;
+ }
+ try {
+ method = clazz.getDeclaredMethod(methodName, Adapter.class, Module[].class);
+ System.out.println("Invoking test method " + methodName);
+ method.invoke(this, adapter, guiceModules);
+ return;
+ } catch (InvocationTargetException e) {
+ throw e.getCause();
+ }
+ }
+ throw new UnsupportedOperationException("Method runTest() not called from overridden testXXX() method.");
+ }
+
+ // The only purpose of this is to avoid magic literals in calls to runTest (which we'd have if we used a bool flag).
+ private enum RequestType {
+ WITHOUT_CONTENT, WITH_CONTENT
+ }
+
+ private <T extends ServerProvider, U, V> void runTest(
+ final Adapter<T, U, V> adapter,
+ final Module testConfig,
+ final RequestType requestType,
+ final TestRequestHandler requestHandler)
+ throws Throwable {
+ final Module config = Modules.override(newDefaultConfig(), adapter.newConfigModule()).with(testConfig);
+ final TestDriver driver = TestDriver.newSimpleApplicationInstance(config);
+ final ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind(builder.getInstance(Key.get(String.class, Names.named("serverBinding"))),
+ requestHandler);
+ final T serverProvider = builder.guiceModules().getInstance(adapter.getServerProviderClass());
+ builder.serverProviders().install(serverProvider);
+ if (builder.getInstance(Key.get(Boolean.class, Names.named("activateContainer")))) {
+ driver.activateContainer(builder);
+ }
+ serverProvider.start();
+ serverProvider.release();
+
+ for (int i = 0; i < NUM_RUNS_EACH_TEST; ++i) {
+ System.out.println("Test run #" + i);
+ requestHandler.reset(adapter.newResponseContent());
+ final U client = adapter.newClient(serverProvider);
+ final boolean withRequestContent = requestType == RequestType.WITH_CONTENT;
+ final V result = adapter.executeRequest(client, withRequestContent);
+ adapter.validateResponse(result);
+ if (client instanceof Closeable) {
+ ((Closeable) client).close();
+ }
+ requestHandler.awaitAsyncTasks();
+ }
+
+ serverProvider.close();
+ driver.close();
+ }
+
+ private static Module newDefaultConfig() {
+ return Modules.combine(newServerBinding("*://*/*"),
+ newActivateContainer(true));
+ }
+
+ private static Module newBindingSetSelector(final String bindingSetName) {
+ return new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(BindingSetSelector.class).toInstance(uri -> bindingSetName);
+ }
+ };
+ }
+
+ private static Module newServerBinding(final String serverBinding) {
+ return new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(String.class).annotatedWith(Names.named("serverBinding")).toInstance(serverBinding);
+ }
+ };
+ }
+
+ private static Module newActivateContainer(final boolean activateContainer) {
+ return new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(Boolean.class).annotatedWith(Names.named("activateContainer")).toInstance(activateContainer);
+ }
+ };
+ }
+
+ /**
+ * Wrapper around CountDownLatch for single-occurrence events.
+ */
+ private static class Event {
+ private final CountDownLatch latch = new CountDownLatch(1);
+
+ public void happened() {
+ latch.countDown();
+ }
+
+ public void await() {
+ try {
+ final boolean success = latch.await(600, TimeUnit.SECONDS);
+ if (!success) {
+ throw new IllegalStateException("Wait for required condition timed out");
+ }
+ } catch (InterruptedException e) {
+ throw new IllegalStateException("Wait for required condition was interrupted", e);
+ }
+ }
+ }
+
+ private static abstract class TestRequestHandler extends AbstractRequestHandler {
+
+ private static class TaskHandle {
+ private final Exception stackTrace = new Exception();
+
+ @Override
+ public String toString() {
+ final StringWriter stringWriter = new StringWriter();
+ stackTrace.printStackTrace(new PrintWriter(stringWriter));
+ return "(" + stringWriter.toString() + ")";
+ }
+ }
+
+ protected Event responseWritten;
+ protected Event responseClosed;
+ private ExecutorService executor;
+ private final Object taskMonitor = new Object();
+ private Set<TaskHandle> pendingTasks = new HashSet<>();
+ private Exception taskException;
+ private Iterable<ByteBuffer> responseContent;
+
+ public void reset(final Iterable<ByteBuffer> responseContent) {
+ synchronized (taskMonitor) {
+ if (!pendingTasks.isEmpty()) {
+ throw new AssertionError("pendingTasks should be empty, was " + pendingTasks);
+ }
+ }
+ this.executor = Executors.newCachedThreadPool();
+ this.responseWritten = new Event();
+ this.responseClosed = new Event();
+ this.responseContent = responseContent;
+ this.taskException = null;
+ }
+
+ protected final void callInOtherThread(final Callable<Void> task) {
+ final TaskHandle taskHandle = addTask();
+ final Runnable runnable = () -> {
+ try {
+ task.call();
+ } catch (Exception e) {
+ setTaskFailure(e);
+ }
+ removeTask(taskHandle);
+ };
+ try {
+ executor.submit(runnable);
+ } catch (RejectedExecutionException e) {
+ setTaskFailure(e);
+ removeTask(taskHandle);
+ }
+ }
+
+ private void setTaskFailure(Exception e) {
+ synchronized (taskMonitor) {
+ if (taskException == null) {
+ taskException = e;
+ } else {
+ System.out.println("Got subsequent exception in task execution: ");
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void removeTask(final TaskHandle taskHandle) {
+ synchronized (taskMonitor) {
+ pendingTasks.remove(taskHandle);
+ taskMonitor.notifyAll();
+ }
+ }
+
+ @CheckReturnValue
+ private TaskHandle addTask() {
+ final TaskHandle taskHandle = new TaskHandle();
+ synchronized (taskMonitor) {
+ pendingTasks.add(taskHandle);
+ }
+ return taskHandle;
+ }
+
+ protected final void writeResponse(final ContentChannel out) {
+ try {
+ writeAll(out, responseContent);
+ } finally {
+ responseWritten.happened();
+ }
+ }
+
+ private void writeAll(final ContentChannel out, final Iterable<ByteBuffer> content) {
+ for (ByteBuffer buf : content) {
+ out.write(buf, newCompletionHandler());
+ }
+ }
+
+ protected final void closeResponseInOtherThread(final ContentChannel out) {
+ callInOtherThread(() -> {
+ closeResponse(out);
+ return null;
+ });
+ }
+
+ protected final void closeResponse(final ContentChannel out) {
+ try {
+ out.close(newCompletionHandler());
+ } finally {
+ responseClosed.happened();
+ }
+ }
+
+ protected final CompletionHandler newCompletionHandler() {
+ final CallableCompletionHandler handler = new CallableCompletionHandler();
+ callInOtherThread(handler);
+ return handler;
+ }
+
+ protected final void respondWithContentInOtherThread(final ResponseHandler handler) {
+ callInOtherThread(() -> {
+ respondWithContent(handler);
+ return null;
+ });
+ }
+
+ protected final void respondNoContentInOtherThread(final ResponseHandler handler) {
+ callInOtherThread(() -> {
+ respondNoContent(handler);
+ return null;
+ });
+ }
+
+ protected void respondWithContent(final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ writeResponse(out);
+ closeResponse(out);
+ }
+
+ protected void respondNoContent(final ResponseHandler handler) {
+ final ContentChannel out = handler.handleResponse(new Response(Response.Status.OK));
+ closeResponse(out);
+ }
+
+ protected final void completeInOtherThread(
+ final CompletionHandler handler,
+ final Class<?>... allowedExceptionTypes) {
+ callInOtherThread(() -> {
+ try {
+ handler.completed();
+ } catch (Throwable t) {
+ if (!isInstanceOfAnyOf(t, allowedExceptionTypes)) {
+ throw t;
+ }
+ }
+ return null;
+ });
+ }
+
+ protected final void fail(
+ final CompletionHandler handler,
+ final Throwable failure,
+ final Class<?>... allowedExceptionTypes) {
+ try {
+ handler.failed(failure);
+ } catch (Throwable t) {
+ if (!isInstanceOfAnyOf(t, allowedExceptionTypes)) {
+ throw t;
+ }
+ }
+ }
+
+ private static boolean isInstanceOfAnyOf(final Object object, final Class<?>... types) {
+ return Stream.of(types).anyMatch(type -> type.isAssignableFrom(object.getClass()));
+ }
+
+ protected final void failInOtherThread(
+ final CompletionHandler handler,
+ final Throwable failure,
+ final Class<?>... allowedExceptionTypes) {
+ callInOtherThread(() -> {
+ fail(handler, failure, allowedExceptionTypes);
+ return null;
+ });
+ }
+
+ @Override
+ public final ContentChannel handleRequest(final Request request, final ResponseHandler responseHandler) {
+ // Ensure that task executor is not shut down before handleResponse() is done.
+ final TaskHandle handleResponseTask = addTask();
+ try {
+ final ContentChannel requestContentChannel = handle(request, responseHandler);
+ if (requestContentChannel == null) {
+ return null;
+ }
+ // Ensure that task executor is not shut down before close() is done.
+ final TaskHandle requestContentChannelCloseTask = addTask();
+ return new ContentChannel() {
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ requestContentChannel.write(buf, handler);
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ try {
+ requestContentChannel.close(handler);
+ } finally {
+ removeTask(requestContentChannelCloseTask);
+ }
+ }
+ };
+ } finally {
+ removeTask(handleResponseTask);
+ }
+ }
+
+ protected abstract ContentChannel handle(Request request, ResponseHandler responseHandler);
+
+ public final void awaitAsyncTasks() throws Exception {
+ final long maxWaitTimeMillis = 600_000L;
+ final long startTimeMillis = System.currentTimeMillis();
+ synchronized (taskMonitor) {
+ while (!pendingTasks.isEmpty()) {
+ final long currentTimeMillis = System.currentTimeMillis();
+ final long timeElapsedMillis = currentTimeMillis - startTimeMillis;
+ if (timeElapsedMillis >= maxWaitTimeMillis) {
+ throw new IllegalStateException(
+ "Wait timed out, still have the following pending tasks: " + pendingTasks);
+ }
+ final long waitTimeMillis = maxWaitTimeMillis - timeElapsedMillis;
+ taskMonitor.wait(waitTimeMillis);
+ }
+ }
+ executor.shutdown();
+ final boolean haltedCleanly = executor.awaitTermination(600, TimeUnit.SECONDS);
+ if (!haltedCleanly) {
+ throw new IllegalStateException("Some tasks did not finish. executor=" + executor);
+ }
+ synchronized (taskMonitor) {
+ if (taskException != null) {
+ throw new Exception("Task threw exception", taskException);
+ }
+ }
+ }
+ }
+
+ private static class CallableCompletionHandler implements Callable<Void>, CompletionHandler {
+
+ final CountDownLatch done = new CountDownLatch(1);
+
+ @Override
+ public void completed() {
+ done.countDown();
+ }
+
+ @Override
+ public void failed(final Throwable t) {
+ done.countDown();
+ }
+
+ @Override
+ public Void call() throws Exception {
+ if (!done.await(600, TimeUnit.SECONDS)) {
+ throw new TimeoutException();
+ }
+ return null;
+ }
+ }
+
+ private static final CompletionHandler EXCEPTION_COMPLETION_HANDLER = new CompletionHandler() {
+
+ @Override
+ public void completed() {
+ throw new ConformanceException();
+ }
+
+ @Override
+ public void failed(Throwable t) {
+ throw new ConformanceException();
+ }
+ };
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/TestDriver.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/TestDriver.java
new file mode 100644
index 00000000000..f5a0f83f4ba
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/TestDriver.java
@@ -0,0 +1,402 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.test;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+import com.google.inject.binder.AnnotatedBindingBuilder;
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.*;
+import com.yahoo.jdisc.core.ApplicationLoader;
+import com.yahoo.jdisc.core.BootstrapLoader;
+import com.yahoo.jdisc.core.FelixFramework;
+import com.yahoo.jdisc.core.FelixParams;
+import com.yahoo.jdisc.handler.*;
+import com.yahoo.jdisc.service.CurrentContainer;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * <p>This class provides a unified way to set up and run unit tests on jDISC components. In short, it is a programmable
+ * {@link BootstrapLoader} that provides convenient access to the {@link ContainerActivator} and {@link
+ * CurrentContainer} interfaces. A typical test case using this class looks as follows:</p>
+ * <pre>
+ * {@literal @}Test
+ * public void requireThatMyComponentIsWellBehaved() {
+ * TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ * ContainerBuilder builder = driver.newContainerBuilder();
+ * (... configure builder ...)
+ * driver.activateContainer(builder);
+ * (... run tests ...)
+ * assertTrue(driver.close());
+ * }
+ * </pre>
+ * <p>One of the most important things to remember when using this class is to always call {@link #close()} at the end
+ * of your test case. This ensures that the tested configuration does not prevent graceful shutdown. If close() returns
+ * FALSE, it means that either your components or the test case itself does not conform to the reference counting
+ * requirements of {@link Request}, {@link RequestHandler}, {@link ContentChannel}, or {@link CompletionHandler}.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class TestDriver implements ContainerActivator, CurrentContainer {
+
+ private static final AtomicInteger testId = new AtomicInteger(0);
+ private final FutureTask<Boolean> closeTask = new FutureTask<>(new CloseTask());
+ private final ApplicationLoader loader;
+
+ private TestDriver(ApplicationLoader loader) {
+ this.loader = loader;
+ }
+
+ @Override
+ public ContainerBuilder newContainerBuilder() {
+ return loader.newContainerBuilder();
+ }
+
+ @Override
+ public DeactivatedContainer activateContainer(ContainerBuilder builder) {
+ return loader.activateContainer(builder);
+ }
+
+ @Override
+ public Container newReference(URI uri) {
+ return loader.newReference(uri);
+ }
+
+ /**
+ * <p>Returns the {@link BootstrapLoader} used by this TestDriver. Use caution when invoking methods on the
+ * BootstrapLoader directly, since the lifecycle management done by this TestDriver may become corrupt.</p>
+ *
+ * @return The BootstrapLoader.
+ */
+ public BootstrapLoader bootstrapLoader() {
+ return loader;
+ }
+
+ /**
+ * <p>Returns the {@link Application} loaded by this TestDriver. Until {@link #close()} is called, this method will
+ * never return null.</p>
+ *
+ * @return The loaded Application.
+ */
+ public Application application() {
+ return loader.application();
+ }
+
+ /**
+ * <p>Returns the {@link OsgiFramework} created by this TestDriver. Although this method will never return null, it
+ * might return a {@link NonWorkingOsgiFramework} depending on the factory method used to instantiate it.</p>
+ *
+ * @return The OSGi framework.
+ */
+ public OsgiFramework osgiFramework() {
+ return loader.osgiFramework();
+ }
+
+ /**
+ * <p>Convenience method to create and {@link Request#connect(ResponseHandler)} a {@link Request} on the {@link
+ * CurrentContainer}. This method will either return the corresponding {@link ContentChannel} or throw the
+ * appropriate exception (see {@link Request#connect(ResponseHandler)}).</p>
+ *
+ * @param requestUri The URI string to parse and pass to the Request constructor.
+ * @param responseHandler The ResponseHandler to pass to {@link Request#connect(ResponseHandler)}.
+ * @return The ContentChannel returned by {@link Request#connect(ResponseHandler)}.
+ * @throws NullPointerException If the URI string or the {@link ResponseHandler} is null.
+ * @throws IllegalArgumentException If the URI string violates RFC&nbsp;2396.
+ * @throws BindingNotFoundException If the corresponding call to {@link Container#resolveHandler(Request)}
+ * returns null.
+ * @throws RequestDeniedException If the corresponding call to {@link RequestHandler#handleRequest(Request,
+ * ResponseHandler)} returns null.
+ */
+ public ContentChannel connectRequest(String requestUri, ResponseHandler responseHandler) {
+ return newRequestDispatch(requestUri, responseHandler).connect();
+ }
+
+ /**
+ * <p>Convenience method to create a {@link Request}, connect it to a {@link RequestHandler}, and close the returned
+ * {@link ContentChannel}. This is the same as calling:</p>
+ * <pre>
+ * connectRequest(uri, responseHandler).close(null);
+ * </pre>
+ *
+ * @param requestUri The URI string to parse and pass to the Request constructor.
+ * @param responseHandler The ResponseHandler to pass to {@link Request#connect(ResponseHandler)}.
+ * @return A waitable Future that provides access to the corresponding {@link Response}.
+ * @throws NullPointerException If the URI string or the {@link ResponseHandler} is null.
+ * @throws IllegalArgumentException If the URI string violates RFC&nbsp;2396.
+ * @throws BindingNotFoundException If the corresponding call to {@link Container#resolveHandler(Request)}
+ * returns null.
+ * @throws RequestDeniedException If the corresponding call to {@link RequestHandler#handleRequest(Request,
+ * ResponseHandler)} returns null.
+ */
+ public Future<Response> dispatchRequest(String requestUri, ResponseHandler responseHandler) {
+ return newRequestDispatch(requestUri, responseHandler).dispatch();
+ }
+
+ /**
+ * <p>Initiates the shut down of this TestDriver in another thread. By doing this in a separate thread, it allows
+ * other code to monitor its progress. Unless you need the added monitoring capability, you should use {@link
+ * #close()} instead.</p>
+ *
+ * @see #awaitClose(long, TimeUnit)
+ */
+ public void scheduleClose() {
+ new Thread(closeTask, "TestDriver.Closer").start();
+ }
+
+ /**
+ * <p>Waits for shut down of this TestDriver to complete. This call must be preceded by a call to {@link
+ * #scheduleClose()}.</p>
+ *
+ * @param timeout The maximum time to wait.
+ * @param unit The time unit of the timeout argument.
+ * @return True if shut down completed within the allocated time.
+ */
+ public boolean awaitClose(long timeout, TimeUnit unit) {
+ try {
+ closeTask.get(timeout, unit);
+ return true;
+ } catch (TimeoutException e) {
+ return false;
+ } catch (Exception e) {
+ throw e instanceof RuntimeException ? (RuntimeException)e : new RuntimeException(e);
+ }
+ }
+
+ /**
+ * <p>Initiatiates shut down of this TestDriver and waits for it to complete. If shut down fails to complete within
+ * 60 seconds, this method throws an exception.</p>
+ *
+ * @return True if shut down completed within the allocated time.
+ * @throws IllegalStateException If shut down failed to complete within the allocated time.
+ */
+ public boolean close() {
+ scheduleClose();
+ if ( ! awaitClose(600, TimeUnit.SECONDS)) {
+ throw new IllegalStateException("Application failed to terminate within allocated time.");
+ }
+ return true;
+ }
+
+ /**
+ * <p>Creates a new {@link RequestDispatch} that dispatches a {@link Request} with the given URI and {@link
+ * ResponseHandler}.</p>
+ *
+ * @param requestUri The uri of the Request to create.
+ * @param responseHandler The ResponseHandler to use for the dispather.
+ * @return The created RequestDispatch.
+ */
+ public RequestDispatch newRequestDispatch(final String requestUri, final ResponseHandler responseHandler) {
+ return new RequestDispatch() {
+
+ @Override
+ protected Request newRequest() {
+ return new Request(loader, URI.create(requestUri));
+ }
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ return responseHandler.handleResponse(response);
+ }
+ };
+ }
+
+ /**
+ * <p>Creates a new TestDriver with an injected {@link Application}.</p>
+ *
+ * @param appClass The Application class to inject.
+ * @param guiceModules The Guice {@link Module Modules} to install prior to startup.
+ * @return The created TestDriver.
+ */
+ public static TestDriver newInjectedApplicationInstance(Class<? extends Application> appClass,
+ Module... guiceModules) {
+ return newInstance(newOsgiFramework(), null, false,
+ newModuleList(null, appClass, guiceModules));
+ }
+
+ /**
+ * <p>Creates a new TestDriver with an injected {@link Application}, but without OSGi support.</p>
+ *
+ * @param appClass The Application class to inject.
+ * @param guiceModules The Guice {@link Module Modules} to install prior to startup.
+ * @return The created TestDriver.
+ * @see #newInjectedApplicationInstance(Class, Module...)
+ * @see #newNonWorkingOsgiFramework()
+ */
+ public static TestDriver newInjectedApplicationInstanceWithoutOsgi(Class<? extends Application> appClass,
+ Module... guiceModules) {
+ return newInstance(newNonWorkingOsgiFramework(), null, false,
+ newModuleList(null, appClass, guiceModules));
+ }
+
+ /**
+ * <p>Creates a new TestDriver with an injected {@link Application}.</p>
+ *
+ * @param app The Application to inject.
+ * @param guiceModules The Guice {@link Module Modules} to install prior to startup.
+ * @return The created TestDriver.
+ */
+ public static TestDriver newInjectedApplicationInstance(Application app, Module... guiceModules) {
+ return newInstance(newOsgiFramework(), null, false, newModuleList(app, null, guiceModules));
+ }
+
+ /**
+ * <p>Creates a new TestDriver with an injected {@link Application}, but without OSGi support.</p>
+ *
+ * @param app The Application to inject.
+ * @param guiceModules The Guice {@link Module Modules} to install prior to startup.
+ * @return The created TestDriver.
+ * @see #newInjectedApplicationInstance(Application, Module...)
+ * @see #newNonWorkingOsgiFramework()
+ */
+ public static TestDriver newInjectedApplicationInstanceWithoutOsgi(Application app, Module... guiceModules) {
+ return newInstance(newNonWorkingOsgiFramework(), null, false, newModuleList(app, null, guiceModules));
+ }
+
+ /**
+ * <p>Creates a new TestDriver with a predefined {@link Application} implementation. The injected Application class
+ * implements nothing but the bare minimum to conform to the Application interface.</p>
+ *
+ * @param guiceModules The Guice {@link Module Modules} to install prior to startup.
+ * @return The created TestDriver.
+ */
+ public static TestDriver newSimpleApplicationInstance(Module... guiceModules) {
+ return newInstance(newOsgiFramework(), null, false,
+ newModuleList(null, SimpleApplication.class, guiceModules));
+ }
+
+ /**
+ * <p>Creates a new TestDriver with a predefined {@link Application} implementation, but without OSGi support. The
+ * injected Application class implements nothing but the bare minimum to conform to the Application interface.</p>
+ *
+ * @param guiceModules The Guice {@link Module Modules} to install prior to startup.
+ * @return The created TestDriver.
+ * @see #newSimpleApplicationInstance(Module...)
+ * @see #newNonWorkingOsgiFramework()
+ */
+ public static TestDriver newSimpleApplicationInstanceWithoutOsgi(Module... guiceModules) {
+ return newInstance(newNonWorkingOsgiFramework(), null, false,
+ newModuleList(null, SimpleApplication.class, guiceModules));
+ }
+
+ /**
+ * <p>Creates a new TestDriver from an application bundle. This runs the same code path as the actual jDISC startup
+ * code. Note that the named bundle must have a "X-JDisc-Application" bundle instruction, or setup will fail.</p>
+ *
+ * @param bundleLocation The location of the application bundle to load.
+ * @param privileged Whether or not privileges should be marked as available to the application bundle.
+ * @param guiceModules The Guice {@link Module Modules} to install prior to startup.
+ * @return The created TestDriver.
+ */
+ public static TestDriver newApplicationBundleInstance(String bundleLocation, boolean privileged,
+ Module... guiceModules) {
+ return newInstance(newOsgiFramework(), bundleLocation, privileged, Arrays.asList(guiceModules));
+ }
+
+ /**
+ * <p>Creates a new TestDriver with the given parameters. This is the factory method that all other factory methods
+ * call. It allows you to specify all parts of the TestDriver manually.</p>
+ *
+ * @param osgiFramework The {@link OsgiFramework} to assign to the created TestDriver.
+ * @param bundleLocation The location of the application bundle to load, may be null.
+ * @param privileged Whether or not privileges should be marked as available to the application bundle.
+ * @param guiceModules The Guice {@link Module Modules} to install prior to startup.
+ * @return The created TestDriver.
+ */
+ public static TestDriver newInstance(OsgiFramework osgiFramework, String bundleLocation, boolean privileged,
+ Module... guiceModules) {
+ return newInstance(osgiFramework, bundleLocation, privileged, Arrays.asList(guiceModules));
+ }
+
+ /**
+ * <p>Factory method to create a working {@link OsgiFramework}. This method is used by all {@link TestDriver}
+ * factories that DO NOT have the "WithoutOsgi" suffix.</p>
+ *
+ * @return A working OsgiFramework.
+ */
+ public static FelixFramework newOsgiFramework() {
+ return new FelixFramework(new FelixParams().setCachePath("target/bundlecache" + testId.getAndIncrement()));
+ }
+
+ /**
+ * <p>Factory method to create a light-weight {@link OsgiFramework} that throws {@link
+ * UnsupportedOperationException} if {@link OsgiFramework#installBundle(String)} or {@link
+ * OsgiFramework#startBundles(List, boolean)} is called. This allows for unit testing without the footprint of OSGi
+ * support. This method is used by {@link TestDriver} factories that have the "WithoutOsgi" suffix.</p>
+ *
+ * @return A non-working OsgiFramework.
+ */
+ public static OsgiFramework newNonWorkingOsgiFramework() {
+ return new NonWorkingOsgiFramework();
+ }
+
+ private class CloseTask implements Callable<Boolean> {
+
+ @Override
+ public Boolean call() throws Exception {
+ loader.stop();
+ loader.destroy();
+ return true;
+ }
+ }
+
+ private static TestDriver newInstance(OsgiFramework osgiFramework, String bundleLocation, boolean privileged,
+ Iterable<? extends Module> guiceModules) {
+ ApplicationLoader loader = new ApplicationLoader(osgiFramework, guiceModules);
+ try {
+ loader.init(bundleLocation, privileged);
+ } catch (Exception e) {
+ throw e instanceof RuntimeException ? (RuntimeException)e : new RuntimeException(e);
+ }
+ try {
+ loader.start();
+ } catch (Exception e) {
+ loader.destroy();
+ throw e instanceof RuntimeException ? (RuntimeException)e : new RuntimeException(e);
+ }
+ return new TestDriver(loader);
+ }
+
+ private static List<Module> newModuleList(final Application app, final Class<? extends Application> appClass,
+ Module... guiceModules) {
+ List<Module> lst = new LinkedList<>();
+ lst.addAll(Arrays.asList(guiceModules));
+ lst.add(new AbstractModule() {
+
+ @Override
+ public void configure() {
+ AnnotatedBindingBuilder<Application> builder = bind(Application.class);
+ if (app != null) {
+ builder.toInstance(app);
+ } else {
+ builder.to(appClass);
+ }
+ }
+ });
+ return lst;
+ }
+
+ private static class SimpleApplication implements Application {
+
+ @Override
+ public void start() {
+
+ }
+
+ @Override
+ public void stop() {
+
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+ }
+}
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/package-info.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/package-info.java
new file mode 100644
index 00000000000..c84cb01e47d
--- /dev/null
+++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/package-info.java
@@ -0,0 +1,8 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * <p>Provides classes and interfaces for implementing unit tests of jDISC components.</p>
+ *
+ * @see com.yahoo.jdisc.test.TestDriver
+ */
+@com.yahoo.api.annotations.PublicApi
+package com.yahoo.jdisc.test;