summaryrefslogtreecommitdiffstats
path: root/jdisc_core/src
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
Publish
Diffstat (limited to 'jdisc_core/src')
-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
-rwxr-xr-xjdisc_core/src/main/perl/jdisc_logfmt324
-rw-r--r--jdisc_core/src/main/perl/jdisc_logfmt.1214
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/AbstractResourceTestCase.java133
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/ContainerTestCase.java49
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/HeaderFieldsTestCase.java372
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/ProxyRequestHandlerTestCase.java583
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/ReferencedResourceTestCase.java34
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/ReferencesTestCase.java27
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/RequestTestCase.java381
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/ResponseTestCase.java86
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/application/AbstractApplicationTestCase.java98
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/application/ApplicationNotReadyTestCase.java53
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingMatchTestCase.java52
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingRepositoryTestCase.java181
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingSetTestCase.java506
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/application/BundleInstallationExceptionTestCase.java53
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/application/ContainerBuilderTestCase.java116
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/application/ContainerThreadTestCase.java61
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/application/GlobPatternTestCase.java157
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/application/GuiceRepositoryTestCase.java197
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/application/MetricImplTestCase.java150
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/application/OsgiHeaderTestCase.java20
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/application/OsgiRepositoryTestCase.java18
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/application/ResourcePoolTestCase.java168
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/application/ServerRepositoryTestCase.java88
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/application/UriPatternTestCase.java342
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/BindingMatchingTestCase.java126
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/LatencyTestCase.java264
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/ThroughputTestCase.java180
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/UriMatchingTestCase.java81
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/client/AbstractClientApplicationTestCase.java138
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/client/ClientDriverTestCase.java78
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/ActiveContainerFinalizerTest.java75
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/ActiveContainerTestCase.java160
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationConfigModuleTestCase.java114
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationEnvironmentModuleTestCase.java57
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationLoaderTestCase.java259
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationRestartTestCase.java153
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationShutdownTestCase.java122
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/BootstrapDaemonTestCase.java154
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/BundleLocationResolverTestCase.java87
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogFormatterTestCase.java270
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogListenerTestCase.java115
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogManagerTestCase.java90
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerResourceTestCase.java162
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerShutdownTestCase.java848
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerSnapshotTestCase.java211
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerTerminationTestCase.java76
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/DefaultBindingSelectorTestCase.java38
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/ExportPackagesTestCase.java30
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/FelixFrameworkTestCase.java41
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/FelixParamsTestCase.java67
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogHandlerTestCase.java192
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogManagerTestCase.java184
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogServiceTestCase.java105
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/ScheduledQueueTestCase.java149
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/SystemTimerTestCase.java31
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/TimeoutManagerImplTestCase.java579
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/AbstractContentOutputStreamTestCase.java127
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/AbstractRequestHandlerTestCase.java187
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/BindingNotFoundTestCase.java49
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/BlockingContentWriterTestCase.java210
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/BufferedContentChannelTestCase.java257
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/CallableRequestDispatchTestCase.java51
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/CallableResponseDispatchTestCase.java30
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/ContentInputStreamTestCase.java32
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/FastContentOutputStreamTestCase.java70
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/FastContentWriterTestCase.java241
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureCompletionTestCase.java106
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureConjunctionTestCase.java255
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureResponseTestCase.java81
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/NullContentTestCase.java48
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/ReadableContentChannelTestCase.java320
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/RequestDeniedTestCase.java70
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/RequestDispatchTestCase.java253
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/ResponseDispatchTestCase.java206
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/RunnableLatch.java22
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/ThreadedRequestHandlerTestCase.java228
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/handler/UnsafeContentInputStreamTestCase.java139
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/service/AbstractClientProviderTestCase.java34
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/service/AbstractServerProviderTestCase.java51
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/service/BindingSetNotFoundTestCase.java58
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/service/ConnectToHandlerTestCase.java99
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/service/ContainerNotReadyTestCase.java28
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/service/CurrentContainerTestCase.java27
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/service/NoBindingSetSelectedTestCase.java55
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingClientTestCase.java54
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingCompletionHandlerTestCase.java43
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingContentChannelTestCase.java80
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingOsgiFrameworkTestCase.java72
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingRequestHandlerTestCase.java42
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingRequestTestCase.java35
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingResponseHandlerTestCase.java25
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingServerTestCase.java35
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/test/ServerProviderConformanceTestTest.java657
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/test/TestDriverTestCase.java163
-rw-r--r--jdisc_core/src/test/perl/help.Levent.expected20
-rw-r--r--jdisc_core/src/test/perl/help.expected20
-rw-r--r--jdisc_core/src/test/perl/jdisc.expected17
-rw-r--r--jdisc_core/src/test/perl/jdisc.lall.expected19
-rw-r--r--jdisc_core/src/test/perl/jdisc.lall_info.expected4
-rw-r--r--jdisc_core/src/test/perl/jdisc.log19
-rw-r--r--jdisc_core/src/test/perl/jdisc.spid.expected17
-rwxr-xr-xjdisc_core/src/test/perl/jdisc_logfmt_test.sh35
-rw-r--r--jdisc_core/src/test/perl/vespa.Levent.expected9
-rw-r--r--jdisc_core/src/test/perl/vespa.Levent.lall.expected19
-rw-r--r--jdisc_core/src/test/perl/vespa.expected18
-rw-r--r--jdisc_core/src/test/perl/vespa.log19
220 files changed, 27014 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;
diff --git a/jdisc_core/src/main/perl/jdisc_logfmt b/jdisc_core/src/main/perl/jdisc_logfmt
new file mode 100755
index 00000000000..1a05e229832
--- /dev/null
+++ b/jdisc_core/src/main/perl/jdisc_logfmt
@@ -0,0 +1,324 @@
+#!/usr/local/bin/perl
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+# BEGIN perl environment bootstrap section
+# Do not edit between here and END as this section should stay identical in all scripts
+
+use File::Basename;
+use File::Path;
+
+sub findpath {
+ my $myfullname = ${0};
+ my($myname, $mypath) = fileparse($myfullname);
+
+ return $mypath if ( $mypath && -d $mypath );
+ $mypath=`pwd`;
+
+ my $pwdfullname = $mypath . "/" . $myname;
+ return $mypath if ( -f $pwdfullname );
+ return 0;
+}
+
+# Returns the argument path if it seems to point to VESPA_HOME, 0 otherwise
+sub is_vespa_home {
+ my($VESPA_HOME) = shift;
+ my $COMMON_ENV="libexec/vespa/common-env.sh";
+ if ( $VESPA_HOME && -d $VESPA_HOME ) {
+ my $common_env = $VESPA_HOME . "/" . $COMMON_ENV;
+ return $VESPA_HOME if -f $common_env;
+ }
+ return 0;
+}
+
+# Returns the home of Vespa, or dies if it cannot
+sub findhome {
+ # Try the VESPA_HOME env variable
+ return $ENV{'VESPA_HOME'} if is_vespa_home($ENV{'VESPA_HOME'});
+ if ( $ENV{'VESPA_HOME'} ) { # was set, but not correctly
+ die "FATAL: bad VESPA_HOME value '" . $ENV{'VESPA_HOME'} . "'\n";
+ }
+
+ # Try the ROOT env variable
+ $ROOT = $ENV{'ROOT'};
+ return $ROOT if is_vespa_home($ROOT);
+
+ # Try the script location or current dir
+ my $mypath = findpath();
+ if ($mypath) {
+ while ( $mypath =~ s|/[^/]*$|| ) {
+ return $mypath if is_vespa_home($mypath);
+ }
+ }
+ die "FATAL: Missing VESPA_HOME environment variable\n";
+}
+
+BEGIN {
+ my $tmp = findhome();
+ if ( $tmp !~ m{[/]$} ) { $tmp .= "/"; }
+ $ENV{'VESPA_HOME'} = $tmp;
+}
+my $VESPA_HOME = $ENV{'VESPA_HOME'};
+
+# END perl environment bootstrap section
+
+use 5.006_001;
+use strict;
+use warnings;
+
+use File::Basename;
+use Getopt::Long qw(:config no_ignore_case);
+
+my %showflags = (
+ time => 1,
+ fmttime => 1,
+ msecs => 1,
+ usecs => 0,
+ host => 0,
+ level => 1,
+ pid => 0,
+ service => 1,
+ component => 1,
+ message => 1
+ );
+
+my %levelflags = (
+ error => 1,
+ warning => 1,
+ info => 1,
+ debug => 0,
+ unknown => 0
+ );
+
+# Do not buffer the output
+$| = 1;
+
+my $compore;
+my $msgtxre;
+my $onlypid;
+my $onlysvc;
+my $onlyhst;
+
+my $shortsvc;
+my $shortcmp;
+
+my @optlevels;
+my @optaddlevels;
+my @optshow;
+my $optaddlevels;
+my $optlevels;
+my $optfollow;
+my $optnldequote;
+my $opthelp = '';
+
+my $bad = 0;
+
+GetOptions ('level|l=s' => \@optlevels,
+ 'add-level|L=s' => \@optaddlevels,
+ 'service|S=s' => \$onlysvc,
+ 'show|s=s' => \@optshow,
+ 'pid|p=s' => \$onlypid,
+ 'component|c=s' => \$compore,
+ 'message|m=s' => \$msgtxre,
+ 'help|h' => \$opthelp,
+ 'follow|f' => \$optfollow,
+ 'nldequote|N' => \$optnldequote,
+ 'host|H=s' => \$onlyhst,
+ 'truncateservice|ts' => \$shortsvc,
+ 'truncatecomponent|tc|t' => \$shortcmp,
+ ) or $bad=1;
+
+if ( @ARGV == 0 and ! -p STDIN) {
+ push(@ARGV, "$VESPA_HOME/logs/jdisc_core/jdisc_core.log");
+}
+
+if ( $optfollow ) {
+ my $filearg = "";
+ if ( @ARGV > 1 ) {
+ print STDERR "ERROR: Cannot follow more than one file\n\n";
+ $bad=1;
+ } else {
+ $filearg = shift @ARGV if (@ARGV > 0);
+ open(STDIN, "tail -F $filearg |")
+ or die "cannot open 'tail -F $filearg' as input pipe\n";
+ }
+}
+
+$optaddlevels = join(",", @optaddlevels );
+if ( $optaddlevels ) {
+ my @l = split(/,/, $optaddlevels);
+ my $l;
+ foreach $l ( @l ) {
+ $levelflags{$l} = 0;
+ }
+}
+
+if ( $opthelp || $bad ) {
+ print STDERR "Usage: ", basename($0), " [options] [inputfile ...]\n",
+ "Options:\n",
+ " -l LEVELLIST\t--level=LEVELLIST\tselect levels to include\n",
+ " -L LEVELLIST\t--add-level=LEVELLIST\tdefine extra levels\n",
+ " -s FIELDLIST\t--show=FIELDLIST\tselect fields to print\n",
+ " -p PID\t--pid=PID\t\tselect messages from given PID\n",
+ " -S SERVICE\t--service=SERVICE\tselect messages from given SERVICE\n",
+ " -H HOST\t--host=HOST\t\tselect messages from given HOST\n",
+ " -c REGEX\t--component=REGEX\tselect components matching REGEX\n",
+ " -m REGEX\t--message=REGEX\t\tselect message text matching REGEX\n",
+ " -f\t\t--follow\t\tinvoke tail -F to follow input file\n",
+ " -N\t\t--nldequote\t\tdequote newlines in message text field\n",
+ " -t\t--tc\t--truncatecomponent\tchop component to 15 chars\n",
+ " \t--ts\t--truncateservice\tchop service to 9 chars\n",
+ "\n",
+ "FIELDLIST is comma separated, available fields:\n",
+ "\t time fmttime msecs usecs host level pid service component message\n",
+ "Available levels for LEVELLIST:\n",
+ "\t ", join(" ", sort keys(%levelflags)), "\n",
+ "for both lists, use 'all' for all possible values, and -xxx to disable xxx.\n";
+ exit $bad;
+}
+
+$optlevels = join(",", @optlevels );
+if ( $optlevels ) {
+ my $k;
+ unless ( $optlevels =~ s/^\+// or $optlevels =~ m/^-/ ) {
+ $levelflags{$_} = 0 foreach ( keys %levelflags );
+ }
+ my @l = split(/,|(?=-)/, $optlevels);
+ my $l;
+ foreach $l ( @l ) {
+ my $v = 1;
+ my $minus = "";
+ if ( $l =~ s/^-// ) { $v = 0; $minus = "-"; }
+ if ( $l eq "all" ) {
+ foreach $k ( keys %levelflags ) {
+ $levelflags{$k} = $v;
+ }
+ } elsif ( defined $levelflags{$l} ) {
+ $levelflags{$l} = $v;
+ } else {
+ print STDERR "bad level option '$minus$l'\n";
+ exit 1;
+ }
+ }
+}
+
+my $optshow;
+$optshow = join(",", @optshow );
+if ( $optshow ) {
+ my $k;
+ unless ( $optshow =~ s/^\+// or $optshow =~ m/^-/ ) {
+ $showflags{$_} = 0 foreach ( keys %showflags );
+ }
+ my @l = split(/,|(?=-)/, $optshow);
+ my $l;
+ foreach $l ( @l ) {
+ my $v = 1;
+ my $minus = "";
+ if ( $l =~ s/^-// ) { $v = 0; $minus = "-"; }
+ if ( $l eq "all" ) {
+ foreach $k ( keys %showflags ) {
+ $showflags{$k} = $v;
+ }
+ } elsif ( defined $showflags{$l} ) {
+ $showflags{$l} = $v;
+ } else {
+ print STDERR "bad show option '$minus$l'\n";
+ exit 1;
+ }
+ }
+}
+
+while (<>) {
+ chomp;
+ if ( /^
+ (\d+)\.?(\d*) # seconds, optional fractional seconds
+ \t
+ ([^\t]*) # host
+ \t
+ (\d+\/?\d*|\-\/\d+) # pid, optional tid
+ \t
+ ([^\t]*) # servicename
+ \t
+ ([^\t]*) # componentname
+ \t
+ (\w+) # level
+ \t
+ (.*) # message text
+ $/x )
+ {
+ my $secs = $1;
+ my $usec = $2 . "000000"; # make sure we have atleast 6 digits
+ my $host = $3;
+ my $pidn = $4;
+ my $svcn = $5;
+ my $comp = $6;
+ my $levl = $7;
+ my $msgt = $8;
+
+ if ( ! defined $levelflags{$levl} ) {
+ print STDERR "Warning: unknown level '$levl' in input\n";
+ $levelflags{$levl} = 1;
+ }
+ next unless ( $levelflags{$levl} );
+
+ if ($compore && $comp !~ m/$compore/o) { next; }
+ if ($msgtxre && $msgt !~ m/$msgtxre/o) { next; }
+ if ($onlypid && $pidn ne $onlypid) { next; }
+ if ($onlysvc && $svcn ne $onlysvc) { next; }
+ if ($onlyhst && $host ne $onlyhst) { next; }
+
+ $levl = "\U$levl";
+
+ my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday);
+ ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday)=localtime($secs);
+ my $datestr = sprintf("%04d-%02d-%02d",
+ 1900+$year, 1+$mon, $mday);
+ my $timestr = sprintf("%02d:%02d:%02d",
+ $hour, $min, $sec);
+
+ if ( $showflags{"time"} || $showflags{"fmttime"} ) {
+ if ($showflags{"fmttime"} ) {
+ print "[$datestr $timestr";
+ if ( $showflags{"usecs"} ) {
+ printf ".%.6s", $usec;
+ } elsif ( $showflags{"msecs"} ) {
+ printf ".%.3s", $usec;
+ }
+ print "] ";
+ } else {
+ printf "%s.%.6s ", $secs, $usec;
+ }
+ }
+ if ( $showflags{"host"} ) {
+ printf "%-8s ", $host;
+ }
+ if ( $showflags{"level"} ) {
+ printf "%-7s : ", $levl;
+ }
+ if ( $showflags{"pid"} ) {
+ printf "%5s ", $pidn;
+ }
+ if ( $showflags{"service"} ) {
+ if ( $shortsvc ) {
+ printf "%-9.9s ", $svcn;
+ } else {
+ printf "%-16s ", $svcn;
+ }
+ }
+ if ( $showflags{"component"} ) {
+ if ( $shortcmp ) {
+ printf "%-15.15s ", $comp;
+ } else {
+ printf "%s\t", $comp;
+ }
+ }
+ if ( $showflags{"message"} ) {
+ if ( $optnldequote ) {
+ $msgt = "\n\t${msgt}" if ( $msgt =~ s/\\n/\n\t/g );
+ }
+ print $msgt;
+ }
+ print "\n";
+ } else {
+ print STDERR "bad log line: '$_'\n";
+ }
+}
diff --git a/jdisc_core/src/main/perl/jdisc_logfmt.1 b/jdisc_core/src/main/perl/jdisc_logfmt.1
new file mode 100644
index 00000000000..0a884c0ec85
--- /dev/null
+++ b/jdisc_core/src/main/perl/jdisc_logfmt.1
@@ -0,0 +1,214 @@
+.\" $Id: logfmt.1,v 1.12 2007-06-19 09:37:25 daljord Exp $
+.\"
+.Dd October 29, 2004
+.Dt LOGFMT \&1 "JDisc documentation"
+.Os "Yahoo! JDisc" "2.3"
+.Os
+.Sh NAME
+.Nm logfmt
+.Nd select and format messages from JDisc log files
+.Sh SYNOPSIS
+.Nm
+.Op Fl L Ar levellist
+.Op Fl l Ar levellist
+.Op Fl s Ar fieldlist
+.Op Fl p Ar pid
+.Op Fl S Ar service
+.Op Fl H Ar host
+.Op Fl c Ar regex
+.Op Fl m Ar regex
+.Op Fl t
+.Op Fl f
+.Op Fl N
+.Op Fl ts
+.Op Ar
+.Sh DESCRIPTION
+The
+.Nm
+utility reads JDisc log files, select messages and writes a formatted
+version of selected messages to the standard output.
+.Pp
+The options are as follows:
+.Bl -tag -width ".It Fl l Ar levellist"
+.It Fl L Ar levellist
+Declares additional log levels that should be treated as known. These
+levels are suppressed unless also given as argument to option -l.
+.Ar levellist
+is a comma separated list of level names.
+.It Fl l Ar levellist
+Select which log levels to select.
+The default is to select "error", "warning" and "info" levels, and
+suppress "debug" and "unknown" levels; but when using this option, only
+the named levels will be selected.
+The
+.Ar levellist
+is a comma separated list of level names.
+The name
+.Em all
+may be used to add all known levels.
+Prepending a minus sign will deselect the level named.
+Starting the list with a plus sign will add and remove levels
+from the current (or default) list of levels instead
+of replacing it.
+.It Fl s Ar fieldlist
+Select which fields of log messages to show.
+The order of the actual output fields is fixed.
+When using this option, only the named fields will be shown. The
+fieldlist is a comma separated list of field names. The name
+.Em all
+may be used to add all possible fields.
+Prepending a minus sign will turn off display of the named field.
+Starting the list with a plus sign will add and remove fields
+from the current (or default) list of fields instead
+of replacing it.
+.Pp
+The fields which may be named are:
+.Bl -tag -width component
+.It time
+Print the time in seconds since the epoch.
+Ignored if
+.Em fmttime
+is shown.
+.It fmttime
+Print the time in human-readable [YYYY-MM-DD HH:mm:ss] format.
+Note that the time is printed in the local timezone; to get GMT
+output use
+.Nm "\*[q]env TZ=GMT logfmt\*[q]"
+as your command.
+.It msecs
+Add milliseconds after the seconds in
+.Em time
+and
+.Em fmttime
+output. Ignored if
+.Em usecs
+is in effect.
+.It usecs
+Add microseconds after the seconds in
+.Em time
+and
+.Em fmttime
+output.
+.It host
+Print the hostname field.
+.It level
+Print the level field (uppercased).
+.It pid
+Print the pid field.
+.It service
+Print the service field.
+.It component
+Print the component field.
+.It message
+Print the message text field.
+You probably always want to add this.
+.El
+.Pp
+Using this option several times works as if the given
+.Ar fieldlist
+arguments had been concatenated into one comma-separated list.
+The default fields to show are as if
+.Bk
+.Op Fl s Ar fmttime,msecs,level,service,component,message
+.Ek
+had been given.
+.It Fl p Ar pid
+Select only messages where the pid field matches the
+.Ar pid
+string exactly.
+.It Fl S Ar service
+Select only messages where the service field matches the
+.Ar service
+string exactly.
+.It Fl H Ar host
+Select only messages where the hostname field matches the
+.Ar host
+string exactly.
+.It Fl c Ar regex
+Select only messages where the component field matches the
+.Ar regex
+given, using
+.Xr perlre
+regular expression matching.
+.It Fl m Ar regex
+Select only messages where the message text field matches the
+.Ar regex
+given, using
+.Xr perlre
+regular expression matching.
+.It Fl f
+Invoke tail -F to follow input file
+.It Fl N
+Dequote quoted newlines in the message text field to an actual newline plus tab.
+.It Fl t
+Format the component field (if shown) as a fixed-with string,
+truncating if necessary.
+.It Fl ts
+Format the service field (if shown) as a fixed-with string,
+truncating if necessary.
+.El
+.Sh EXAMPLES
+The command:
+.Pp
+.Bd -literal -offset indent
+logfmt -l event -s service,message,fmttime,message
+.Ed
+.Pp
+will display only messages with log level "event",
+printing a human-readable time (without any fractional seconds),
+the service generating the event and the event message, like this:
+.Bd -literal -offset indent
+[2004-12-07 18:43:01] config-sentinel starting/1 name="logd"
+[2004-12-07 18:43:01] logd started/1 name="logdemon"
+[2004-12-07 18:45:51] rtc starting/1 name="rtc.index0"
+[2004-12-07 18:45:51] rtc.index0 started/1 name="flexindexer.index"
+[2004-12-07 18:45:51] rtc.index0 stopping/1 name="flexindexer.index" why="done"
+[2004-12-07 18:45:53] rtc stopped/1 name="rtc.index0" pid=50600 exitcode=0
+[2004-12-07 18:46:13] logd stopping/1 name="logdemon" why="done ok."
+[2004-12-07 18:46:13] config-sentinel stopped/1 name="logd" pid=49633 exitcode=0
+.Ed
+.Pp
+Note that the second "message" item in the fieldlist is redundant,
+and that order of printed field is fixed no matter what the fieldlist
+order is.
+.Pp
+The command:
+.Pp
+.Bd -literal -offset indent
+logfmt -l all-info,-debug -s level \e
+ -s time,usecs,component,message -t -l -event
+.Ed
+.Pp
+will display messages with log levels that are
+.Em not
+any of
+.Em info, debug,
+or
+.Em event,
+printing the time in seconds and microseconds, the log level, the
+component name, and the message text, possibly somewhat like this:
+.Bd -literal -offset indent
+1102441382.530423 CONFIG : nc Config handle: 'pandora.0-rtx'
+1102441551.471568 CONFIG : flexindexer.doc Adding document type typetest-0
+1102441573.148211 WARNING : logdemon stopping on signal 15
+1102441887.158000 WARNING : com.yahoo.fs4.m read exception
+1102441935.569567 WARNING : rtc Dispatch inherited job failed for dir dispatch0
+1102442115.746001 WARNING : fdispatch Search node 172.24.94.75:10124 down
+1102442474.205920 WARNING : rtx RTC (tcp/172.24.94.75:10161) : DOWN
+1102442474.515877 WARNING : fdispatch Search node localhost:10128 down
+1102442983.075669 ERROR : flexindexer.std Unable to find cluster map defaultcluster
+.Ed
+.Sh FILES
+If no file argument is given,
+.Nm
+will read the last JDisc log file $VESPA_HOME/logs/jdisc_core/jdisc_core.log (this also works with the
+.Fl f
+option).
+Otherwise, reads only the files given as arguments.
+To read standard input, supply a single dash '-' as a file argument.
+.Sh SEE ALSO
+Documentation in the "log" module for input file format.
+.Sh HISTORY
+Developed as part of Vespa 1.1, later moved to JDisc 2.3. The default output
+format reflects the old "fastlib" log formatting, with minor differences
+and is intended to be human-readable, not parsed.
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/AbstractResourceTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/AbstractResourceTestCase.java
new file mode 100644
index 00000000000..d43771c9cfd
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/AbstractResourceTestCase.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;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class AbstractResourceTestCase {
+
+ @Test
+ public void requireThatDestroyIsCalledWhenReleased() {
+ MyResource res = new MyResource();
+ assertFalse(res.destroyed);
+ res.release();
+ assertTrue(res.destroyed);
+ }
+
+ @Test
+ public void requireThatDestroyIsCalledWhenRetainCountReachesZero() {
+ MyResource res = new MyResource();
+ assertEquals(1, res.retainCount());
+ assertFalse(res.destroyed);
+ final ResourceReference reference = res.refer();
+ assertEquals(2, res.retainCount());
+ res.release();
+ assertEquals(1, res.retainCount());
+ assertFalse(res.destroyed);
+ reference.close();
+ assertEquals(0, res.retainCount());
+ assertTrue(res.destroyed);
+ }
+
+ @Test
+ public void requireThatDestroyIsCalledWhenRetainCountReachesZeroOppositeOrder() {
+ MyResource res = new MyResource();
+ assertEquals(1, res.retainCount());
+ assertFalse(res.destroyed);
+ final ResourceReference reference = res.refer();
+ assertEquals(2, res.retainCount());
+ reference.close();
+ assertEquals(1, res.retainCount());
+ assertFalse(res.destroyed);
+ res.release();
+ assertEquals(0, res.retainCount());
+ assertTrue(res.destroyed);
+ }
+
+ @Test
+ public void requireThatReleaseCanOnlyBeCalledOnceEvenWhenReferenceCountIsPositive() {
+ MyResource res = new MyResource();
+ final ResourceReference secondReference = res.refer();
+ res.release();
+ try {
+ res.release();
+ fail();
+ } catch (IllegalStateException e) {
+ // As expected.
+ }
+ secondReference.close();
+ }
+
+ @Test
+ public void requireThatSecondaryReferenceCanOnlyBeClosedOnceEvenWhenReferenceCountIsPositive() {
+ MyResource res = new MyResource();
+ final ResourceReference secondReference = res.refer();
+ secondReference.close();
+ try {
+ secondReference.close();
+ fail();
+ } catch (IllegalStateException e) {
+ // As expected.
+ }
+ res.release();
+ }
+
+ @Test
+ public void requireThatReleaseAfterDestroyThrows() {
+ MyResource res = new MyResource();
+ res.release();
+ assertTrue(res.destroyed);
+ try {
+ res.release();
+ fail();
+ } catch (IllegalStateException e) {
+
+ }
+ assertEquals(0, res.retainCount());
+ try {
+ res.release();
+ fail();
+ } catch (IllegalStateException e) {
+
+ }
+ assertEquals(0, res.retainCount());
+ }
+
+ @Test
+ public void requireThatReferAfterDestroyThrows() {
+ MyResource res = new MyResource();
+ res.release();
+ assertTrue(res.destroyed);
+ try {
+ res.refer();
+ fail();
+ } catch (IllegalStateException e) {
+
+ }
+ assertEquals(0, res.retainCount());
+ try {
+ res.refer();
+ fail();
+ } catch (IllegalStateException e) {
+
+ }
+ assertEquals(0, res.retainCount());
+ }
+
+ private static class MyResource extends AbstractResource {
+
+ boolean destroyed = false;
+
+ @Override
+ protected void destroy() {
+ destroyed = true;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/ContainerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/ContainerTestCase.java
new file mode 100644
index 00000000000..9eef00b2cbe
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/ContainerTestCase.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;
+
+import com.google.inject.AbstractModule;
+import com.yahoo.jdisc.service.BindingSetNotFoundException;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ContainerTestCase {
+
+ @Test
+ public void requireThatNewRequestsReferenceSameSnapshot() throws Exception {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ driver.activateContainer(driver.newContainerBuilder());
+ Request foo = new Request(driver, URI.create("http://foo"));
+ Request bar = new Request(foo, URI.create("http://bar"));
+ assertNotNull(foo.container());
+ assertSame(foo.container(), bar.container());
+ foo.release();
+ bar.release();
+ driver.close();
+ }
+
+ @Test
+ public void requireThatInjectionWorks() throws BindingSetNotFoundException {
+ final Object foo = new Object();
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(Object.class).toInstance(foo);
+ }
+ });
+ driver.activateContainer(driver.newContainerBuilder());
+ Request request = new Request(driver, URI.create("http://host/path"));
+ assertSame(foo, request.container().getInstance(Object.class));
+ request.release();
+ driver.close();
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/HeaderFieldsTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/HeaderFieldsTestCase.java
new file mode 100644
index 00000000000..2d0ada8113c
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/HeaderFieldsTestCase.java
@@ -0,0 +1,372 @@
+// 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 org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class HeaderFieldsTestCase {
+
+ @Test
+ public void requireThatSizeWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ assertEquals(0, headers.size());
+ headers.add("foo", "bar");
+ assertEquals(1, headers.size());
+ headers.add("foo", "baz");
+ assertEquals(1, headers.size());
+ headers.add("bar", "baz");
+ assertEquals(2, headers.size());
+ headers.remove("foo");
+ assertEquals(1, headers.size());
+ headers.remove("bar");
+ assertEquals(0, headers.size());
+ }
+
+ @Test
+ public void requireThatIsEmptyWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ assertTrue(headers.isEmpty());
+ headers.add("foo", "bar");
+ assertFalse(headers.isEmpty());
+ headers.remove("foo");
+ assertTrue(headers.isEmpty());
+ }
+
+ @Test
+ public void requireThatContainsKeyWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ assertFalse(headers.containsKey("foo"));
+ assertFalse(headers.containsKey("FOO"));
+ headers.add("foo", "bar");
+ assertTrue(headers.containsKey("foo"));
+ assertTrue(headers.containsKey("FOO"));
+ }
+
+ @Test
+ public void requireThatContainsValueWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ assertFalse(headers.containsValue(Arrays.asList("bar")));
+ headers.add("foo", "bar");
+ assertTrue(headers.containsValue(Arrays.asList("bar")));
+ }
+
+ @Test
+ public void requireThatContainsWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ assertFalse(headers.contains("foo", "bar"));
+ assertFalse(headers.contains("FOO", "bar"));
+ assertFalse(headers.contains("foo", "BAR"));
+ assertFalse(headers.contains("FOO", "BAR"));
+ headers.add("foo", "bar");
+ assertTrue(headers.contains("foo", "bar"));
+ assertTrue(headers.contains("FOO", "bar"));
+ assertFalse(headers.contains("foo", "BAR"));
+ assertFalse(headers.contains("FOO", "BAR"));
+ }
+
+ @Test
+ public void requireThatContainsIgnoreCaseWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ assertFalse(headers.containsIgnoreCase("foo", "bar"));
+ assertFalse(headers.containsIgnoreCase("FOO", "bar"));
+ assertFalse(headers.containsIgnoreCase("foo", "BAR"));
+ assertFalse(headers.containsIgnoreCase("FOO", "BAR"));
+ headers.add("foo", "bar");
+ assertTrue(headers.containsIgnoreCase("foo", "bar"));
+ assertTrue(headers.containsIgnoreCase("FOO", "bar"));
+ assertTrue(headers.containsIgnoreCase("foo", "BAR"));
+ assertTrue(headers.containsIgnoreCase("FOO", "BAR"));
+ }
+
+ @Test
+ public void requireThatAddStringWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ assertNull(headers.get("foo"));
+ headers.add("foo", "bar");
+ assertEquals(Arrays.asList("bar"), headers.get("foo"));
+ headers.add("foo", "baz");
+ assertEquals(Arrays.asList("bar", "baz"), headers.get("foo"));
+ }
+
+ @Test
+ public void requireThatAddListWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ assertNull(headers.get("foo"));
+ headers.add("foo", Arrays.asList("bar"));
+ assertEquals(Arrays.asList("bar"), headers.get("foo"));
+ headers.add("foo", Arrays.asList("baz", "cox"));
+ assertEquals(Arrays.asList("bar", "baz", "cox"), headers.get("foo"));
+ }
+
+ @Test
+ public void requireThatAddAllWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ headers.add("foo", "bar");
+ headers.add("bar", "baz");
+ assertEquals(Arrays.asList("bar"), headers.get("foo"));
+ assertEquals(Arrays.asList("baz"), headers.get("bar"));
+
+ Map<String, List<String>> map = new HashMap<>();
+ map.put("foo", Arrays.asList("baz", "cox"));
+ map.put("bar", Arrays.asList("cox"));
+ headers.addAll(map);
+
+ assertEquals(Arrays.asList("bar", "baz", "cox"), headers.get("foo"));
+ assertEquals(Arrays.asList("baz", "cox"), headers.get("bar"));
+ }
+
+ @Test
+ public void requireThatPutStringWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ assertNull(headers.get("foo"));
+ headers.put("foo", "bar");
+ assertEquals(Arrays.asList("bar"), headers.get("foo"));
+ headers.put("foo", "baz");
+ assertEquals(Arrays.asList("baz"), headers.get("foo"));
+ }
+
+ @Test
+ public void requireThatPutListWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ assertNull(headers.get("foo"));
+ headers.put("foo", Arrays.asList("bar"));
+ assertEquals(Arrays.asList("bar"), headers.get("foo"));
+ headers.put("foo", Arrays.asList("baz", "cox"));
+ assertEquals(Arrays.asList("baz", "cox"), headers.get("foo"));
+ }
+
+ @Test
+ public void requireThatPutAllWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ headers.add("foo", "bar");
+ headers.add("bar", "baz");
+ assertEquals(Arrays.asList("bar"), headers.get("foo"));
+ assertEquals(Arrays.asList("baz"), headers.get("bar"));
+
+ Map<String, List<String>> map = new HashMap<>();
+ map.put("foo", Arrays.asList("baz", "cox"));
+ map.put("bar", Arrays.asList("cox"));
+ headers.putAll(map);
+
+ assertEquals(Arrays.asList("baz", "cox"), headers.get("foo"));
+ assertEquals(Arrays.asList("cox"), headers.get("bar"));
+ }
+
+ @Test
+ public void requireThatRemoveWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ headers.put("foo", Arrays.asList("bar", "baz"));
+ assertEquals(Arrays.asList("bar", "baz"), headers.get("foo"));
+ assertEquals(Arrays.asList("bar", "baz"), headers.remove("foo"));
+ assertNull(headers.get("foo"));
+ assertNull(headers.remove("foo"));
+ }
+
+ @Test
+ public void requireThatRemoveStringWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ headers.put("foo", Arrays.asList("bar", "baz"));
+ assertEquals(Arrays.asList("bar", "baz"), headers.get("foo"));
+ assertTrue(headers.remove("foo", "bar"));
+ assertFalse(headers.remove("foo", "cox"));
+ assertEquals(Arrays.asList("baz"), headers.get("foo"));
+ assertTrue(headers.remove("foo", "baz"));
+ assertFalse(headers.remove("foo", "cox"));
+ assertNull(headers.get("foo"));
+ }
+
+ @Test
+ public void requireThatClearWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ headers.add("foo", "bar");
+ headers.add("bar", "baz");
+ assertEquals(Arrays.asList("bar"), headers.get("foo"));
+ assertEquals(Arrays.asList("baz"), headers.get("bar"));
+ headers.clear();
+ assertNull(headers.get("foo"));
+ assertNull(headers.get("bar"));
+ }
+
+ @Test
+ public void requireThatGetWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ assertNull(headers.get("foo"));
+ headers.add("foo", "bar");
+ assertEquals(Arrays.asList("bar"), headers.get("foo"));
+ }
+
+ @Test
+ public void requireThatGetFirstWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ assertNull(headers.getFirst("foo"));
+ headers.add("foo", Arrays.asList("bar", "baz"));
+ assertEquals("bar", headers.getFirst("foo"));
+ }
+
+ @Test
+ public void requireThatIsTrueWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ assertFalse(headers.isTrue("foo"));
+ headers.put("foo", Arrays.asList("true"));
+ assertTrue(headers.isTrue("foo"));
+ headers.put("foo", Arrays.asList("true", "true"));
+ assertTrue(headers.isTrue("foo"));
+ headers.put("foo", Arrays.asList("true", "false"));
+ assertFalse(headers.isTrue("foo"));
+ headers.put("foo", Arrays.asList("false", "true"));
+ assertFalse(headers.isTrue("foo"));
+ headers.put("foo", Arrays.asList("false", "false"));
+ assertFalse(headers.isTrue("foo"));
+ headers.put("foo", Arrays.asList("false"));
+ assertFalse(headers.isTrue("foo"));
+ }
+
+ @Test
+ public void requireThatKeySetWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ assertTrue(headers.keySet().isEmpty());
+ headers.add("foo", "bar");
+ assertEquals(new HashSet<>(Arrays.asList("foo")), headers.keySet());
+ headers.add("bar", "baz");
+ assertEquals(new HashSet<>(Arrays.asList("foo", "bar")), headers.keySet());
+ }
+
+ @Test
+ public void requireThatValuesWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ assertTrue(headers.values().isEmpty());
+ headers.add("foo", "bar");
+ Collection<List<String>> values = headers.values();
+ assertEquals(1, values.size());
+ assertTrue(values.contains(Arrays.asList("bar")));
+
+ headers.add("bar", "baz");
+ values = headers.values();
+ assertEquals(2, values.size());
+ assertTrue(values.contains(Arrays.asList("bar")));
+ assertTrue(values.contains(Arrays.asList("baz")));
+ }
+
+ @Test
+ public void requireThatEntrySetWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ assertTrue(headers.entrySet().isEmpty());
+ headers.put("foo", Arrays.asList("bar", "baz"));
+
+ Set<Map.Entry<String, List<String>>> entries = headers.entrySet();
+ assertEquals(1, entries.size());
+ Map.Entry<String, List<String>> entry = entries.iterator().next();
+ assertNotNull(entry);
+ assertEquals("foo", entry.getKey());
+ assertEquals(Arrays.asList("bar", "baz"), entry.getValue());
+ }
+
+ @Test
+ public void requireThatEntriesWorksAsExpected() {
+ HeaderFields headers = new HeaderFields();
+ assertTrue(headers.entries().isEmpty());
+ headers.put("foo", Arrays.asList("bar", "baz"));
+
+ List<Map.Entry<String, String>> entries = headers.entries();
+ assertEquals(2, entries.size());
+
+ Map.Entry<String, String> entry = entries.get(0);
+ assertNotNull(entry);
+ assertEquals("foo", entry.getKey());
+ assertEquals("bar", entry.getValue());
+
+ assertNotNull(entry = entries.get(1));
+ assertEquals("foo", entry.getKey());
+ assertEquals("baz", entry.getValue());
+ }
+
+ @Test
+ public void requireThatEntryIsUnmodifiable() {
+ HeaderFields headers = new HeaderFields();
+ headers.put("foo", "bar");
+ Map.Entry<String, String> entry = headers.entries().get(0);
+ try {
+ entry.setValue("baz");
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatEntriesAreUnmodifiable() {
+ HeaderFields headers = new HeaderFields();
+ headers.put("foo", "bar");
+ List<Map.Entry<String, String>> entries = headers.entries();
+ try {
+ entries.add(new MyEntry());
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ try {
+ entries.remove(new MyEntry());
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatEqualsWorksAsExpected() {
+ HeaderFields lhs = new HeaderFields();
+ HeaderFields rhs = new HeaderFields();
+ assertTrue(lhs.equals(rhs));
+ lhs.add("foo", "bar");
+ assertFalse(lhs.equals(rhs));
+ rhs.add("foo", "bar");
+ assertTrue(lhs.equals(rhs));
+ }
+
+ @Test
+ public void requireThatHashCodeWorksAsExpected() {
+ HeaderFields lhs = new HeaderFields();
+ HeaderFields rhs = new HeaderFields();
+ assertTrue(lhs.hashCode() == rhs.hashCode());
+ lhs.add("foo", "bar");
+ assertTrue(lhs.hashCode() != rhs.hashCode());
+ rhs.add("foo", "bar");
+ assertTrue(lhs.hashCode() == rhs.hashCode());
+ }
+
+ private static class MyEntry implements Map.Entry<String, String> {
+
+ @Override
+ public String getKey() {
+ return "key";
+ }
+
+ @Override
+ public String getValue() {
+ return "value";
+ }
+
+ @Override
+ public String setValue(String value) {
+ return "value";
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/ProxyRequestHandlerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/ProxyRequestHandlerTestCase.java
new file mode 100644
index 00000000000..54a5a697791
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/ProxyRequestHandlerTestCase.java
@@ -0,0 +1,583 @@
+// 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.ContainerBuilder;
+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 com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Handler;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ProxyRequestHandlerTestCase {
+
+ @Test
+ public void requireThatRequestHandlerIsProxied() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion();
+ Request request = newRequest(driver, requestHandler);
+ RequestHandler resolvedHandler = new ProxyRequestHandler(request.container().resolveHandler(request));
+ MyResponseHandler responseHandler = MyResponseHandler.newEagerCompletion();
+ resolvedHandler.handleRequest(request, responseHandler).close(null);
+ request.release();
+ assertNotNull(requestHandler.handler);
+ resolvedHandler.handleTimeout(request, responseHandler);
+ assertTrue(requestHandler.timeout);
+ requestHandler.respond();
+
+ requestHandler.release();
+ final ResourceReference resolvedHandlerReference = resolvedHandler.refer();
+ assertTrue(driver.close()); // release installed ref
+
+ assertFalse(requestHandler.destroyed);
+ resolvedHandlerReference.close();
+ assertTrue(requestHandler.destroyed);
+ }
+
+ @Test
+ public void requireThatRequestContentCompletedIsProxied() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ MyRequestHandler requestHandler = MyRequestHandler.newInstance();
+ Request request = newRequest(driver, requestHandler);
+ ContentChannel resolvedContent = request.connect(MyResponseHandler.newEagerCompletion());
+ request.release();
+
+ assertSame(request, requestHandler.request);
+
+ ByteBuffer buf = ByteBuffer.allocate(69);
+ resolvedContent.write(buf, null);
+ assertSame(buf, requestHandler.content.writeBuf);
+ requestHandler.content.writeCompletion.completed();
+ MyCompletion writeCompletion = new MyCompletion();
+ resolvedContent.write(buf = ByteBuffer.allocate(69), writeCompletion);
+ assertSame(buf, requestHandler.content.writeBuf);
+ assertFalse(writeCompletion.completed);
+ assertNull(writeCompletion.failed);
+ requestHandler.content.writeCompletion.completed();
+ assertTrue(writeCompletion.completed);
+ assertNull(writeCompletion.failed);
+
+ MyCompletion closeCompletion = new MyCompletion();
+ resolvedContent.close(closeCompletion);
+ assertTrue(requestHandler.content.closed);
+ assertFalse(closeCompletion.completed);
+ assertNull(writeCompletion.failed);
+ requestHandler.content.closeCompletion.completed();
+ assertTrue(closeCompletion.completed);
+ assertNull(closeCompletion.failed);
+
+ requestHandler.respond();
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatRequestContentFailedIsProxied() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ MyRequestHandler requestHandler = MyRequestHandler.newInstance();
+ Request request = newRequest(driver, requestHandler);
+ ContentChannel resolvedContent = request.connect(MyResponseHandler.newEagerCompletion());
+ request.release();
+
+ assertSame(request, requestHandler.request);
+
+ ByteBuffer buf = ByteBuffer.allocate(69);
+ resolvedContent.write(buf, null);
+ assertSame(buf, requestHandler.content.writeBuf);
+ requestHandler.content.writeCompletion.completed();
+ MyCompletion writeCompletion = new MyCompletion();
+ resolvedContent.write(buf = ByteBuffer.allocate(69), writeCompletion);
+ assertSame(buf, requestHandler.content.writeBuf);
+ assertFalse(writeCompletion.completed);
+ assertNull(writeCompletion.failed);
+ MyException writeFailed = new MyException();
+ requestHandler.content.writeCompletion.failed(writeFailed);
+ assertFalse(writeCompletion.completed);
+ assertSame(writeFailed, writeCompletion.failed);
+
+ MyCompletion closeCompletion = new MyCompletion();
+ resolvedContent.close(closeCompletion);
+ assertTrue(requestHandler.content.closed);
+ assertFalse(closeCompletion.completed);
+ assertNull(closeCompletion.failed);
+ MyException closeFailed = new MyException();
+ requestHandler.content.closeCompletion.failed(closeFailed);
+ assertFalse(writeCompletion.completed);
+ assertSame(closeFailed, closeCompletion.failed);
+
+ requestHandler.respond();
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatNullRequestContentIsProxied() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ MyRequestHandler requestHandler = MyRequestHandler.newNullContent();
+ Request request = newRequest(driver, requestHandler);
+ request.connect(MyResponseHandler.newEagerCompletion()).close(null);
+ request.release();
+
+ requestHandler.respond();
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatRequestWriteCompletionCanOnlyBeCalledOnce() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ MyRequestHandler requestHandler = MyRequestHandler.newInstance();
+ Request request = newRequest(driver, requestHandler);
+ ContentChannel resolvedContent = request.connect(MyResponseHandler.newEagerCompletion());
+ request.release();
+
+ CountingCompletionHandler completion = new CountingCompletionHandler();
+ resolvedContent.write(ByteBuffer.allocate(0), completion);
+ assertEquals(0, completion.numCompleted.get());
+ assertEquals(0, completion.numFailed.get());
+ requestHandler.content.writeCompletion.completed();
+ assertEquals(1, completion.numCompleted.get());
+ assertEquals(0, completion.numFailed.get());
+ try {
+ requestHandler.content.writeCompletion.completed();
+ fail();
+ } catch (IllegalStateException e) {
+ // ignore
+ }
+ assertEquals(1, completion.numCompleted.get());
+ assertEquals(0, completion.numFailed.get());
+ try {
+ requestHandler.content.writeCompletion.failed(new Throwable());
+ fail();
+ } catch (IllegalStateException e) {
+ // ignore
+ }
+ assertEquals(1, completion.numCompleted.get());
+ assertEquals(0, completion.numFailed.get());
+
+ resolvedContent.close(null);
+ requestHandler.content.closeCompletion.completed();
+ requestHandler.respond();
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatRequestCloseCompletionCanOnlyBeCalledOnce() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ MyRequestHandler requestHandler = MyRequestHandler.newInstance();
+ Request request = newRequest(driver, requestHandler);
+ ContentChannel resolvedContent = request.connect(MyResponseHandler.newEagerCompletion());
+ request.release();
+
+ CountingCompletionHandler completion = new CountingCompletionHandler();
+ resolvedContent.close(completion);
+ assertEquals(0, completion.numCompleted.get());
+ assertEquals(0, completion.numFailed.get());
+ requestHandler.content.closeCompletion.completed();
+ assertEquals(1, completion.numCompleted.get());
+ assertEquals(0, completion.numFailed.get());
+ try {
+ requestHandler.content.closeCompletion.completed();
+ fail();
+ } catch (IllegalStateException e) {
+ // ignore
+ }
+ assertEquals(1, completion.numCompleted.get());
+ assertEquals(0, completion.numFailed.get());
+ try {
+ requestHandler.content.closeCompletion.failed(new Throwable());
+ fail();
+ } catch (IllegalStateException e) {
+ // ignore
+ }
+ assertEquals(1, completion.numCompleted.get());
+ assertEquals(0, completion.numFailed.get());
+
+ requestHandler.respond();
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatResponseContentCompletedIsProxied() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion();
+ Request request = newRequest(driver, requestHandler);
+ MyResponseHandler responseHandler = MyResponseHandler.newInstance();
+ request.connect(responseHandler).close(null);
+ request.release();
+ Response response = new Response(Response.Status.OK);
+ ContentChannel resolvedContent = requestHandler.handler.handleResponse(response);
+
+ assertSame(response, responseHandler.response);
+
+ ByteBuffer buf = ByteBuffer.allocate(69);
+ resolvedContent.write(buf, null);
+ assertSame(buf, responseHandler.content.writeBuf);
+ responseHandler.content.writeCompletion.completed();
+ MyCompletion writeCompletion = new MyCompletion();
+ resolvedContent.write(buf = ByteBuffer.allocate(69), writeCompletion);
+ assertSame(buf, responseHandler.content.writeBuf);
+ assertFalse(writeCompletion.completed);
+ assertNull(writeCompletion.failed);
+ responseHandler.content.writeCompletion.completed();
+ assertTrue(writeCompletion.completed);
+ assertNull(writeCompletion.failed);
+
+ MyCompletion closeCompletion = new MyCompletion();
+ resolvedContent.close(closeCompletion);
+ assertTrue(responseHandler.content.closed);
+ assertFalse(closeCompletion.completed);
+ assertNull(closeCompletion.failed);
+ responseHandler.content.closeCompletion.completed();
+ assertTrue(closeCompletion.completed);
+ assertNull(closeCompletion.failed);
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatResponseContentFailedIsProxied() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion();
+ Request request = newRequest(driver, requestHandler);
+ MyResponseHandler responseHandler = MyResponseHandler.newInstance();
+ request.connect(responseHandler).close(null);
+ request.release();
+ Response response = new Response(Response.Status.OK);
+ ContentChannel resolvedContent = requestHandler.handler.handleResponse(response);
+
+ assertSame(response, responseHandler.response);
+
+ ByteBuffer buf = ByteBuffer.allocate(69);
+ resolvedContent.write(buf, null);
+ assertSame(buf, responseHandler.content.writeBuf);
+ responseHandler.content.writeCompletion.completed();
+ MyCompletion writeCompletion = new MyCompletion();
+ resolvedContent.write(buf = ByteBuffer.allocate(69), writeCompletion);
+ assertSame(buf, responseHandler.content.writeBuf);
+ assertFalse(writeCompletion.completed);
+ assertNull(writeCompletion.failed);
+ MyException writeFailed = new MyException();
+ responseHandler.content.writeCompletion.failed(writeFailed);
+ assertFalse(writeCompletion.completed);
+ assertSame(writeFailed, writeCompletion.failed);
+
+ MyCompletion closeCompletion = new MyCompletion();
+ resolvedContent.close(closeCompletion);
+ assertTrue(responseHandler.content.closed);
+ assertFalse(closeCompletion.completed);
+ assertNull(closeCompletion.failed);
+ MyException closeFailed = new MyException();
+ responseHandler.content.closeCompletion.failed(closeFailed);
+ assertFalse(closeCompletion.completed);
+ assertSame(closeFailed, closeCompletion.failed);
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatNullResponseContentIsProxied() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion();
+ Request request = newRequest(driver, requestHandler);
+ ResponseHandler responseHandler = new ResponseHandler() {
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ return null;
+ }
+ };
+ request.connect(responseHandler).close(null);
+ requestHandler.handler.handleResponse(new Response(Response.Status.OK)).close(null);
+ request.release();
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatResponseWriteCompletionCanOnlyBeCalledOnce() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion();
+ MyResponseHandler responseHandler = MyResponseHandler.newInstance();
+ Request request = newRequest(driver, requestHandler);
+ request.connect(responseHandler).close(null);
+ request.release();
+ ContentChannel resolvedContent = requestHandler.handler.handleResponse(new Response(Response.Status.OK));
+
+ CountingCompletionHandler completion = new CountingCompletionHandler();
+ resolvedContent.write(ByteBuffer.allocate(0), completion);
+ assertEquals(0, completion.numCompleted.get());
+ assertEquals(0, completion.numFailed.get());
+ responseHandler.content.writeCompletion.completed();
+ assertEquals(1, completion.numCompleted.get());
+ assertEquals(0, completion.numFailed.get());
+ try {
+ responseHandler.content.writeCompletion.completed();
+ fail();
+ } catch (IllegalStateException e) {
+ // ignore
+ }
+ assertEquals(1, completion.numCompleted.get());
+ assertEquals(0, completion.numFailed.get());
+ try {
+ responseHandler.content.writeCompletion.failed(new Throwable());
+ fail();
+ } catch (IllegalStateException e) {
+ // ignore
+ }
+ assertEquals(1, completion.numCompleted.get());
+ assertEquals(0, completion.numFailed.get());
+
+ resolvedContent.close(null);
+ responseHandler.content.closeCompletion.completed();
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatResponseCloseCompletionCanOnlyBeCalledOnce() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion();
+ MyResponseHandler responseHandler = MyResponseHandler.newInstance();
+ Request request = newRequest(driver, requestHandler);
+ request.connect(responseHandler).close(null);
+ request.release();
+ ContentChannel resolvedContent = requestHandler.handler.handleResponse(new Response(Response.Status.OK));
+
+ CountingCompletionHandler completion = new CountingCompletionHandler();
+ resolvedContent.close(completion);
+ assertEquals(0, completion.numCompleted.get());
+ assertEquals(0, completion.numFailed.get());
+ responseHandler.content.closeCompletion.completed();
+ assertEquals(1, completion.numCompleted.get());
+ assertEquals(0, completion.numFailed.get());
+ try {
+ responseHandler.content.closeCompletion.completed();
+ fail();
+ } catch (IllegalStateException e) {
+ // ignore
+ }
+ assertEquals(1, completion.numCompleted.get());
+ assertEquals(0, completion.numFailed.get());
+ try {
+ responseHandler.content.closeCompletion.failed(new Throwable());
+ fail();
+ } catch (IllegalStateException e) {
+ // ignore
+ }
+ assertEquals(1, completion.numCompleted.get());
+ assertEquals(0, completion.numFailed.get());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatUncaughtCompletionFailureIsLogged() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ MyRequestHandler requestHandler = MyRequestHandler.newInstance();
+ Request request = newRequest(driver, requestHandler);
+ ContentChannel resolvedContent = request.connect(MyResponseHandler.newEagerCompletion());
+ request.release();
+
+ MyLogHandler logHandler = new MyLogHandler();
+ Logger.getLogger(ProxyRequestHandler.class.getName()).addHandler(logHandler);
+
+ resolvedContent.write(ByteBuffer.allocate(69), null);
+ MyException writeFailed = new MyException();
+ requestHandler.content.writeCompletion.failed(writeFailed);
+ assertNotNull(logHandler.record);
+ assertSame(writeFailed, logHandler.record.getThrown());
+
+ resolvedContent.close(null);
+ MyException closeFailed = new MyException();
+ requestHandler.content.closeCompletion.failed(closeFailed);
+ assertNotNull(logHandler.record);
+ assertSame(closeFailed, logHandler.record.getThrown());
+
+ requestHandler.respond();
+ assertTrue(driver.close());
+ }
+
+ private static Request newRequest(TestDriver driver, RequestHandler requestHandler) {
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind("http://host/path", requestHandler);
+ driver.activateContainer(builder);
+ return new Request(driver, URI.create("http://host/path"));
+ }
+
+ private static class MyException extends RuntimeException {
+
+ }
+
+ private static class MyCompletion implements CompletionHandler {
+
+ boolean completed = false;
+ Throwable failed = null;
+
+ @Override
+ public void completed() {
+ completed = true;
+ }
+
+ @Override
+ public void failed(Throwable t) {
+ failed = t;
+ }
+ }
+
+ private static class MyContent implements ContentChannel {
+
+ final boolean eagerCompletion;
+ CompletionHandler writeCompletion = null;
+ CompletionHandler closeCompletion = null;
+ ByteBuffer writeBuf = null;
+ boolean closed = false;
+
+ MyContent(boolean eagerCompletion) {
+ this.eagerCompletion = eagerCompletion;
+ }
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ writeBuf = buf;
+ writeCompletion = handler;
+ if (eagerCompletion) {
+ writeCompletion.completed();
+ }
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ closed = true;
+ closeCompletion = handler;
+ if (eagerCompletion) {
+ closeCompletion.completed();
+ }
+ }
+
+ static MyContent newInstance() {
+ return new MyContent(false);
+ }
+
+ static MyContent newEagerCompletion() {
+ return new MyContent(true);
+ }
+ }
+
+ private static class MyRequestHandler extends AbstractResource implements RequestHandler {
+
+ final MyContent content;
+ Request request = null;
+ ResponseHandler handler = null;
+ boolean timeout = false;
+ boolean destroyed = false;
+
+ MyRequestHandler(MyContent content) {
+ this.content = content;
+ }
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ this.request = request;
+ this.handler = handler;
+ return content;
+ }
+
+ @Override
+ public void handleTimeout(Request request, ResponseHandler handler) {
+ timeout = true;
+ }
+
+ @Override
+ public void destroy() {
+ destroyed = true;
+ }
+
+ void respond() {
+ handler.handleResponse(new Response(Response.Status.OK)).close(null);
+ }
+
+ static MyRequestHandler newInstance() {
+ return new MyRequestHandler(MyContent.newInstance());
+ }
+
+ static MyRequestHandler newEagerCompletion() {
+ return new MyRequestHandler(MyContent.newEagerCompletion());
+ }
+
+ static MyRequestHandler newNullContent() {
+ return new MyRequestHandler(null);
+ }
+ }
+
+ private static class MyResponseHandler implements ResponseHandler {
+
+ final MyContent content;
+ Response response;
+
+ MyResponseHandler(MyContent content) {
+ this.content = content;
+ }
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ this.response = response;
+ return content;
+ }
+
+ static MyResponseHandler newInstance() {
+ return new MyResponseHandler(MyContent.newInstance());
+ }
+
+ static MyResponseHandler newEagerCompletion() {
+ return new MyResponseHandler(MyContent.newEagerCompletion());
+ }
+ }
+
+ private static class MyLogHandler extends Handler {
+
+ LogRecord record;
+
+ @Override
+ public void publish(LogRecord record) {
+ this.record = record;
+ }
+
+ @Override
+ public void flush() {
+
+ }
+
+ @Override
+ public void close() throws SecurityException {
+
+ }
+ }
+
+ private static class CountingCompletionHandler implements CompletionHandler {
+
+ final AtomicInteger numCompleted = new AtomicInteger(0);
+ final AtomicInteger numFailed = new AtomicInteger(0);
+
+ @Override
+ public void completed() {
+ numCompleted.incrementAndGet();
+ }
+
+ @Override
+ public void failed(Throwable t) {
+ numFailed.incrementAndGet();
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/ReferencedResourceTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/ReferencedResourceTestCase.java
new file mode 100644
index 00000000000..4337d8b8c6c
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/ReferencedResourceTestCase.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;
+
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.sameInstance;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+ */
+public class ReferencedResourceTestCase {
+ @Test
+ public void requireThatGettersMatchConstructor() {
+ final SharedResource resource = mock(SharedResource.class);
+ final ResourceReference reference = mock(ResourceReference.class);
+ final ReferencedResource<SharedResource> referencedResource = new ReferencedResource<>(resource, reference);
+ assertThat(referencedResource.getResource(), is(sameInstance(resource)));
+ assertThat(referencedResource.getReference(), is(sameInstance(reference)));
+ }
+
+ @Test
+ public void requireThatCloseCallsReferenceClose() {
+ final SharedResource resource = mock(SharedResource.class);
+ final ResourceReference reference = mock(ResourceReference.class);
+ final ReferencedResource<SharedResource> referencedResource = new ReferencedResource<>(resource, reference);
+ referencedResource.close();
+ verify(reference, times(1)).close();
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/ReferencesTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/ReferencesTestCase.java
new file mode 100644
index 00000000000..d8cbe36c04f
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/ReferencesTestCase.java
@@ -0,0 +1,27 @@
+// 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 org.junit.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+ */
+public class ReferencesTestCase {
+ @Test
+ public void requireThatFromResourceCallsReleaseOnResource() {
+ final SharedResource resource = mock(SharedResource.class);
+ final ResourceReference reference = References.fromResource(resource);
+ reference.close();
+ verify(resource, times(1)).release();
+ }
+
+ @Test
+ public void requireThatNoopReferenceCanBeCalledMultipleTimes() {
+ References.NOOP_REFERENCE.close();
+ References.NOOP_REFERENCE.close();
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/RequestTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/RequestTestCase.java
new file mode 100644
index 00000000000..fe5af79a6d3
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/RequestTestCase.java
@@ -0,0 +1,381 @@
+// 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.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Key;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.jdisc.application.UriPattern;
+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 com.yahoo.jdisc.service.BindingSetNotFoundException;
+import com.yahoo.jdisc.service.CurrentContainer;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class RequestTestCase {
+
+ @Test
+ public void requireThatAccessorsWork() throws BindingSetNotFoundException {
+ MyTimer timer = new MyTimer();
+ timer.currentTime = 69;
+
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(timer);
+ driver.activateContainer(driver.newContainerBuilder());
+ Request request = new Request(driver, URI.create("http://foo/bar"));
+ assertNotNull(request);
+ assertEquals(URI.create("http://foo/bar"), request.getUri());
+ request.setUri(URI.create("http://baz/cox"));
+ assertEquals(URI.create("http://baz/cox"), request.getUri());
+ assertTrue(request.isServerRequest());
+ request.setServerRequest(false);
+ assertFalse(request.isServerRequest());
+ assertEquals(69, request.creationTime(TimeUnit.MILLISECONDS));
+ assertNull(request.getTimeout(TimeUnit.MILLISECONDS));
+ request.setTimeout(10, TimeUnit.MILLISECONDS);
+ assertNotNull(request.getTimeout(TimeUnit.MILLISECONDS));
+ assertEquals(10, request.timeRemaining(TimeUnit.MILLISECONDS).longValue());
+ assertTrue(request.context().isEmpty());
+ assertTrue(request.headers().isEmpty());
+ TimeoutManager timeoutManager = new MyTimeoutManager();
+ request.setTimeoutManager(timeoutManager);
+ assertSame(timeoutManager, request.getTimeoutManager());
+ request.release();
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatCancelWorks() {
+ MyTimer timer = new MyTimer();
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(timer);
+ Request request = newRequest(driver);
+ assertFalse(request.isCancelled());
+ request.cancel();
+ assertTrue(request.isCancelled());
+ request.release();
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatDefaultTimeoutIsInfinite() {
+ MyTimer timer = new MyTimer();
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(timer);
+ Request request = newRequest(driver);
+ assertNull(request.getTimeout(TimeUnit.MILLISECONDS));
+ assertNull(request.timeRemaining(TimeUnit.MILLISECONDS));
+ assertFalse(request.isCancelled());
+ timer.currentTime = Long.MAX_VALUE;
+ assertFalse(request.isCancelled());
+ request.release();
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatTimeRemainingUsesTimer() {
+ MyTimer timer = new MyTimer();
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(timer);
+ Request request = newRequest(driver);
+ request.setTimeout(10, TimeUnit.MILLISECONDS);
+ for (timer.currentTime = 0; timer.currentTime <= request.getTimeout(TimeUnit.MILLISECONDS);
+ ++timer.currentTime)
+ {
+ assertEquals(request.getTimeout(TimeUnit.MILLISECONDS) - timer.currentTime,
+ request.timeRemaining(TimeUnit.MILLISECONDS).longValue());
+ }
+ request.release();
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatTimeoutCausesCancel() {
+ MyTimer timer = new MyTimer();
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(timer);
+ Request request = newRequest(driver);
+ request.setTimeout(10, TimeUnit.MILLISECONDS);
+ assertFalse(request.isCancelled());
+ timer.currentTime = 10;
+ assertTrue(request.isCancelled());
+ request.release();
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatCancelIsTrueIfParentIsCancelled() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ Request parent = newRequest(driver);
+ Request child = new Request(parent, URI.create("http://localhost/"));
+ parent.cancel();
+ assertTrue(child.isCancelled());
+ parent.release();
+ child.release();
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatDestroyReleasesContainer() {
+ final MyContainer container = new MyContainer();
+ Request request = new Request(new CurrentContainer() {
+
+ @Override
+ public Container newReference(URI uri) {
+ return container;
+ }
+ }, URI.create("http://localhost/"));
+ assertEquals(1, container.refCount);
+ request.release();
+ assertEquals(0, container.refCount);
+ }
+
+ @Test
+ public void requireThatServerConnectResolvesToServerBinding() {
+ MyContainer container = new MyContainer();
+ Request request = new Request(container, URI.create("http://localhost/"));
+ request.connect(new MyResponseHandler());
+ assertNotNull(container.asServer);
+ assertTrue(container.asServer);
+ }
+
+ @Test
+ public void requireThatClientConnectResolvesToClientBinding() {
+ MyContainer container = new MyContainer();
+ Request serverReq = new Request(container, URI.create("http://localhost/"));
+ Request clientReq = new Request(serverReq, URI.create("http://localhost/"));
+ clientReq.connect(new MyResponseHandler());
+ assertNotNull(container.asServer);
+ assertFalse(container.asServer);
+ }
+
+ @Test
+ public void requireThatNullTimeoutManagerThrowsException() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ Request request = newRequest(driver);
+
+ try {
+ request.setTimeoutManager(null);
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+
+ request.release();
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatTimeoutManagerCanNotBeReplaced() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ Request request = newRequest(driver);
+
+ TimeoutManager manager = new MyTimeoutManager();
+ request.setTimeoutManager(manager);
+ try {
+ request.setTimeoutManager(manager);
+ fail();
+ } catch (IllegalStateException e) {
+ assertEquals("Timeout manager already set.", e.getMessage());
+ }
+
+ request.release();
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatSetTimeoutCallsTimeoutManager() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ Request request = newRequest(driver);
+
+ MyTimeoutManager timeoutManager = new MyTimeoutManager();
+ request.setTimeoutManager(timeoutManager);
+ request.setTimeout(6, TimeUnit.SECONDS);
+ assertEquals(6000, timeoutManager.timeoutMillis);
+
+ request.release();
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatSetTimeoutManagerPropagatesCurrentTimeout() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ Request request = newRequest(driver);
+
+ MyTimeoutManager timeoutManager = new MyTimeoutManager();
+ request.setTimeout(6, TimeUnit.SECONDS);
+ request.setTimeoutManager(timeoutManager);
+ assertEquals(6000, timeoutManager.timeoutMillis);
+
+ request.release();
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatUriIsNormalized() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ driver.activateContainer(driver.newContainerBuilder());
+
+ assertUri(driver, "http://host/foo", "http://host/foo");
+ assertUri(driver, "http://host/./foo", "http://host/foo");
+ assertUri(driver, "http://host/././foo", "http://host/foo");
+ assertUri(driver, "http://host/foo/", "http://host/foo/");
+ assertUri(driver, "http://host/foo/.", "http://host/foo/");
+ assertUri(driver, "http://host/foo/./", "http://host/foo/");
+ assertUri(driver, "http://host/foo/./.", "http://host/foo/");
+ assertUri(driver, "http://host/foo/././", "http://host/foo/");
+ assertUri(driver, "http://host/foo/..", "http://host/");
+ assertUri(driver, "http://host/foo/../", "http://host/");
+ assertUri(driver, "http://host/foo/../bar", "http://host/bar");
+ assertUri(driver, "http://host/foo/../bar/", "http://host/bar/");
+ assertUri(driver, "http://host//foo//", "http://host/foo/");
+ assertUri(driver, "http://host///foo///", "http://host/foo/");
+ assertUri(driver, "http://host///foo///bar///", "http://host/foo/bar/");
+
+ assertTrue(driver.close());
+ }
+
+ private static void assertUri(CurrentContainer container, String requestUri, String expectedUri) {
+ Request serverReq = new Request(container, URI.create(requestUri));
+ assertEquals(expectedUri, serverReq.getUri().toString());
+
+ serverReq.setUri(URI.create(requestUri));
+ assertEquals(expectedUri, serverReq.getUri().toString());
+
+ Request clientReq = new Request(serverReq, URI.create(requestUri));
+ assertEquals(expectedUri, clientReq.getUri().toString());
+
+ serverReq.release();
+ clientReq.release();
+ }
+
+ private static Request newRequest(TestDriver driver) {
+ driver.activateContainer(driver.newContainerBuilder());
+ return new Request(driver, URI.create("http://host/path"));
+ }
+
+ private static class MyTimer extends AbstractModule implements Timer {
+
+ long currentTime = 0;
+
+ @Override
+ public long currentTimeMillis() {
+ return currentTime;
+ }
+
+ @Override
+ protected void configure() {
+ bind(Timer.class).toInstance(this);
+ }
+ }
+
+ private static class MyContainer implements CurrentContainer, Container {
+
+ Boolean asServer = null;
+ int refCount = 1;
+
+ @Override
+ public Container newReference(URI uri) {
+ return this;
+ }
+
+ @Override
+ public RequestHandler resolveHandler(Request request) {
+ this.asServer = request.isServerRequest();
+ RequestHandler requestHandler = new MyRequestHandler();
+ request.setBindingMatch(new BindingMatch<>(
+ new UriPattern("http://*/*").match(request.getUri()),
+ requestHandler));
+ return requestHandler;
+ }
+
+ @Override
+ public <T> T getInstance(Key<T> key) {
+ return Guice.createInjector().getInstance(key);
+ }
+
+ @Override
+ public <T> T getInstance(Class<T> type) {
+ return Guice.createInjector().getInstance(type);
+ }
+
+ @Override
+ public ResourceReference refer() {
+ ++refCount;
+ return new ResourceReference() {
+ @Override
+ public void close() {
+ --refCount;
+ }
+ };
+ }
+
+ @Override
+ public void release() {
+ --refCount;
+ }
+
+ @Override
+ public long currentTimeMillis() {
+ return 0;
+ }
+ }
+
+ private static class MyRequestHandler extends NoopSharedResource implements RequestHandler {
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ return new MyContentChannel();
+ }
+
+ @Override
+ public void handleTimeout(Request request, ResponseHandler handler) {
+
+ }
+ }
+
+ private static class MyResponseHandler implements ResponseHandler {
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ return null;
+ }
+ }
+
+ private static class MyContentChannel implements ContentChannel {
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+
+ }
+ }
+
+ private static class MyTimeoutManager implements TimeoutManager {
+
+ long timeoutMillis;
+
+ @Override
+ public void scheduleTimeout(Request request) {
+ timeoutMillis = request.getTimeout(TimeUnit.MILLISECONDS);
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/ResponseTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/ResponseTestCase.java
new file mode 100644
index 00000000000..303d6b0dc55
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/ResponseTestCase.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;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ResponseTestCase {
+
+ @Test
+ public void requireThatAccessorsWork() {
+ Response response = new Response(69);
+ assertEquals(69, response.getStatus());
+ response.setStatus(96);
+ assertEquals(96, response.getStatus());
+ Throwable t = new Throwable();
+ response.setError(t);
+ assertSame(t, response.getError());
+ assertTrue(response.context().isEmpty());
+ assertTrue(response.headers().isEmpty());
+ }
+
+ @Test
+ public void requireThatStatusCodesDoNotChange() {
+ assertEquals(100, Response.Status.CONTINUE);
+ assertEquals(101, Response.Status.SWITCHING_PROTOCOLS);
+ assertEquals(102, Response.Status.PROCESSING);
+
+ assertEquals(200, Response.Status.OK);
+ assertEquals(201, Response.Status.CREATED);
+ assertEquals(202, Response.Status.ACCEPTED);
+ assertEquals(203, Response.Status.NON_AUTHORITATIVE_INFORMATION);
+ assertEquals(204, Response.Status.NO_CONTENT);
+ assertEquals(205, Response.Status.RESET_CONTENT);
+ assertEquals(206, Response.Status.PARTIAL_CONTENT);
+ assertEquals(207, Response.Status.MULTI_STATUS);
+
+ assertEquals(300, Response.Status.MULTIPLE_CHOICES);
+ assertEquals(301, Response.Status.MOVED_PERMANENTLY);
+ assertEquals(302, Response.Status.FOUND);
+ assertEquals(303, Response.Status.SEE_OTHER);
+ assertEquals(304, Response.Status.NOT_MODIFIED);
+ assertEquals(305, Response.Status.USE_PROXY);
+ assertEquals(307, Response.Status.TEMPORARY_REDIRECT);
+
+ assertEquals(400, Response.Status.BAD_REQUEST);
+ assertEquals(401, Response.Status.UNAUTHORIZED);
+ assertEquals(402, Response.Status.PAYMENT_REQUIRED);
+ assertEquals(403, Response.Status.FORBIDDEN);
+ assertEquals(404, Response.Status.NOT_FOUND);
+ assertEquals(405, Response.Status.METHOD_NOT_ALLOWED);
+ assertEquals(406, Response.Status.NOT_ACCEPTABLE);
+ assertEquals(407, Response.Status.PROXY_AUTHENTICATION_REQUIRED);
+ assertEquals(408, Response.Status.REQUEST_TIMEOUT);
+ assertEquals(409, Response.Status.CONFLICT);
+ assertEquals(410, Response.Status.GONE);
+ assertEquals(411, Response.Status.LENGTH_REQUIRED);
+ assertEquals(412, Response.Status.PRECONDITION_FAILED);
+ assertEquals(413, Response.Status.REQUEST_TOO_LONG);
+ assertEquals(414, Response.Status.REQUEST_URI_TOO_LONG);
+ assertEquals(415, Response.Status.UNSUPPORTED_MEDIA_TYPE);
+ assertEquals(416, Response.Status.REQUESTED_RANGE_NOT_SATISFIABLE);
+ assertEquals(417, Response.Status.EXPECTATION_FAILED);
+ assertEquals(419, Response.Status.INSUFFICIENT_SPACE_ON_RESOURCE);
+ assertEquals(420, Response.Status.METHOD_FAILURE);
+ assertEquals(422, Response.Status.UNPROCESSABLE_ENTITY);
+ assertEquals(423, Response.Status.LOCKED);
+ assertEquals(424, Response.Status.FAILED_DEPENDENCY);
+
+ assertEquals(505, Response.Status.VERSION_NOT_SUPPORTED);
+ assertEquals(500, Response.Status.INTERNAL_SERVER_ERROR);
+ assertEquals(501, Response.Status.NOT_IMPLEMENTED);
+ assertEquals(502, Response.Status.BAD_GATEWAY);
+ assertEquals(503, Response.Status.SERVICE_UNAVAILABLE);
+ assertEquals(504, Response.Status.GATEWAY_TIMEOUT);
+ assertEquals(505, Response.Status.VERSION_NOT_SUPPORTED);
+ assertEquals(507, Response.Status.INSUFFICIENT_STORAGE);
+ }
+
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/AbstractApplicationTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/AbstractApplicationTestCase.java
new file mode 100644
index 00000000000..da5f046ef1f
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/AbstractApplicationTestCase.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.application;
+
+import com.google.inject.Inject;
+import com.yahoo.jdisc.service.CurrentContainer;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class AbstractApplicationTestCase {
+
+ @Test
+ public void requireThatContainerApiIsAvailable() {
+ TestDriver driver = TestDriver.newInjectedApplicationInstance(MyApplication.class);
+ MyApplication app = (MyApplication)driver.application();
+ app.activateContainer(app.newContainerBuilder());
+ assertNotNull(app.container());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatDestroySignalsTermination() {
+ TestDriver driver = TestDriver.newInjectedApplicationInstance(MyApplication.class);
+ MyApplication app = (MyApplication)driver.application();
+ assertFalse(app.isTerminated());
+ assertTrue(driver.close());
+ assertTrue(app.isTerminated());
+ }
+
+ @Test
+ public void requireThatTerminationCanBeWaitedForWithTimeout() throws InterruptedException {
+ TestDriver driver = TestDriver.newInjectedApplicationInstance(MyApplication.class);
+ final MyApplication app = (MyApplication)driver.application();
+ final CountDownLatch latch = new CountDownLatch(1);
+ Executors.newSingleThreadExecutor().execute(new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ app.awaitTermination(600, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ latch.countDown();
+ }
+ });
+ assertFalse(latch.await(100, TimeUnit.MILLISECONDS));
+ assertTrue(driver.close());
+ assertTrue(latch.await(600, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void requireThatTerminationCanBeWaitedForWithoutTimeout() throws InterruptedException {
+ TestDriver driver = TestDriver.newInjectedApplicationInstance(MyApplication.class);
+ final MyApplication app = (MyApplication)driver.application();
+ final CountDownLatch latch = new CountDownLatch(1);
+ Executors.newSingleThreadExecutor().execute(new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ app.awaitTermination();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ latch.countDown();
+ }
+ });
+ assertFalse(latch.await(100, TimeUnit.MILLISECONDS));
+ assertTrue(driver.close());
+ assertTrue(latch.await(600, TimeUnit.SECONDS));
+ }
+
+ private static class MyApplication extends AbstractApplication {
+
+ @Inject
+ public MyApplication(BundleInstaller bundleInstaller, ContainerActivator activator,
+ CurrentContainer container) {
+ super(bundleInstaller, activator, container);
+ }
+
+ @Override
+ public void start() {
+
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/ApplicationNotReadyTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/ApplicationNotReadyTestCase.java
new file mode 100644
index 00000000000..1351e717015
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/ApplicationNotReadyTestCase.java
@@ -0,0 +1,53 @@
+// 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.ProvisionException;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ApplicationNotReadyTestCase {
+
+ @Test
+ public void requireThatExceptionIsThrown() {
+ try {
+ TestDriver.newInjectedApplicationInstanceWithoutOsgi(MyApplication.class);
+ fail();
+ } catch (ProvisionException e) {
+ Throwable t = e.getCause();
+ assertNotNull(t);
+ assertTrue(t instanceof ApplicationNotReadyException);
+ }
+ }
+
+ private static class MyApplication implements Application {
+
+ @Inject
+ MyApplication(ContainerActivator activator) {
+ activator.activateContainer(activator.newContainerBuilder());
+ }
+
+ @Override
+ public void start() {
+
+ }
+
+ @Override
+ public void stop() {
+
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingMatchTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingMatchTestCase.java
new file mode 100644
index 00000000000..59d0535f99e
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingMatchTestCase.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.application;
+
+import org.junit.Test;
+
+import java.net.URI;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class BindingMatchTestCase {
+
+ @Test
+ public void requireThatAccessorsWork() {
+ Object obj = new Object();
+ BindingMatch<Object> match = new BindingMatch<>(
+ new UriPattern("http://*/*").match(URI.create("http://localhost:69/status.html")),
+ obj);
+ assertSame(obj, match.target());
+ assertEquals(3, match.groupCount());
+ assertEquals("localhost", match.group(0));
+ assertEquals("69", match.group(1));
+ assertEquals("status.html", match.group(2));
+ }
+
+ @Test
+ public void requireThatConstructorArgumentsCanNotBeNull() {
+ try {
+ new BindingMatch<>(null, null);
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+ try {
+ new BindingMatch<>(new UriPattern("http://*/*").match(URI.create("http://localhost/")), null);
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+ try {
+ new BindingMatch<>(null, new Object());
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingRepositoryTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingRepositoryTestCase.java
new file mode 100644
index 00000000000..aa9bc783b74
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingRepositoryTestCase.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.application;
+
+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;
+import com.yahoo.jdisc.test.NonWorkingRequestHandler;
+import org.junit.Test;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class BindingRepositoryTestCase {
+
+ @Test
+ public void requireThatRepositoryCanBeActivated() {
+ BindingRepository<Object> bindings = new BindingRepository<>();
+ bindings.bind("http://host/path", new Object());
+
+ BindingSet<Object> set = bindings.activate();
+ assertNotNull(set);
+ Iterator<Map.Entry<UriPattern, Object>> it = set.iterator();
+ assertNotNull(it);
+ assertTrue(it.hasNext());
+ assertNotNull(it.next());
+ assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void requireThatActivationIsSnapshotOfRepository() {
+ BindingRepository<Object> bindings = new BindingRepository<>();
+ bindings.bind("http://host/path", new Object());
+
+ BindingSet<Object> set = bindings.activate();
+ assertNotNull(set);
+ bindings.clear();
+
+ Iterator<Map.Entry<UriPattern, Object>> it = set.iterator();
+ assertNotNull(it);
+ assertTrue(it.hasNext());
+ assertNotNull(it.next());
+ assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void requireThatObjectsCanBeBound() {
+ BindingRepository<Object> bindings = new BindingRepository<>();
+ Object foo = new Object();
+ Object bar = new Object();
+ bindings.bind("http://host/foo", foo);
+ bindings.bind("http://host/bar", bar);
+
+ Iterator<Map.Entry<UriPattern, Object>> it = bindings.activate().iterator();
+ assertNotNull(it);
+ assertTrue(it.hasNext());
+ Map.Entry<UriPattern, Object> entry = it.next();
+ assertNotNull(entry);
+ assertEquals(new UriPattern("http://host/foo"), entry.getKey());
+ assertSame(foo, entry.getValue());
+ assertTrue(it.hasNext());
+ assertNotNull(entry = it.next());
+ assertEquals(new UriPattern("http://host/bar"), entry.getKey());
+ assertSame(bar, entry.getValue());
+ assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void requireThatPatternCannotBeStolen() {
+ final String pattern = "http://host/path";
+ final RequestHandler originallyBoundHandler = new NonWorkingRequestHandler();
+
+ BindingRepository<Object> bindings = new BindingRepository<>();
+ bindings.bind(pattern, originallyBoundHandler);
+ bindings.bind(pattern, new PatternStealingRequestHandler());
+
+ BindingSet bindingSet = bindings.activate();
+ assertEquals(originallyBoundHandler, bindingSet.resolve(URI.create(pattern)));
+ }
+
+ @Test
+ public void requireThatBindAllMethodWorks() {
+ Object foo = new Object();
+ Object bar = new Object();
+ Object baz = new Object();
+
+ Map<String, Object> toAdd = new HashMap<>();
+ toAdd.put("http://host/foo", foo);
+ toAdd.put("http://host/bar", bar);
+
+ BindingRepository<Object> addTo = new BindingRepository<>();
+ addTo.bind("http://host/baz", baz);
+ addTo.bindAll(toAdd);
+
+ Iterator<Map.Entry<UriPattern, Object>> it = addTo.activate().iterator();
+ Map.Entry<UriPattern, Object> entry = it.next();
+ assertNotNull(entry);
+ assertEquals(new UriPattern("http://host/foo"), entry.getKey());
+ assertSame(foo, entry.getValue());
+ assertTrue(it.hasNext());
+ assertNotNull(entry = it.next());
+ assertEquals(new UriPattern("http://host/baz"), entry.getKey());
+ assertSame(baz, entry.getValue());
+ assertTrue(it.hasNext());
+ assertNotNull(entry = it.next());
+ assertEquals(new UriPattern("http://host/bar"), entry.getKey());
+ assertSame(bar, entry.getValue());
+ assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void requireThatPutAllMethodWorks() {
+ Object foo = new Object();
+ Object bar = new Object();
+ Object baz = new Object();
+
+ BindingRepository<Object> toAdd = new BindingRepository<>();
+ toAdd.bind("http://host/foo", foo);
+ toAdd.bind("http://host/bar", bar);
+
+ BindingRepository<Object> addTo = new BindingRepository<>();
+ addTo.bind("http://host/baz", baz);
+ addTo.putAll(toAdd);
+
+ Iterator<Map.Entry<UriPattern, Object>> it = addTo.activate().iterator();
+ assertTrue(it.hasNext());
+ Map.Entry<UriPattern, Object> entry = it.next();
+ assertNotNull(entry);
+ assertEquals(new UriPattern("http://host/foo"), entry.getKey());
+ assertSame(foo, entry.getValue());
+ assertTrue(it.hasNext());
+ assertNotNull(entry = it.next());
+ assertEquals(new UriPattern("http://host/baz"), entry.getKey());
+ assertSame(baz, entry.getValue());
+ assertTrue(it.hasNext());
+ assertNotNull(entry = it.next());
+ assertEquals(new UriPattern("http://host/bar"), entry.getKey());
+ assertSame(bar, entry.getValue());
+ assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void requireThatPutNullThrowsException() {
+ try {
+ new BindingRepository<>().put(null, new Object());
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+ try {
+ new BindingRepository<>().put(new UriPattern("http://host/foo"), null);
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+ }
+
+ static class PatternStealingRequestHandler extends NoopSharedResource implements RequestHandler {
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void handleTimeout(Request request, ResponseHandler handler) { }
+
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingSetTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingSetTestCase.java
new file mode 100644
index 00000000000..7ff2aa6908b
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/BindingSetTestCase.java
@@ -0,0 +1,506 @@
+// 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.handler.RequestHandler;
+import com.yahoo.jdisc.test.NonWorkingRequestHandler;
+import static com.yahoo.vespa.defaults.Defaults.getDefaults;
+import org.junit.Test;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class BindingSetTestCase {
+
+ @Test
+ public void requireThatAccessorsWork() {
+ Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>();
+ RequestHandler foo = new NonWorkingRequestHandler();
+ handlers.put(new UriPattern("http://host/foo"), foo);
+ RequestHandler bar = new NonWorkingRequestHandler();
+ handlers.put(new UriPattern("http://host/bar"), bar);
+ BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet());
+
+ Iterator<Map.Entry<UriPattern, RequestHandler>> it = bindings.iterator();
+ assertNotNull(it);
+ assertTrue(it.hasNext());
+ Map.Entry<UriPattern, RequestHandler> entry = it.next();
+ assertNotNull(entry);
+ assertEquals(new UriPattern("http://host/foo"), entry.getKey());
+ assertSame(foo, entry.getValue());
+ assertTrue(it.hasNext());
+ assertNotNull(entry = it.next());
+ assertEquals(new UriPattern("http://host/bar"), entry.getKey());
+ assertSame(bar, entry.getValue());
+ assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void requireThatSimpleResolutionWorks() {
+ Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>();
+ RequestHandler foo = new NonWorkingRequestHandler();
+ handlers.put(new UriPattern("http://host/foo"), foo);
+ RequestHandler bar = new NonWorkingRequestHandler();
+ handlers.put(new UriPattern("http://host/bar"), bar);
+
+ BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet());
+ BindingMatch<RequestHandler> match = bindings.match(URI.create("http://host/foo"));
+ assertNotNull(match);
+ assertEquals(0, match.groupCount());
+ assertSame(foo, match.target());
+ assertSame(foo, bindings.resolve(URI.create("http://host/foo")));
+
+ assertNotNull(match = bindings.match(URI.create("http://host/bar")));
+ assertEquals(0, match.groupCount());
+ assertSame(bar, match.target());
+ assertSame(bar, bindings.resolve(URI.create("http://host/bar")));
+ }
+
+ @Test
+ public void requireThatPatternResolutionWorks() {
+ Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>();
+ RequestHandler foo = new NonWorkingRequestHandler();
+ handlers.put(new UriPattern("http://host/*"), foo);
+ RequestHandler bar = new NonWorkingRequestHandler();
+ handlers.put(new UriPattern("http://host/path"), bar);
+
+ BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet());
+ BindingMatch<RequestHandler> match = bindings.match(URI.create("http://host/anon"));
+ assertNotNull(match);
+ assertEquals(1, match.groupCount());
+ assertEquals("anon", match.group(0));
+ assertSame(foo, match.target());
+ assertSame(foo, bindings.resolve(URI.create("http://host/anon")));
+
+ assertNotNull(match = bindings.match(URI.create("http://host/path")));
+ assertEquals(0, match.groupCount());
+ assertSame(bar, match.target());
+ assertSame(bar, bindings.resolve(URI.create("http://host/path")));
+ }
+
+ @Test
+ public void requireThatPatternResolutionWorksForWildCards() {
+ Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>();
+ RequestHandler foo = new NonWorkingRequestHandler();
+ handlers.put(new UriPattern("http://host:*/bar"), foo);
+ RequestHandler bob = new NonWorkingRequestHandler();
+ handlers.put(new UriPattern("http://*abc:*/*bar"), bob);
+ RequestHandler car = new NonWorkingRequestHandler();
+ handlers.put(new UriPattern("*://*:21/*"), car);
+
+
+ BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet());
+ BindingMatch<RequestHandler> match = bindings.match(URI.create("http://host:8080/bar"));
+ assertNotNull(match);
+ assertEquals(1, match.groupCount());
+ assertEquals("8080", match.group(0));
+ assertSame(foo, match.target());
+ assertSame(foo, bindings.resolve(URI.create("http://host:8080/bar")));
+
+ match = bindings.match(URI.create("http://host:8080/foo/bar"));
+ assertNull(match);
+
+ match = bindings.match(URI.create("http://xyzabc:8080/pqrbar"));
+ assertNotNull(match);
+ assertSame(bob, match.target());
+
+ match = bindings.match(URI.create("ftp://lmn:21/abc"));
+ assertNotNull(match);
+ assertSame(car, match.target());
+ }
+
+ @Test
+ public void requireThatPatternResolutionWorksForFilters() {
+ Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>();
+ RequestHandler foo = new NonWorkingRequestHandler();
+ handlers.put(new UriPattern("http://*/filtered/*"), foo);
+
+ BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet());
+ BindingMatch<RequestHandler> match = bindings.match(URI.create("http://localhost:80/status.html"));
+ assertNull(match);
+ match = bindings.match(URI.create("http://localhost/filtered/status.html"));
+ assertNotNull(match);
+ assertSame(foo, match.target());
+ }
+
+ @Test
+ public void requireThatTreeSplitCanBeBoundForSchemes() {
+ Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>();
+ RequestHandler httpfoo = new NonWorkingRequestHandler();
+ RequestHandler httpsfoo = new NonWorkingRequestHandler();
+ RequestHandler ftpfoo = new NonWorkingRequestHandler();
+ handlers.put(new UriPattern("http://host/foo"), httpfoo);
+ handlers.put(new UriPattern("https://host/foo"), httpsfoo);
+ handlers.put(new UriPattern("ftp://host/foo"), ftpfoo);
+ BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet());
+ assertNotNull(bindings);
+ }
+
+ @Test
+ public void requireThatTreeSplitCanBeBoundForHosts() {
+ Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>();
+ RequestHandler foo = new NonWorkingRequestHandler();
+ RequestHandler foobar = new NonWorkingRequestHandler();
+ RequestHandler fooqux = new NonWorkingRequestHandler();
+ handlers.put(new UriPattern("http://hostabc/foo"), foobar);
+ handlers.put(new UriPattern("http://hostpqr/foo"), fooqux);
+ handlers.put(new UriPattern("http://host/foo"), foo);
+ BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet());
+ assertNotNull(bindings);
+ }
+
+ @Test
+ public void requireThatTreeSplitCanBeBoundForPorts() {
+ Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>();
+ RequestHandler foo8080 = new NonWorkingRequestHandler();
+ RequestHandler foo80 = new NonWorkingRequestHandler();
+ RequestHandler foobar = new NonWorkingRequestHandler();
+ RequestHandler foopqrbar = new NonWorkingRequestHandler();
+
+ handlers.put(new UriPattern("http://host:8080/foo"), foo8080);
+ handlers.put(new UriPattern("http://host:70/foo"), foo80);
+ handlers.put(new UriPattern("http://hostpqr:70/foo"), foopqrbar);
+ handlers.put(new UriPattern("http://host:80/foobar"), foobar);
+ BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet());
+ assertNotNull(bindings);
+ }
+
+ @Test
+ public void requireThatTreeSplitCanBeBoundForPaths() {
+ Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>();
+ RequestHandler foo = new NonWorkingRequestHandler();
+ RequestHandler foobar = new NonWorkingRequestHandler();
+ RequestHandler fooqux = new NonWorkingRequestHandler();
+ handlers.put(new UriPattern("http://host/foobar"), foobar);
+ handlers.put(new UriPattern("http://host/fooqux"), fooqux);
+ handlers.put(new UriPattern("http://host/foo"), foo);
+ BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet());
+ assertNotNull(bindings);
+ }
+
+ @Test
+ public void requireThatTreeSplitCanBeBoundForWildcards() {
+ Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>();
+ RequestHandler foo8080 = new NonWorkingRequestHandler();
+ RequestHandler foo80 = new NonWorkingRequestHandler();
+ RequestHandler foobar = new NonWorkingRequestHandler();
+ RequestHandler foopqrbar = new NonWorkingRequestHandler();
+
+ handlers.put(new UriPattern("http://host:8080/foo"), foo8080);
+ handlers.put(new UriPattern("http://host:708/foo"), foo80);
+ handlers.put(new UriPattern("http://host:80/foobar"), foobar);
+ handlers.put(new UriPattern("http://hos*:708/foo"), foopqrbar);
+ BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet());
+ assertNotNull(bindings);
+ assertSame(foopqrbar, bindings.resolve(URI.create("http://hostabc:708/foo")));
+ assertSame(foo80, bindings.resolve(URI.create("http://host:708/foo")));
+ assertSame(foo8080, bindings.resolve(URI.create("http://host:8080/foo")));
+ }
+
+ @Test
+ public void requireThatTreeWorksForURIWithQueryOrFragments() {
+ Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>();
+ RequestHandler foo = new NonWorkingRequestHandler();
+ handlers.put(new UriPattern("http://*/application/v1/session"), foo);
+ BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet());
+ assertNotNull(bindings);
+ assertSame(foo, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" +
+ "/application/v1/session?name=base")));
+ assertSame(foo, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" +
+ "/application/v1/session#application")));
+ }
+
+ @Test
+ public void requireThatTreeWorksForURIWithPathWildCards() {
+ Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>();
+ RequestHandler foo = new NonWorkingRequestHandler();
+ RequestHandler foo1 = new NonWorkingRequestHandler();
+ RequestHandler foo2 = new NonWorkingRequestHandler();
+ RequestHandler foo3 = new NonWorkingRequestHandler();
+ RequestHandler foo4 = new NonWorkingRequestHandler();
+ RequestHandler foo5 = new NonWorkingRequestHandler();
+ RequestHandler foo6 = new NonWorkingRequestHandler();
+ RequestHandler foo7 = new NonWorkingRequestHandler();
+ RequestHandler foo8 = new NonWorkingRequestHandler();
+ RequestHandler foo9 = new NonWorkingRequestHandler();
+ RequestHandler foo10 = new NonWorkingRequestHandler();
+ RequestHandler foo11 = new NonWorkingRequestHandler();
+ RequestHandler foo12 = new NonWorkingRequestHandler();
+ RequestHandler foo13 = new NonWorkingRequestHandler();
+ RequestHandler foo14 = new NonWorkingRequestHandler();
+ RequestHandler foo15 = new NonWorkingRequestHandler();
+
+ handlers.put(new UriPattern("http://*/config/v1/*"), foo);
+ handlers.put(new UriPattern("http://*/config/v1/*/"), foo1);
+ handlers.put(new UriPattern("http://*/config/v1/*/*"), foo2);
+ handlers.put(new UriPattern("http://*/config/v1/*/*/"), foo3);
+ handlers.put(new UriPattern("http://*/application/v2/tenant/"), foo4);
+ handlers.put(new UriPattern("http://*/application/v2/tenant/*"), foo5);
+ handlers.put(new UriPattern("http://*/application/v2/tenant/*/session"), foo6);
+ handlers.put(new UriPattern("http://*/application/v2/tenant/*/session/*/prepared"), foo7);
+ handlers.put(new UriPattern("http://*/application/v2/tenant/*/session/*/active"), foo8);
+ handlers.put(new UriPattern("http://*/application/v2/tenant/*/session/*/content/*"), foo9);
+ handlers.put(new UriPattern("http://*/application/v2/tenant/*/application/"), foo10);
+ handlers.put(new UriPattern("http://*/application/v2/tenant/*/application/*/environment/*/" +
+ "region/*/instance/*/content/*"), foo11);
+ handlers.put(new UriPattern("http://*/config/v2/tenant/*/application/*/*"), foo12);
+ handlers.put(new UriPattern("http://*/config/v2/tenant/*/application/*/*/*"), foo13);
+ handlers.put(new UriPattern("http://*/config/v2/tenant/*/application/*/environment" +
+ "/*/region/*/instance/*/*"), foo14);
+ handlers.put(new UriPattern("http://*/config/v2/tenant/*/application/*/*/*/"), foo15);
+
+
+ BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet());
+ assertNotNull(bindings);
+ assertSame(foo, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" +
+ "/config/v1/cloud.config.log.logd")));
+ assertSame(foo1, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" +
+ "/config/v1/cloud.config.log.logd/")));
+ assertSame(foo2, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" +
+ "/config/v1/cloud.config.log.logd/admin")));
+ assertSame(foo3, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" +
+ "/config/v1/cloud.config.log.logd/admin/")));
+ assertSame(foo4, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" +
+ "/application/v2/tenant/")));
+ assertSame(foo5, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" +
+ "/application/v2/tenant/b")));
+ assertSame(foo6, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" +
+ "/application/v2/tenant/bar/session")));
+ assertSame(foo7, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" +
+ "/application/v2/tenant/bar/session/aef/prepared")));
+ assertSame(foo8, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" +
+ "/application/v2/tenant/bar/session/a/active")));
+ assertSame(foo9, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" +
+ "/application/v2/tenant/bar/session/aef/content/x")));
+ assertSame(foo10, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" +
+ "/application/v2/tenant/bar/session/application/")));
+ assertSame(foo11, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" +
+ "/application/v2/tenant/bar/application/bbc/environment/xyz/region/m/inst" +
+ "ance/a/content/l")));
+ assertSame(foo12, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" +
+ "/config/v2/tenant/bar/application/bbc/xyz")));
+ assertSame(foo13, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" +
+ "/config/v2/tenant/bar/application/bbc/xyz/a")));
+ assertSame(foo14, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" +
+ "/config/v2/tenant/bar/application/bbc/environment/a/region/b/instance/a/b")));
+ assertSame(foo15, bindings.resolve(URI.create("http://abcxyz.yahoo.com:19071" +
+ "/config/v2/tenant/bar/application/bbc/xyz/a/c/")));
+ }
+
+ @Test
+ public void requireThatPathOverPortWorks() {
+ Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>();
+ RequestHandler applicationStatus = new NonWorkingRequestHandler();
+ RequestHandler search = new NonWorkingRequestHandler();
+ RequestHandler legacy = new NonWorkingRequestHandler();
+ handlers.put(new UriPattern("http://*/processing/*"), new NonWorkingRequestHandler());
+ handlers.put(new UriPattern("http://*/statistics/*"), new NonWorkingRequestHandler());
+ handlers.put(new UriPattern("http://*/state/v1/*"), new NonWorkingRequestHandler());
+ handlers.put(new UriPattern("http://*/search/*"), search);
+ handlers.put(new UriPattern("http://*/status.html"), new NonWorkingRequestHandler());
+ handlers.put(new UriPattern("http://*/ApplicationStatus"), applicationStatus);
+ handlers.put(new UriPattern("http://*:" + getDefaults().vespaWebServicePort() + "/*"), legacy);
+ BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet());
+ assertNotNull(bindings);
+
+ assertSame(applicationStatus, bindings.resolve(URI.create
+ ("http://abcxyz.yahoo.com:" + getDefaults().vespaWebServicePort() + "/ApplicationStatus")));
+ assertSame(search, bindings.resolve(URI.create
+ ("http://abcxyz.yahoo.com:" + getDefaults().vespaWebServicePort() + "/search/?query=sddocname:music")));
+ assertSame(legacy, bindings.resolve(URI.create
+ ("http://abcxyz.yahoo.com:" + getDefaults().vespaWebServicePort() + "/stats/?query=stat:query")));
+ }
+
+ @Test
+ public void requireThatPathOverPortsDoNotWorkOverStricterPatterns() {
+ Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>();
+ RequestHandler foo = new NonWorkingRequestHandler();
+ RequestHandler bar = new NonWorkingRequestHandler();
+ handlers.put(new UriPattern("http://host:4050/a/"), foo);
+ handlers.put(new UriPattern("http://host/a/"), bar);
+ BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet());
+ assertNotNull(bindings);
+ assertSame(foo, bindings.resolve(URI.create("http://host:4050/a/")));
+ }
+
+ @Test
+ public void requireThatSchemeOrderOverHost() {
+ Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>();
+ RequestHandler foo = new NonWorkingRequestHandler();
+ RequestHandler bar = new NonWorkingRequestHandler();
+ handlers.put(new UriPattern("http://host:5050/a/"), foo);
+ handlers.put(new UriPattern("ftp://host:5050/a/"), bar);
+ BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet());
+ assertNotNull(bindings);
+ assertSame(foo, bindings.resolve(URI.create("http://host:5050/a/")));
+ assertSame(bar, bindings.resolve(URI.create("ftp://host:5050/a/")));
+ }
+
+ @Test
+ public void requireThatPortsAreOrdered() {
+ Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>();
+ RequestHandler foo = new NonWorkingRequestHandler();
+ RequestHandler bar = new NonWorkingRequestHandler();
+ RequestHandler car = new NonWorkingRequestHandler();
+ handlers.put(new UriPattern("http://host:5050/a/"), foo);
+ handlers.put(new UriPattern("http://host:5051/a/"), bar);
+ handlers.put(new UriPattern("http://host/a/"), car);
+ BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet());
+ assertNotNull(bindings);
+ assertSame(foo, bindings.resolve(URI.create("http://host:5050/a/")));
+ assertSame(bar, bindings.resolve(URI.create("http://host:5051/a/")));
+ assertSame(car, bindings.resolve(URI.create("http://host/a/")));
+ assertSame(car, bindings.resolve(URI.create("http://host:8080/a/")));
+ assertSame(car, bindings.resolve(URI.create("http://host:80/a/")));
+ }
+
+ @Test
+ public void requireThatPathsAreOrdered() {
+ Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>();
+ RequestHandler foo = new NonWorkingRequestHandler();
+ RequestHandler bar = new NonWorkingRequestHandler();
+ RequestHandler car = new NonWorkingRequestHandler();
+ handlers.put(new UriPattern("http://host:5050/a/"), foo);
+ handlers.put(new UriPattern("http://host:5050/b/"), bar);
+ handlers.put(new UriPattern("http://host/a/"), car);
+ BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet());
+ assertNotNull(bindings);
+ assertSame(foo, bindings.resolve(URI.create("http://host:5050/a/")));
+ assertSame(bar, bindings.resolve(URI.create("http://host:5050/b/")));
+ assertSame(car, bindings.resolve(URI.create("http://host/a/")));
+ assertSame(car, bindings.resolve(URI.create("http://host:8080/a/")));
+ assertSame(car, bindings.resolve(URI.create("http://host:80/a/")));
+ }
+
+ @Test
+ public void requireThatStrictPatternsOrderBeforeWildcards() {
+ Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>();
+
+ RequestHandler fooScheme = new NonWorkingRequestHandler();
+ RequestHandler barScheme = new NonWorkingRequestHandler();
+
+ RequestHandler fooHost = new NonWorkingRequestHandler();
+ RequestHandler barHost = new NonWorkingRequestHandler();
+
+ RequestHandler fooPort = new NonWorkingRequestHandler();
+ RequestHandler barPort = new NonWorkingRequestHandler();
+ RequestHandler carPort = new NonWorkingRequestHandler();
+
+ RequestHandler fooPath = new NonWorkingRequestHandler();
+ RequestHandler barPath = new NonWorkingRequestHandler();
+
+ handlers.put(new UriPattern("http://host/x/"), fooScheme);
+ handlers.put(new UriPattern("*://host/x/"), barScheme);
+
+ handlers.put(new UriPattern("http://host/abc/"), fooHost);
+ handlers.put(new UriPattern("http://*/abc/"), barHost);
+
+ handlers.put(new UriPattern("http://host:*/a/"), fooPort);
+ handlers.put(new UriPattern("http://host:5050/b/"), barPort);
+ handlers.put(new UriPattern("http://host/b/"), carPort);
+
+ handlers.put(new UriPattern("http://hostname/abcde/"), fooPath);
+ handlers.put(new UriPattern("http://hostname/*/"), barPath);
+
+ BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet());
+ assertNotNull(bindings);
+ assertSame(fooScheme, bindings.resolve(URI.create("http://host/x/")));
+ assertSame(barScheme, bindings.resolve(URI.create("ftp://host/x/")));
+
+ assertSame(fooHost, bindings.resolve(URI.create("http://host:8080/abc/")));
+ assertSame(barHost, bindings.resolve(URI.create("http://lmn:5050/abc/")));
+
+ assertSame(fooPort, bindings.resolve(URI.create("http://host:5050/a/")));
+ assertSame(barPort, bindings.resolve(URI.create("http://host:5050/b/")));
+ assertSame(carPort, bindings.resolve(URI.create("http://host/b/")));
+ assertSame(carPort, bindings.resolve(URI.create("http://host:8080/b/")));
+ assertSame(carPort, bindings.resolve(URI.create("http://host:80/b/")));
+ assertSame(fooPath, bindings.resolve(URI.create("http://hostname/abcde/")));
+ assertSame(barPath, bindings.resolve(URI.create("http://hostname/abcd/")));
+
+ }
+
+ @Test
+ public void requireThatToStringMethodWorks() {
+ Map<UriPattern, RequestHandler> handlers = new LinkedHashMap<>();
+ RequestHandler foo = new NonWorkingRequestHandler();
+ RequestHandler bar = new NonWorkingRequestHandler();
+ handlers.put(new UriPattern("http://host/foo"), foo);
+ handlers.put(new UriPattern("http://host/bar"), bar);
+ BindingSet<RequestHandler> bindings = new BindingSet<>(handlers.entrySet());
+ assertNotNull(bindings);
+ assertNotNull(bindings.toString()); //Just to get code coverage.
+ }
+
+
+ @Test
+ public void requireThatPatternsAreOrderedMoreSpecificToLess() {
+ assertOrder("3://host/path", "2://host/path", "1://host/path");
+ assertOrder("http://3/path", "http://2/path", "http://1/path");
+ assertOrder("http://host:3/path", "http://host:2/path", "http://host:1/path");
+ assertOrder("http://host/3", "http://host/2", "http://host/1");
+ assertOrder("http://*/*", "*://host/2", "*://host/1");
+ assertOrder("http://host/*", "http://*/2", "http://*/1");
+ assertOrder("http://host:*/3", "http://host:2/2", "http://host:1/1");
+ assertOrder("http://host/*/3/2/", "http://host/*/1/2", "http://host/*/2/*");
+ assertOrder("http://host:69/path",
+ "http://host/*",
+ "http://*:69/path",
+ "http://*/path",
+ "http://*:69/*",
+ "http://*/*",
+ "*://host/path",
+ "*://*/path",
+ "*://*/*");
+ assertOrder("http://*/HelloWorld",
+ "http://*:4080/state/v1/*",
+ "http://*:4083/*",
+ "http://*:4081/*",
+ "http://*:4080/*");
+ }
+
+ private static void assertOrder(String... expected) {
+ for (int off = 0; off < expected.length; ++off) {
+ List<String> actual = new ArrayList<>();
+ for (int i = 0; i < expected.length; ++i) {
+ actual.add(expected[(off + i) % expected.length]);
+ }
+ assertOrder(Arrays.asList(expected), actual);
+
+ actual = new ArrayList<>();
+ for (int i = expected.length; --i >= 0; ) {
+ actual.add(expected[(off + i) % expected.length]);
+ }
+ assertOrder(Arrays.asList(expected), actual);
+ }
+ }
+
+ private static void assertOrder(List<String> expected, List<String> actual) {
+ BindingRepository<Object> repo = new BindingRepository<>();
+ for (String pattern : actual) {
+ repo.bind(pattern, new Object());
+ }
+ BindingSet<Object> bindings = repo.activate();
+ Iterator<Map.Entry<UriPattern, Object>> it = bindings.iterator();
+ for (String pattern : expected) {
+ assertTrue(it.hasNext());
+ assertEquals(new UriPattern(pattern), it.next().getKey());
+ }
+ assertFalse(it.hasNext());
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/BundleInstallationExceptionTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/BundleInstallationExceptionTestCase.java
new file mode 100644
index 00000000000..870db066dc0
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/BundleInstallationExceptionTestCase.java
@@ -0,0 +1,53 @@
+// 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.junit.Test;
+import org.mockito.Mockito;
+import org.osgi.framework.Bundle;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedList;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class BundleInstallationExceptionTestCase {
+
+ @Test
+ public void requireThatAccessorsWork() {
+ Throwable t = new Throwable("foo");
+ Collection<Bundle> bundles = new LinkedList<>();
+ bundles.add(Mockito.mock(Bundle.class));
+ BundleInstallationException e = new BundleInstallationException(bundles, t);
+ assertSame(t, e.getCause());
+ assertEquals(t.getMessage(), e.getCause().getMessage());
+ assertEquals(bundles, e.installedBundles());
+ }
+
+ @Test
+ public void requireThatBundlesCollectionIsDefensivelyCopied() {
+ Collection<Bundle> bundles = new LinkedList<>();
+ bundles.add(Mockito.mock(Bundle.class));
+ BundleInstallationException e = new BundleInstallationException(bundles, new Throwable());
+ bundles.add(Mockito.mock(Bundle.class));
+ assertEquals(1, e.installedBundles().size());
+ }
+
+ @Test
+ public void requireThatBundlesCollectionIsUnmodifiable() {
+ BundleInstallationException e = new BundleInstallationException(Arrays.asList(Mockito.mock(Bundle.class)),
+ new Throwable());
+ try {
+ e.installedBundles().add(Mockito.mock(Bundle.class));
+ fail();
+ } catch (UnsupportedOperationException f) {
+
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/ContainerBuilderTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/ContainerBuilderTestCase.java
new file mode 100644
index 00000000000..811f8fa901b
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/ContainerBuilderTestCase.java
@@ -0,0 +1,116 @@
+// 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.AbstractModule;
+import com.google.inject.Key;
+import com.google.inject.name.Names;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ContainerBuilderTestCase {
+
+ @Test
+ public void requireThatAccessorsWork() throws URISyntaxException {
+ final Object obj = new Object();
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(Object.class).toInstance(obj);
+ bind(String.class).annotatedWith(Names.named("foo")).toInstance("foo");
+ }
+ });
+ ContainerBuilder builder = driver.newContainerBuilder();
+ assertSame(obj, builder.getInstance(Object.class));
+ assertEquals("foo", builder.getInstance(Key.get(String.class, Names.named("foo"))));
+
+ Object ctx = new Object();
+ builder.setAppContext(ctx);
+ assertSame(ctx, builder.appContext());
+
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatContainerThreadFactoryIsBound() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ assertSame(ContainerThread.Factory.class, builder.getInstance(ThreadFactory.class).getClass());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatThreadFactoryCanBeReconfigured() {
+ final ThreadFactory factory = Executors.defaultThreadFactory();
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.guiceModules().install(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(ThreadFactory.class).toInstance(factory);
+ }
+ });
+ assertSame(factory, builder.getInstance(ThreadFactory.class));
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatBindingSetsAreCreatedOnDemand() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ BindingRepository repo = builder.serverBindings("foo");
+ assertNotNull(repo);
+ assertSame(repo, builder.serverBindings("foo"));
+ assertNotNull(repo = builder.serverBindings("bar"));
+ assertSame(repo, builder.serverBindings("bar"));
+ assertNotNull(repo = builder.clientBindings("baz"));
+ assertSame(repo, builder.clientBindings("baz"));
+ assertNotNull(repo = builder.clientBindings("cox"));
+ assertSame(repo, builder.clientBindings("cox"));
+ driver.close();
+ }
+
+ @Test
+ public void requireThatSafeClassCastWorks() {
+ ContainerBuilder.safeClassCast(Integer.class, Integer.class);
+ }
+
+ @Test
+ public void requireThatSafeClassCastThrowsIllegalArgument() {
+ try {
+ ContainerBuilder.safeClassCast(Integer.class, Double.class);
+ fail();
+ } catch (IllegalArgumentException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatSafeStringSplitWorks() {
+ assertTrue(ContainerBuilder.safeStringSplit(new Object(), ",").isEmpty());
+ assertTrue(ContainerBuilder.safeStringSplit("", ",").isEmpty());
+ assertTrue(ContainerBuilder.safeStringSplit(" \f\n\r\t", ",").isEmpty());
+ assertEquals(Arrays.asList("foo"), ContainerBuilder.safeStringSplit("foo", ","));
+ assertEquals(Arrays.asList("foo"), ContainerBuilder.safeStringSplit(" foo", ","));
+ assertEquals(Arrays.asList("foo"), ContainerBuilder.safeStringSplit("foo ", ","));
+ assertEquals(Arrays.asList("foo"), ContainerBuilder.safeStringSplit("foo, ", ","));
+ assertEquals(Arrays.asList("foo"), ContainerBuilder.safeStringSplit("foo ,", ","));
+ assertEquals(Arrays.asList("foo", "bar"), ContainerBuilder.safeStringSplit("foo, bar", ","));
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/ContainerThreadTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/ContainerThreadTestCase.java
new file mode 100644
index 00000000000..d92512b3650
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/ContainerThreadTestCase.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.application;
+
+import com.yahoo.jdisc.Metric;
+import org.junit.Test;
+
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertSame;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ContainerThreadTestCase {
+
+ @Test
+ public void requireThatAccessorsWork() {
+ MetricConsumer consumer = new MyConsumer();
+ ContainerThread thread = new ContainerThread(new MyTask(), consumer);
+ assertSame(consumer, thread.consumer());
+ }
+
+ @Test
+ public void requireThatTaskIsRun() throws InterruptedException {
+ MyTask task = new MyTask();
+ ContainerThread thread = new ContainerThread(task, null);
+ thread.start();
+ task.latch.await(600, TimeUnit.SECONDS);
+ }
+
+ private static class MyConsumer implements MetricConsumer {
+
+ @Override
+ public void set(String key, Number val, Metric.Context ctx) {
+
+ }
+
+ @Override
+ public void add(String key, Number val, Metric.Context ctx) {
+
+ }
+
+ @Override
+ public Metric.Context createContext(Map<String, ?> properties) {
+ return null;
+ }
+ }
+
+ private static class MyTask implements Runnable {
+
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ @Override
+ public void run() {
+ latch.countDown();
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/GlobPatternTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/GlobPatternTestCase.java
new file mode 100644
index 00000000000..c9b4650e572
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/GlobPatternTestCase.java
@@ -0,0 +1,157 @@
+// 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.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class GlobPatternTestCase {
+
+ @Test
+ public void requireThatCompileCreatesExpectedParts() {
+ assertToString("foo");
+ assertToString("*foo");
+ assertToString("*oo");
+ assertToString("f*o");
+ assertToString("fo*");
+ assertToString("foo*");
+ assertToString("**foo");
+ assertToString("**oo");
+ assertToString("**o");
+ assertToString("f**");
+ assertToString("fo**");
+ assertToString("foo**");
+ assertToString("");
+ assertToString("*");
+ }
+
+ @Test
+ public void requireThatGlobMatcherWorks() {
+ assertMatch("foo", "foo", Collections.<String>emptyList());
+ assertNotMatch("foo", "bar");
+
+ assertMatch("*", "foo", Arrays.asList("foo"));
+ assertMatch("*", "bar", Arrays.asList("bar"));
+
+ assertMatch("*foo", "foo", Arrays.asList(""));
+ assertMatch("*oo", "foo", Arrays.asList("f"));
+ assertMatch("f*o", "foo", Arrays.asList("o"));
+ assertMatch("fo*", "foo", Arrays.asList("o"));
+ assertMatch("foo*", "foo", Arrays.asList(""));
+
+ assertNotMatch("*foo", "bar");
+ assertNotMatch("*oo", "bar");
+ assertNotMatch("f*o", "bar");
+ assertNotMatch("fo*", "bar");
+ assertNotMatch("foo*", "bar");
+
+ assertMatch("**foo", "foo", Arrays.asList("", ""));
+ assertMatch("**oo", "foo", Arrays.asList("", "f"));
+ assertMatch("f**o", "foo", Arrays.asList("", "o"));
+ assertMatch("fo**", "foo", Arrays.asList("", "o"));
+ assertMatch("foo**", "foo", Arrays.asList("", ""));
+
+ assertNotMatch("**foo", "bar");
+ assertNotMatch("**oo", "bar");
+ assertNotMatch("f**o", "bar");
+ assertNotMatch("fo**", "bar");
+ assertNotMatch("foo**", "bar");
+
+ assertMatch("foo bar", "foo bar", Collections.<String>emptyList());
+ assertMatch("*foo *bar", "foo bar", Arrays.asList("", ""));
+ assertMatch("foo* bar*", "foo bar", Arrays.asList("", ""));
+ assertMatch("f* *r", "foo bar", Arrays.asList("oo", "ba"));
+
+ assertNotMatch("foo bar", "baz cox");
+ assertNotMatch("*foo *bar", "baz cox");
+ assertNotMatch("foo* bar*", "baz cox");
+ assertNotMatch("f* *r", "baz cox");
+ }
+
+ @Test
+ public void requireThatGlobPatternOrdersMoreSpecificFirst() {
+ assertCompareEq("foo", "foo");
+ assertCompareLt("foo", "foo*");
+ assertCompareLt("foo", "*foo");
+
+ assertCompareEq("foo/bar", "foo/bar");
+ assertCompareLt("foo/bar", "foo");
+ assertCompareLt("foo/bar", "foo*");
+ assertCompareLt("foo/bar", "*foo");
+
+ assertCompareLt("foo/bar", "foo*bar");
+ assertCompareLt("foo/bar", "foo*bar*");
+ assertCompareLt("foo/bar", "*foo*bar");
+
+ assertCompareLt("foo*bar", "foo");
+ assertCompareLt("foo*bar", "foo*");
+ assertCompareLt("foo*bar", "*foo");
+
+ assertCompareLt("foo", "foo*bar*");
+ assertCompareLt("foo*bar*", "foo*");
+ assertCompareLt("*foo", "foo*bar*");
+
+ assertCompareLt("foo", "*foo*bar");
+ assertCompareLt("*foo*bar", "foo*");
+ assertCompareLt("*foo*bar", "*foo");
+
+ assertCompareLt("*/3/2", "*/1/2");
+ assertCompareLt("*/1/2", "*/2/*");
+ }
+
+ @Test
+ public void requireThatEqualsIsImplemented() {
+ assertTrue(GlobPattern.compile("foo").equals(GlobPattern.compile("foo")));
+ assertFalse(GlobPattern.compile("foo").equals(GlobPattern.compile("bar")));
+ }
+
+ @Test
+ public void requireThatHashCodeIsImplemented() {
+ assertTrue(GlobPattern.compile("foo").hashCode() == GlobPattern.compile("foo").hashCode());
+ assertFalse(GlobPattern.compile("foo").hashCode() == GlobPattern.compile("bar").hashCode());
+ }
+
+ private static void assertCompareLt(String lhs, String rhs) {
+ assertTrue(compare(lhs, rhs) < 0);
+ assertTrue(compare(rhs, lhs) > 0);
+ }
+
+ private static void assertCompareEq(String lhs, String rhs) {
+ assertEquals(0, compare(lhs, rhs));
+ assertEquals(0, compare(rhs, lhs));
+ }
+
+ private static int compare(String lhs, String rhs) {
+ return GlobPattern.compile(lhs).compareTo(GlobPattern.compile(rhs));
+ }
+
+ private static void assertMatch(String glob, String str, List<String> expected) {
+ GlobPattern.Match match = GlobPattern.match(glob, str);
+ assertNotNull(match);
+ List<String> actual = new ArrayList<>(match.groupCount());
+ for (int i = 0, len = match.groupCount(); i < len; ++i) {
+ actual.add(match.group(i));
+ }
+ assertEquals(expected, actual);
+ }
+
+ private static void assertNotMatch(String glob, String str) {
+ assertNull(GlobPattern.match(glob, str));
+ }
+
+ private static void assertToString(String pattern) {
+ assertEquals(pattern, GlobPattern.compile(pattern).toString());
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/GuiceRepositoryTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/GuiceRepositoryTestCase.java
new file mode 100644
index 00000000000..39981a812b5
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/GuiceRepositoryTestCase.java
@@ -0,0 +1,197 @@
+// 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.AbstractModule;
+import com.google.inject.ConfigurationException;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.PrivateModule;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class GuiceRepositoryTestCase {
+
+ @Test
+ public void requireThatInstallWorks() {
+ GuiceRepository guice = new GuiceRepository();
+ StringBinding module = new StringBinding("fooKey", "fooVal");
+ guice.install(module);
+ assertBinding(guice, "fooKey", "fooVal");
+
+ Iterator<Module> it = guice.iterator();
+ assertTrue(it.hasNext());
+ assertSame(module, it.next());
+ assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void requireThatInstallAllWorks() {
+ GuiceRepository guice = new GuiceRepository();
+ StringBinding foo = new StringBinding("fooKey", "fooVal");
+ StringBinding bar = new StringBinding("barKey", "barVal");
+ guice.installAll(Arrays.asList(foo, bar));
+ assertBinding(guice, "fooKey", "fooVal");
+ assertBinding(guice, "barKey", "barVal");
+
+ Iterator<Module> it = guice.iterator();
+ assertTrue(it.hasNext());
+ assertSame(foo, it.next());
+ assertTrue(it.hasNext());
+ assertSame(bar, it.next());
+ assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void requireThatUninstallWorks() {
+ GuiceRepository guice = new GuiceRepository();
+ StringBinding module = new StringBinding("fooKey", "fooVal");
+ guice.install(module);
+ assertBinding(guice, "fooKey", "fooVal");
+
+ guice.uninstall(module);
+ assertNoBinding(guice, "fooKey");
+ assertFalse(guice.iterator().hasNext());
+ }
+
+ @Test
+ public void requireThatUninstallAllWorks() {
+ GuiceRepository guice = new GuiceRepository();
+ StringBinding foo = new StringBinding("fooKey", "fooVal");
+ StringBinding bar = new StringBinding("barKey", "barVal");
+ StringBinding baz = new StringBinding("bazKey", "bazVal");
+ guice.installAll(Arrays.asList(foo, bar, baz));
+ assertBinding(guice, "fooKey", "fooVal");
+ assertBinding(guice, "barKey", "barVal");
+ assertBinding(guice, "bazKey", "bazVal");
+
+ guice.uninstallAll(Arrays.asList(foo, baz));
+ assertNoBinding(guice, "fooKey");
+ assertBinding(guice, "barKey", "barVal");
+ assertNoBinding(guice, "bazKey");
+
+ Iterator<Module> it = guice.iterator();
+ assertNotNull(it);
+ assertTrue(it.hasNext());
+ assertSame(bar, it.next());
+ assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void requireThatBindingsCanBeOverridden() {
+ GuiceRepository guice = new GuiceRepository();
+ guice.install(new StringBinding("fooKey", "fooVal1"));
+ assertBinding(guice, "fooKey", "fooVal1");
+ guice.install(new StringBinding("fooKey", "fooVal2"));
+ assertBinding(guice, "fooKey", "fooVal2");
+ }
+
+ @Test
+ public void requireThatModulesAreOnlyEvaluatedOnce() {
+ GuiceRepository guice = new GuiceRepository();
+ EvalCounter foo = new EvalCounter();
+ EvalCounter bar = new EvalCounter();
+ assertEquals(0, foo.cnt);
+ assertEquals(0, bar.cnt);
+ guice.install(foo);
+ assertEquals(1, foo.cnt);
+ assertEquals(0, bar.cnt);
+ guice.install(bar);
+ assertEquals(1, foo.cnt);
+ assertEquals(1, bar.cnt);
+ }
+
+ @Test
+ public void requireThatPrivateModulesWorks() {
+ GuiceRepository guice = new GuiceRepository();
+
+ List<Named> names = Arrays.asList(Names.named("A"), Names.named("B"));
+
+ for (Named name: names) {
+ guice.install(createPrivateInjectNameModule(name));
+ }
+
+ Injector injector = guice.getInjector();
+
+ for (Named name: names) {
+ NameHolder nameHolder = injector.getInstance(Key.get(NameHolder.class, name));
+ assertEquals(name, nameHolder.name);
+ }
+ }
+
+ private Module createPrivateInjectNameModule(final Named name) {
+ return new PrivateModule() {
+ @Override
+ protected void configure() {
+ bind(NameHolder.class).annotatedWith(name).to(NameHolder.class);
+ expose(NameHolder.class).annotatedWith(name);
+ bind(Named.class).toInstance(name);
+ }
+ };
+ }
+
+ private static void assertBinding(GuiceRepository guice, String name, String expected) {
+ assertEquals(expected, guice.getInjector().getInstance(Key.get(String.class, Names.named(name))));
+ }
+
+ private static void assertNoBinding(GuiceRepository guice, String name) {
+ try {
+ guice.getInjector().getInstance(Key.get(String.class, Names.named(name)));
+ fail();
+ } catch (ConfigurationException e) {
+
+ }
+ }
+
+ private static class EvalCounter extends AbstractModule {
+
+ int cnt = 0;
+
+ @Override
+ protected void configure() {
+ ++cnt;
+ }
+ }
+
+ private static class StringBinding extends AbstractModule {
+
+ final String name;
+ final String val;
+
+ StringBinding(String name, String val) {
+ this.name = name;
+ this.val = val;
+ }
+
+ @Override
+ protected void configure() {
+ bind(String.class).annotatedWith(Names.named(name)).toInstance(val);
+ }
+ }
+
+ public static final class NameHolder {
+ public final Named name;
+
+ @Inject
+ public NameHolder(Named name) {
+ this.name = name;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/MetricImplTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/MetricImplTestCase.java
new file mode 100644
index 00000000000..84ee425f55f
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/MetricImplTestCase.java
@@ -0,0 +1,150 @@
+// 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.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.yahoo.jdisc.Metric;
+import org.junit.Test;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class MetricImplTestCase {
+
+ @Test
+ public void requireThatClassIsInjectedByDefault() {
+ Metric metric = Guice.createInjector().getInstance(Metric.class);
+ assertTrue(metric instanceof MetricImpl);
+ }
+
+ @Test
+ public void requireThatConsumerIsOptional() {
+ Injector injector = Guice.createInjector();
+ Metric metric = injector.getInstance(Metric.class);
+ metric.set("foo", 6, null);
+ metric.add("foo", 9, null);
+ }
+
+ @Test
+ public void requireThatConsumerIsCalled() throws InterruptedException {
+ final MyConsumer consumer = new MyConsumer();
+ Injector injector = Guice.createInjector(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(MetricConsumer.class).toInstance(consumer);
+ }
+ });
+ Metric metric = injector.getInstance(Metric.class);
+ metric.set("foo", 6, null);
+ assertEquals(6, consumer.map.get("foo").intValue());
+ metric.add("foo", 9, null);
+ assertEquals(15, consumer.map.get("foo").intValue());
+ Metric.Context ctx = metric.createContext(null);
+ assertEquals(consumer.ctx, ctx);
+ }
+
+ @Test
+ public void requireThatWorkerMetricHasPrecedence() throws InterruptedException {
+ final MyConsumer globalConsumer = new MyConsumer();
+ Injector injector = Guice.createInjector(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(MetricConsumer.class).toInstance(globalConsumer);
+ }
+ });
+ Metric metric = injector.getInstance(Metric.class);
+
+ MyConsumer localConsumer = new MyConsumer();
+ localConsumer.latchRef.set(new CountDownLatch(1));
+ new ContainerThread(new SetTask(metric, "foo", 6), localConsumer).start();
+ localConsumer.latchRef.get().await(600, TimeUnit.SECONDS);
+ assertEquals(6, localConsumer.map.get("foo").intValue());
+ assertTrue(globalConsumer.map.isEmpty());
+
+ localConsumer.latchRef.set(new CountDownLatch(1));
+ new ContainerThread(new AddTask(metric, "foo", 9), localConsumer).start();
+ localConsumer.latchRef.get().await(600, TimeUnit.SECONDS);
+ assertEquals(15, localConsumer.map.get("foo").intValue());
+ assertTrue(globalConsumer.map.isEmpty());
+ }
+
+ private static class SetTask implements Runnable {
+
+ final Metric metric;
+ final String key;
+ final Number val;
+
+ public SetTask(Metric metric, String key, Number val) {
+ this.metric = metric;
+ this.key = key;
+ this.val = val;
+ }
+
+ @Override
+ public void run() {
+ metric.set(key, val, null);
+ }
+ }
+
+ private static class AddTask implements Runnable {
+
+ final Metric metric;
+ final String key;
+ final Number val;
+
+ public AddTask(Metric metric, String key, Number val) {
+ this.metric = metric;
+ this.key = key;
+ this.val = val;
+ }
+
+ @Override
+ public void run() {
+ metric.add(key, val, null);
+ }
+ }
+
+ private static class MyConsumer implements MetricConsumer {
+
+ final ConcurrentMap<String, Integer> map = new ConcurrentHashMap<>();
+ final AtomicReference<CountDownLatch> latchRef = new AtomicReference<>();
+ final Metric.Context ctx = new Metric.Context() { };
+
+ @Override
+ public void set(String key, Number val, Metric.Context ctx) {
+ map.put(key, val.intValue());
+ CountDownLatch latch = latchRef.get();
+ if (latch != null) {
+ latch.countDown();
+ }
+ }
+
+ @Override
+ public void add(String key, Number val, Metric.Context ctx) {
+ map.put(key, map.get(key) + val.intValue());
+ CountDownLatch latch = this.latchRef.get();
+ if (latch != null) {
+ latch.countDown();
+ }
+ }
+
+ @Override
+ public Metric.Context createContext(Map<String, ?> properties) {
+ return ctx;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/OsgiHeaderTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/OsgiHeaderTestCase.java
new file mode 100644
index 00000000000..2d167aa08b6
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/OsgiHeaderTestCase.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.application;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class OsgiHeaderTestCase {
+
+ @Test
+ public void requireThatOsgiHeadersDoNotChange() {
+ assertEquals("X-JDisc-Application", OsgiHeader.APPLICATION);
+ assertEquals("X-JDisc-Preinstall-Bundle", OsgiHeader.PREINSTALL_BUNDLE);
+ assertEquals("X-JDisc-Privileged-Activator", OsgiHeader.PRIVILEGED_ACTIVATOR);
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/OsgiRepositoryTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/OsgiRepositoryTestCase.java
new file mode 100644
index 00000000000..2da63190616
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/OsgiRepositoryTestCase.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.application;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class OsgiRepositoryTestCase {
+
+ @Test
+ public void requireNothingSinceIntegrationModuleTestsThis() {
+ assertTrue(true);
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/ResourcePoolTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/ResourcePoolTestCase.java
new file mode 100644
index 00000000000..ed113572bb7
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/ResourcePoolTestCase.java
@@ -0,0 +1,168 @@
+// 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.AbstractModule;
+import com.google.inject.Key;
+import com.yahoo.jdisc.AbstractResource;
+import com.yahoo.jdisc.ResourceReference;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class ResourcePoolTestCase {
+
+ @Test
+ public void requireThatAddReturnsArgument() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ MyResource foo = new MyResource();
+ assertSame(foo, new ResourcePool(driver.newContainerBuilder()).add(foo));
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatAddDoesNotRetainArgument() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ MyResource foo = new MyResource();
+ assertEquals(1, foo.retainCount());
+ new ResourcePool(driver.newContainerBuilder()).add(foo);
+ assertEquals(1, foo.retainCount());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatAddCanBeUsedWithoutContainerBuilder() {
+ new ResourcePool().add(new MyResource());
+ }
+
+ @Test
+ public void requireThatRetainReturnsArgument() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ MyResource foo = new MyResource();
+ assertSame(foo, new ResourcePool(driver.newContainerBuilder()).retain(foo));
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatRetainRetainsArgument() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ MyResource foo = new MyResource();
+ assertEquals(1, foo.retainCount());
+ new ResourcePool(driver.newContainerBuilder()).retain(foo);
+ assertEquals(2, foo.retainCount());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatRetainCanBeUsedWithoutContainerBuilder() {
+ new ResourcePool().retain(new MyResource());
+ }
+
+ @Test
+ public void requireThatGetReturnsBoundInstance() {
+ final MyResource foo = new MyResource();
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(MyResource.class).toInstance(foo);
+ }
+ });
+ ResourcePool pool = new ResourcePool(driver.newContainerBuilder());
+ assertSame(foo, pool.get(MyResource.class));
+ assertSame(foo, pool.get(Key.get(MyResource.class)));
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatGetDoesNotRetainArgument() {
+ final MyResource foo = new MyResource();
+ assertEquals(1, foo.retainCount());
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(MyResource.class).toInstance(foo);
+ }
+ });
+ ResourcePool pool = new ResourcePool(driver.newContainerBuilder());
+ pool.get(MyResource.class);
+ assertEquals(1, foo.retainCount());
+ pool.get(Key.get(MyResource.class));
+ assertEquals(1, foo.retainCount());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatGetCanNotBeUsedWithoutContainerBuilder() {
+ ResourcePool pool = new ResourcePool();
+ try {
+ pool.get(MyResource.class);
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+ try {
+ pool.get(Key.get(MyResource.class));
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatResourcesAreReleasedOnDestroy() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+
+ ResourcePool pool = new ResourcePool(driver.newContainerBuilder());
+ MyResource foo = pool.add(new MyResource());
+ MyResource bar = pool.add(new MyResource());
+ MyResource baz = pool.add(new MyResource());
+ assertEquals(1, pool.retainCount());
+ assertEquals(1, foo.retainCount());
+ assertEquals(1, bar.retainCount());
+ assertEquals(1, baz.retainCount());
+
+ final ResourceReference secondPoolReference = pool.refer();
+ assertEquals(2, pool.retainCount());
+ assertEquals(1, foo.retainCount());
+ assertEquals(1, bar.retainCount());
+ assertEquals(1, baz.retainCount());
+
+ secondPoolReference.close();
+ assertEquals(1, pool.retainCount());
+ assertEquals(1, foo.retainCount());
+ assertEquals(1, bar.retainCount());
+ assertEquals(1, baz.retainCount());
+
+ pool.release();
+ assertEquals(0, pool.retainCount());
+ assertEquals(0, foo.retainCount());
+ assertEquals(0, bar.retainCount());
+ assertEquals(0, baz.retainCount());
+
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatAutoCloseCallsRelease() throws Exception {
+ MyResource foo = new MyResource();
+ assertEquals(1, foo.retainCount());
+ try (ResourcePool pool = new ResourcePool()) {
+ pool.retain(foo);
+ assertEquals(2, foo.retainCount());
+ }
+ assertEquals(1, foo.retainCount());
+ }
+
+ private static class MyResource extends AbstractResource {
+
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/ServerRepositoryTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/ServerRepositoryTestCase.java
new file mode 100644
index 00000000000..6ce125ff590
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/ServerRepositoryTestCase.java
@@ -0,0 +1,88 @@
+// 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.NoopSharedResource;
+import com.yahoo.jdisc.service.ServerProvider;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Iterator;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ServerRepositoryTestCase {
+
+ @Test
+ public void requireThatInstallWorks() {
+ ServerRepository servers = newServerRepository();
+ MyServer server = new MyServer();
+ servers.install(server);
+
+ Iterator<ServerProvider> it = servers.iterator();
+ assertTrue(it.hasNext());
+ assertSame(server, it.next());
+ assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void requireThatInstallAllWorks() {
+ ServerRepository servers = newServerRepository();
+ ServerProvider foo = new MyServer();
+ ServerProvider bar = new MyServer();
+ servers.installAll(Arrays.asList(foo, bar));
+
+ Iterator<ServerProvider> it = servers.iterator();
+ assertTrue(it.hasNext());
+ assertSame(foo, it.next());
+ assertTrue(it.hasNext());
+ assertSame(bar, it.next());
+ assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void requireThatUninstallWorks() {
+ ServerRepository servers = newServerRepository();
+ ServerProvider server = new MyServer();
+ servers.install(server);
+ servers.uninstall(server);
+ assertFalse(servers.iterator().hasNext());
+ }
+
+ @Test
+ public void requireThatUninstallAllWorks() {
+ ServerRepository servers = newServerRepository();
+ ServerProvider foo = new MyServer();
+ ServerProvider bar = new MyServer();
+ ServerProvider baz = new MyServer();
+ servers.installAll(Arrays.asList(foo, bar, baz));
+ servers.uninstallAll(Arrays.asList(foo, bar));
+ Iterator<ServerProvider> it = servers.iterator();
+ assertNotNull(it);
+ assertTrue(it.hasNext());
+ assertSame(baz, it.next());
+ assertFalse(it.hasNext());
+ }
+
+ private static ServerRepository newServerRepository() {
+ return new ServerRepository(new GuiceRepository());
+ }
+
+ private static class MyServer extends NoopSharedResource implements ServerProvider {
+
+ @Override
+ public void start() {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/application/UriPatternTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/application/UriPatternTestCase.java
new file mode 100644
index 00000000000..c7c45be481a
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/application/UriPatternTestCase.java
@@ -0,0 +1,342 @@
+// 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.junit.Test;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class UriPatternTestCase {
+
+ private static final List<String> NO_GROUPS = Collections.emptyList();
+
+ @Test
+ public void requireThatIllegalPatternsAreDetected() {
+ assertIllegalPattern("scheme");
+ assertIllegalPattern("scheme://");
+ assertIllegalPattern("scheme://host");
+ assertIllegalPattern("scheme://host:0");
+ assertIllegalPattern("scheme://host:69");
+ assertIllegalPattern("scheme://host:-69");
+ assertIllegalPattern("scheme://host:6*/");
+ assertIllegalPattern("scheme://host:6*9/");
+ assertIllegalPattern("scheme://host:*9/");
+ }
+
+ @Test
+ public void requireThatNoPortImpliesWildcard() {
+ assertEquals(new UriPattern("scheme://host/path"),
+ new UriPattern("scheme://host:*/path"));
+ }
+
+ @Test
+ public void requireThatPatternMatches() {
+ // scheme matching
+ UriPattern pattern = new UriPattern("bar://host:69/path");
+ assertNotMatch(pattern, "foobar://host:69/path");
+ assertMatch(pattern, "bar://host:69/path", NO_GROUPS);
+ assertNotMatch(pattern, "barbaz://host:69/path");
+
+ pattern = new UriPattern("*://host:69/path");
+ assertMatch(pattern, "foobar://host:69/path", Arrays.asList("foobar"));
+ assertMatch(pattern, "bar://host:69/path", Arrays.asList("bar"));
+ assertMatch(pattern, "barbaz://host:69/path", Arrays.asList("barbaz"));
+
+ pattern = new UriPattern("*bar://host:69/path");
+ assertMatch(pattern, "foobar://host:69/path", Arrays.asList("foo"));
+ assertMatch(pattern, "bar://host:69/path", Arrays.asList(""));
+ assertNotMatch(pattern, "barbaz://host:69/path");
+
+ pattern = new UriPattern("bar*://host:69/path");
+ assertNotMatch(pattern, "foobar://host:69/path");
+ assertMatch(pattern, "bar://host:69/path", Arrays.asList(""));
+ assertMatch(pattern, "barbaz://host:69/path", Arrays.asList("baz"));
+
+ // host matching
+ pattern = new UriPattern("scheme://bar:69/path");
+ assertNotMatch(pattern, "scheme://foobar:69/path");
+ assertMatch(pattern, "scheme://bar:69/path", NO_GROUPS);
+ assertNotMatch(pattern, "scheme://barbaz:69/path");
+
+ pattern = new UriPattern("scheme://*:69/path");
+ assertMatch(pattern, "scheme://foobar:69/path", Arrays.asList("foobar"));
+ assertMatch(pattern, "scheme://bar:69/path", Arrays.asList("bar"));
+ assertMatch(pattern, "scheme://barbaz:69/path", Arrays.asList("barbaz"));
+
+ pattern = new UriPattern("scheme://*bar:69/path");
+ assertMatch(pattern, "scheme://foobar:69/path", Arrays.asList("foo"));
+ assertMatch(pattern, "scheme://bar:69/path", Arrays.asList(""));
+ assertNotMatch(pattern, "scheme://barbaz:69/path");
+
+ pattern = new UriPattern("scheme://bar*:69/path");
+ assertNotMatch(pattern, "scheme://foobar:69/path");
+ assertMatch(pattern, "scheme://bar:69/path", Arrays.asList(""));
+ assertMatch(pattern, "scheme://barbaz:69/path", Arrays.asList("baz"));
+
+ // port matching
+ pattern = new UriPattern("scheme://host:69/path");
+ assertNotMatch(pattern, "scheme://host:669/path");
+ assertMatch(pattern, "scheme://host:69/path", NO_GROUPS);
+ assertNotMatch(pattern, "scheme://host:699/path");
+
+ pattern = new UriPattern("scheme://host:*/path");
+ assertMatch(pattern, "scheme://host:669/path", Arrays.asList("669"));
+ assertMatch(pattern, "scheme://host:69/path", Arrays.asList("69"));
+ assertMatch(pattern, "scheme://host:699/path", Arrays.asList("699"));
+
+ // path matching
+ pattern = new UriPattern("scheme://host:69/");
+ assertMatch(pattern, "scheme://host:69/", NO_GROUPS);
+ assertNotMatch(pattern, "scheme://host:69/foo");
+
+ pattern = new UriPattern("scheme://host:69/bar");
+ assertNotMatch(pattern, "scheme://host:69/foobar");
+ assertMatch(pattern, "scheme://host:69/bar", NO_GROUPS);
+ assertNotMatch(pattern, "scheme://host:69/barbaz");
+
+ pattern = new UriPattern("scheme://host:69/*");
+ assertMatch(pattern, "scheme://host:69/", Arrays.asList(""));
+ assertMatch(pattern, "scheme://host:69/foobar", Arrays.asList("foobar"));
+ assertMatch(pattern, "scheme://host:69/bar", Arrays.asList("bar"));
+ assertMatch(pattern, "scheme://host:69/barbaz", Arrays.asList("barbaz"));
+
+ pattern = new UriPattern("scheme://host:69/*bar");
+ assertMatch(pattern, "scheme://host:69/foobar", Arrays.asList("foo"));
+ assertMatch(pattern, "scheme://host:69/bar", Arrays.asList(""));
+ assertNotMatch(pattern, "scheme://host:69/barbaz");
+
+ pattern = new UriPattern("scheme://host:69/bar*");
+ assertNotMatch(pattern, "scheme://host:69/foobar");
+ assertMatch(pattern, "scheme://host:69/bar", Arrays.asList(""));
+ assertMatch(pattern, "scheme://host:69/barbaz", Arrays.asList("baz"));
+ }
+
+ @Test
+ public void requireThatUriWithoutHostDoesNotThrowException() {
+ String schemeOnly = "scheme:schemeSpecificPart";
+ String schemeAndPath = "scheme:/path";
+ String pathOnly = "path";
+ String pathOnlyWithSlash = "/path";
+
+ UriPattern pattern = new UriPattern("scheme://host/path");
+ assertNotMatch(pattern, schemeOnly);
+ assertNotMatch(pattern, schemeAndPath);
+ assertNotMatch(pattern, pathOnly);
+ assertNotMatch(pattern, pathOnlyWithSlash);
+
+ pattern = new UriPattern("scheme*://host*/path*");
+ assertNotMatch(pattern, schemeOnly);
+ assertNotMatch(pattern, schemeAndPath);
+ assertNotMatch(pattern, pathOnly);
+ assertNotMatch(pattern, pathOnlyWithSlash);
+
+ pattern = new UriPattern("*://*/*");
+ assertMatch(pattern, schemeOnly, Arrays.asList("scheme", "", ""));
+ assertMatch(pattern, schemeAndPath, Arrays.asList("scheme", "", "path"));
+ assertMatch(pattern, pathOnly, Arrays.asList("", "", "path"));
+ assertMatch(pattern, pathOnlyWithSlash, Arrays.asList("", "", "path"));
+ }
+
+ @Test
+ public void requireThatUriWithoutPathDoesNotThrowException() {
+ UriPattern pattern = new UriPattern("scheme://host/path");
+ assertNotMatch(pattern, "scheme://host");
+
+ pattern = new UriPattern("scheme://host/*");
+ assertMatch(pattern, "scheme://host", Arrays.asList(""));
+ }
+
+ @Test
+ public void requireThatOnlySchemeHostPortAndPathIsMatched() {
+ UriPattern pattern = new UriPattern("scheme://host:69/path");
+ assertMatch(pattern, "scheme://host:69/path?foo", NO_GROUPS);
+ assertMatch(pattern, "scheme://host:69/path?foo#bar", NO_GROUPS);
+ }
+
+ @Test
+ public void requireThatHostSupportsWildcard() {
+ UriPattern pattern = new UriPattern("scheme://*.host/path");
+ assertMatch(pattern, "scheme://a.host/path", Arrays.asList("a"));
+ assertMatch(pattern, "scheme://a.b.host/path", Arrays.asList("a.b"));
+ }
+
+ @Test
+ public void requireThatPrioritiesAreOrderedDescending() {
+ assertCompareLt(new UriPattern("scheme://host:69/path", 1),
+ new UriPattern("scheme://host:69/path", 0));
+ }
+
+ @Test
+ public void requireThatPriorityOrdersBeforeScheme() {
+ assertCompareLt(new UriPattern("*://host:69/path", 1),
+ new UriPattern("scheme://host:69/path", 0));
+ }
+
+ @Test
+ public void requireThatSchemesAreOrdered() {
+ assertCompareLt("b://host:69/path",
+ "a://host:69/path");
+ }
+
+ @Test
+ public void requireThatSchemeOrdersBeforeHost() {
+ assertCompareLt("b://*:69/path",
+ "a://host:69/path");
+ }
+
+ @Test
+ public void requireThatHostsAreOrdered() {
+ assertCompareLt("scheme://b:69/path",
+ "scheme://a:69/path");
+ }
+
+ @Test
+ public void requireThatHostOrdersBeforePath() {
+ assertCompareLt("scheme://b:69/*",
+ "scheme://a:69/path");
+ }
+
+ @Test
+ public void requireThatPortsAreOrdered() {
+ for (int i = 1; i < 69; ++i) {
+ assertCompareEq("scheme://host:" + i + "/path",
+ "scheme://host:" + i + "/path");
+ assertCompareLt("scheme://host:" + (i + 1) + "/path",
+ "scheme://host:" + i + "/path");
+ assertCompareLt("scheme://host:" + i + "/path",
+ "scheme://host:*/path");
+ }
+ }
+
+ @Test
+ public void requireThatPathsAreOrdered() {
+ assertCompareLt("scheme://host:69/b",
+ "scheme://host:69/a");
+ }
+
+ @Test
+ public void requireThatPathOrdersBeforePort() {
+ assertCompareLt("scheme://host:*/b",
+ "scheme://host:69/a");
+ }
+
+ @Test
+ public void requireThatEqualPatternsOrderEqual() {
+ assertCompareEq("scheme://host:69/path",
+ "scheme://host:69/path");
+ assertCompareEq("*://host:69/path",
+ "*://host:69/path");
+ assertCompareEq("scheme://*:69/path",
+ "scheme://*:69/path");
+ assertCompareEq("scheme://host:*/path",
+ "scheme://host:*/path");
+ assertCompareEq("scheme://host:69/*",
+ "scheme://host:69/*");
+ }
+
+ @Test
+ public void requireThatStrictPatternsOrderBeforeWildcards() {
+ assertCompareLt("scheme://host:69/path",
+ "*://host:69/path");
+ assertCompareLt("scheme://a:69/path",
+ "scheme://*:69/path");
+ assertCompareLt("scheme://a:69/path",
+ "scheme://*a:69/path");
+ assertCompareLt("scheme://*aa:69/path",
+ "scheme://*a:69/path");
+ assertCompareLt("scheme://host:69/path",
+ "scheme://host:*/path");
+ assertCompareLt("scheme://host:69/a",
+ "scheme://host:69/*");
+ assertCompareLt("scheme://host:69/a",
+ "scheme://host:69/a*");
+ assertCompareLt("scheme://host:69/aa*",
+ "scheme://host:69/a*");
+ assertCompareLt("scheme://*:69/path",
+ "*://host:69/path");
+ assertCompareLt("scheme://host:*/path",
+ "scheme://*:69/path");
+ assertCompareLt("scheme://host:*/path",
+ "scheme://host:69/*");
+ assertCompareLt("scheme://host:69/foo",
+ "scheme://host:69/*");
+ assertCompareLt("scheme://host:69/foo/bar",
+ "scheme://host:69/foo/*");
+ assertCompareLt("scheme://host:69/foo/bar/baz",
+ "scheme://host:69/foo/bar/*");
+ }
+
+ @Test
+ public void requireThatLongPatternsOrderBeforeShort() {
+ assertCompareLt("scheme://host:69/foo/bar",
+ "scheme://host:69/foo");
+ assertCompareLt("scheme://host:69/foo/bar/baz",
+ "scheme://host:69/foo/bar");
+ }
+
+ private static void assertIllegalPattern(String uri) {
+ try {
+ new UriPattern(uri);
+ fail();
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ private static void assertCompareLt(String lhs, String rhs) {
+ assertCompareLt(new UriPattern(lhs, 0), new UriPattern(rhs, 0));
+ }
+
+ private static void assertCompareLt(UriPattern lhs, UriPattern rhs) {
+ assertEquals(-1, compare(lhs, rhs));
+ }
+
+ private static void assertCompareEq(String lhs, String rhs) {
+ assertCompareEq(new UriPattern(lhs, 0), new UriPattern(rhs, 0));
+ }
+
+ private static void assertCompareEq(UriPattern lhs, UriPattern rhs) {
+ assertEquals(0, compare(lhs, rhs));
+ }
+
+ private static int compare(UriPattern lhs, UriPattern rhs) {
+ int lhsCmp = lhs.compareTo(rhs);
+ int rhsCmp = rhs.compareTo(lhs);
+ if (lhsCmp < 0) {
+ assertTrue(rhsCmp > 0);
+ return -1;
+ }
+ if (lhsCmp > 0) {
+ assertTrue(rhsCmp < 0);
+ return 1;
+ }
+ assertTrue(rhsCmp == 0);
+ return 0;
+ }
+
+ private static void assertMatch(UriPattern pattern, String uri, List<String> expected) {
+ UriPattern.Match match = pattern.match(URI.create(uri));
+ assertNotNull(match);
+ List<String> actual = new ArrayList<>(match.groupCount());
+ for (int i = 0, len = match.groupCount(); i < len; ++i) {
+ actual.add(match.group(i));
+ }
+ assertEquals(expected, actual);
+ }
+
+ private static void assertNotMatch(UriPattern pattern, String uri) {
+ assertNull(pattern.match(URI.create(uri)));
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/BindingMatchingTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/BindingMatchingTestCase.java
new file mode 100644
index 00000000000..7c858a38b80
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/BindingMatchingTestCase.java
@@ -0,0 +1,126 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.benchmark;
+
+import com.yahoo.jdisc.application.BindingRepository;
+import com.yahoo.jdisc.application.BindingSet;
+import com.yahoo.jdisc.application.UriPattern;
+import org.junit.Test;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class BindingMatchingTestCase {
+
+ private static final int NUM_CANDIDATES = 1024;
+ private static final int NUM_MATCHES = 100;
+ private static final int MIN_THREADS = 1;
+ private static final int MAX_THREADS = 64;
+ private static final Random random = new Random();
+ private static final ExecutorService executor = Executors.newFixedThreadPool(MAX_THREADS);
+
+ @Test
+ public void runThroughtputMeasurements() throws Exception {
+ System.err.format("%15s%15s%15s%15s%15s%15s%15s%15s\n",
+ "No. of Bindings", "1 thread", "2 thread", "4 thread", "8 thread", "16 thread", "32 thread", "64 thread");
+ for (int numBindings : Arrays.asList(1, 10, 25, 50, 100, 250)) {
+ BindingRepository<Object> repo = new BindingRepository<>();
+ for (int binding = 0; binding < numBindings; ++binding) {
+ repo.bind("http://*/v" + binding + "/*/data/", new Object());
+ }
+ System.err.format("%15s", numBindings + " binding(s)");
+
+ List<URI> candidates = newCandidates(repo);
+ measureThroughput(repo.activate(), candidates, MAX_THREADS); // warmup
+
+ BindingSet<Object> bindings = repo.activate();
+ for (int numThreads = MIN_THREADS;
+ numThreads <= MAX_THREADS;
+ numThreads *= 2)
+ {
+ System.err.format("%15s", measureThroughput(bindings, candidates, numThreads));
+ }
+ System.err.format("\n");
+ }
+ }
+
+ private static long measureThroughput(BindingSet<Object> bindings, List<URI> candidates, int numThreads) throws Exception {
+ List<MatchTask> tasks = new LinkedList<>();
+ for (int i = 0; i < numThreads; ++i) {
+ MatchTask task = new MatchTask(bindings, candidates);
+ tasks.add(task);
+ }
+ List<Future<Long>> results = executor.invokeAll(tasks);
+ long nanos = 0;
+ for (Future<Long> res : results) {
+ nanos = Math.max(nanos, res.get());
+ }
+ return (numThreads * NUM_MATCHES * TimeUnit.SECONDS.toNanos(1)) / nanos;
+ }
+
+ private List<URI> newCandidates(BindingRepository<Object> bindings) {
+ List<URI> lst = new ArrayList<>(NUM_CANDIDATES);
+ Iterator<Map.Entry<UriPattern, Object>> it = bindings.iterator();
+ for (int i = 0; i < NUM_CANDIDATES; ++i) {
+ if (!it.hasNext()) {
+ it = bindings.iterator();
+ }
+ lst.add(newCandidate(it.next().getKey()));
+ }
+ return lst;
+ }
+
+ private URI newCandidate(UriPattern key) {
+ String pattern = key.toString();
+ StringBuilder uri = new StringBuilder();
+ for (int i = 0, len = pattern.length(); i < len; ++i) {
+ char c = pattern.charAt(i);
+ if (c == '*') {
+ uri.append(random.nextInt(Integer.MAX_VALUE));
+ } else {
+ uri.append(c);
+ }
+ }
+ return URI.create(uri.toString());
+ }
+
+ private static class MatchTask implements Callable<Long> {
+
+ final BindingSet<Object> bindings;
+ final List<URI> candidates;
+
+ MatchTask(BindingSet<Object> bindings, List<URI> candidates) {
+ this.bindings = bindings;
+ this.candidates = candidates;
+ }
+
+ @Override
+ public Long call() throws Exception {
+ Iterator<URI> it = candidates.iterator();
+ for (int i = 0, len = random.nextInt(candidates.size()); i < len; ++i) {
+ it.next();
+ }
+ long time = System.nanoTime();
+ for (int i = 0; i < NUM_MATCHES; ++i) {
+ if (!it.hasNext()) {
+ it = candidates.iterator();
+ }
+ bindings.match(it.next());
+ }
+ return System.nanoTime() - time;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/LatencyTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/LatencyTestCase.java
new file mode 100644
index 00000000000..ec5d1d2f908
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/LatencyTestCase.java
@@ -0,0 +1,264 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.benchmark;
+
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+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.CurrentContainer;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class LatencyTestCase {
+
+ private static final int NUM_REQUESTS = 100;
+
+ @Test
+ public void runLatencyMeasurements() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ MyRequestHandler foo = new MyRequestHandler("foo");
+ MyRequestHandler bar = new MyRequestHandler("bar");
+ MyRequestHandler baz = new MyRequestHandler("baz");
+ builder.serverBindings().bind(foo.uri, foo);
+ builder.serverBindings().bind(bar.uri, bar);
+ builder.serverBindings().bind(baz.uri, baz);
+ driver.activateContainer(builder);
+
+ measureLatencies(NUM_REQUESTS, driver, foo, bar, baz);
+ TimeTrack time = measureLatencies(NUM_REQUESTS, driver, foo, bar, baz);
+ System.err.println("\n" + time);
+
+ foo.release();
+ bar.release();
+ baz.release();
+ assertTrue(driver.close());
+ }
+
+ private static TimeTrack measureLatencies(int numRequests, CurrentContainer container,
+ MyRequestHandler... requestHandlers)
+ {
+ TimeTrack track = new TimeTrack();
+ Random rnd = new Random();
+ for (int i = 0; i < numRequests; ++i) {
+ track.add(measureLatency(container, requestHandlers[rnd.nextInt(requestHandlers.length)]));
+ }
+ return track;
+ }
+
+ private static TimeFrame measureLatency(CurrentContainer container, MyRequestHandler requestHandler) {
+ TimeFrame frame = new TimeFrame();
+
+ Request request = null;
+ ContentChannel requestContent = null;
+ MyResponseHandler responseHandler = new MyResponseHandler();
+ try {
+ URI uri = URI.create(requestHandler.uri);
+ request = new Request(container, uri);
+ frame.handleRequestBegin = System.nanoTime();
+ requestContent = request.connect(responseHandler);
+ frame.handleRequestEnd = requestHandler.handleTime;
+ } finally {
+ if (request != null) {
+ request.release();
+ }
+ }
+ ByteBuffer buf = ByteBuffer.allocate(69);
+ MyCompletion requestWrite = new MyCompletion();
+ frame.requestWriteBegin = System.nanoTime();
+ requestContent.write(buf, requestWrite);
+ frame.requestWriteEnd = requestHandler.requestContent.writeTime;
+ frame.requestWriteCompletionBegin = System.nanoTime();
+ requestHandler.requestContent.writeCompletion.completed();
+ frame.requestWriteCompletionEnd = requestWrite.completedTime;
+
+ MyCompletion requestClose = new MyCompletion();
+ frame.requestCloseBegin = System.nanoTime();
+ requestContent.close(requestClose);
+ frame.requestCloseEnd = requestHandler.requestContent.closeTime;
+ frame.requestCloseCompletionBegin = System.nanoTime();
+ requestHandler.requestContent.closeCompletion.completed();
+ frame.requestCloseCompletionEnd = requestClose.completedTime;
+
+ Response response = new Response(Response.Status.OK);
+ frame.handleResponseBegin = System.nanoTime();
+ ContentChannel responseContent = requestHandler.responseHandler.handleResponse(response);
+ frame.handleResponseEnd = responseHandler.handleTime;
+ MyCompletion responseWrite = new MyCompletion();
+ frame.responseWriteBegin = System.nanoTime();
+ responseContent.write(buf, responseWrite);
+ frame.responseWriteEnd = responseHandler.responseContent.writeTime;
+ frame.responseWriteCompletionBegin = System.nanoTime();
+ responseHandler.responseContent.writeCompletion.completed();
+ frame.responseWriteCompletionEnd = responseWrite.completedTime;
+
+ MyCompletion responseClose = new MyCompletion();
+ frame.responseCloseBegin = System.nanoTime();
+ responseContent.close(responseClose);
+ frame.responseCloseEnd = responseHandler.responseContent.closeTime;
+ frame.responseCloseCompletionBegin = System.nanoTime();
+ responseHandler.responseContent.closeCompletion.completed();
+ frame.responseCloseCompletionEnd = responseClose.completedTime;
+
+ return frame;
+ }
+
+ private static class MyRequestHandler extends AbstractRequestHandler {
+
+ final MyContent requestContent = new MyContent();
+ final String uri;
+ long handleTime;
+ Request request;
+ ResponseHandler responseHandler;
+
+ MyRequestHandler(String path) {
+ this.uri = "http://localhost/" + path;
+ }
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ handleTime = System.nanoTime();
+ this.request = request;
+ responseHandler = handler;
+ return requestContent;
+ }
+ }
+
+ private static class MyResponseHandler implements ResponseHandler {
+
+ final MyContent responseContent = new MyContent();
+ long handleTime;
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ handleTime = System.nanoTime();
+ return responseContent;
+ }
+ }
+
+ private static class MyContent implements ContentChannel {
+
+ long writeTime;
+ long closeTime;
+ CompletionHandler writeCompletion;
+ CompletionHandler closeCompletion;
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ writeTime = System.nanoTime();
+ writeCompletion = handler;
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ closeTime = System.nanoTime();
+ closeCompletion = handler;
+ }
+ }
+
+ private static class MyCompletion implements CompletionHandler {
+
+ long completedTime;
+
+ @Override
+ public void completed() {
+ completedTime = System.nanoTime();
+ }
+
+ @Override
+ public void failed(Throwable t) {
+
+ }
+ }
+
+ private static class TimeFrame {
+
+ long handleRequestBegin;
+ long handleRequestEnd;
+ long requestWriteBegin;
+ long requestWriteEnd;
+ long requestWriteCompletionBegin;
+ long requestWriteCompletionEnd;
+ long requestCloseBegin;
+ long requestCloseEnd;
+ long requestCloseCompletionBegin;
+ long requestCloseCompletionEnd;
+ long handleResponseBegin;
+ long handleResponseEnd;
+ long responseWriteBegin;
+ long responseWriteEnd;
+ long responseWriteCompletionBegin;
+ long responseWriteCompletionEnd;
+ long responseCloseBegin;
+ long responseCloseEnd;
+ long responseCloseCompletionBegin;
+ long responseCloseCompletionEnd;
+ }
+
+ private static class TimeTrack {
+
+ long frameCnt = 0;
+ long handleRequest;
+ long requestWrite;
+ long requestWriteCompletion;
+ long requestClose;
+ long requestCloseCompletion;
+ long handleResponse;
+ long responseWrite;
+ long responseWriteCompletion;
+ long responseClose;
+ long responseCloseCompletion;
+
+ public void add(TimeFrame frame) {
+ ++frameCnt;
+ handleRequest += frame.handleRequestEnd - frame.handleRequestBegin;
+ requestWrite += frame.requestWriteEnd - frame.requestWriteBegin;
+ requestWriteCompletion += frame.requestWriteCompletionEnd - frame.requestWriteCompletionBegin;
+ requestClose += frame.requestCloseEnd - frame.requestCloseBegin;
+ requestCloseCompletion += frame.requestCloseCompletionEnd - frame.requestCloseCompletionBegin;
+ handleResponse += frame.handleResponseEnd - frame.handleResponseBegin;
+ responseWrite += frame.responseWriteEnd - frame.responseWriteBegin;
+ responseWriteCompletion += frame.responseWriteCompletionEnd - frame.responseWriteCompletionBegin;
+ responseClose += frame.responseCloseEnd - frame.responseCloseBegin;
+ responseCloseCompletion += frame.responseCloseCompletionEnd - frame.responseCloseCompletionBegin;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder ret = new StringBuilder();
+ ret.append("------------------------------------\n");
+ ret.append(String.format("HandleRequest : %10.2f\n", (double)handleRequest / frameCnt));
+ ret.append(String.format("RequestWrite : %10.2f\n", (double)requestWrite / frameCnt));
+ ret.append(String.format("RequestWriteCompletion : %10.2f\n", (double)requestWriteCompletion / frameCnt));
+ ret.append(String.format("RequestClose : %10.2f\n", (double)requestClose / frameCnt));
+ ret.append(String.format("RequestCloseCompletion : %10.2f\n", (double)requestCloseCompletion / frameCnt));
+ ret.append(String.format("HandleResponse : %10.2f\n", (double)handleResponse / frameCnt));
+ ret.append(String.format("ResponseWrite : %10.2f\n", (double)responseWrite / frameCnt));
+ ret.append(String.format("ResponseWriteCompletion : %10.2f\n", (double)responseWriteCompletion / frameCnt));
+ ret.append(String.format("ResponseClose : %10.2f\n", (double)responseClose / frameCnt));
+ ret.append(String.format("ResponseCloseCompletion : %10.2f\n", (double)responseCloseCompletion / frameCnt));
+ ret.append("------------------------------------\n");
+
+ double time = (handleRequest + requestWrite + requestWriteCompletion + requestClose +
+ requestCloseCompletion + handleResponse + responseWrite + responseWriteCompletion +
+ responseClose + responseCloseCompletion) / frameCnt;
+ ret.append(String.format("Total nanos : %10.2f\n", time));
+ ret.append(String.format("Requests per second : %10.2f\n", TimeUnit.SECONDS.toNanos(1) / time));
+ ret.append("------------------------------------\n");
+ return ret.toString();
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/ThroughputTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/ThroughputTestCase.java
new file mode 100644
index 00000000000..54a94e3e2dd
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/ThroughputTestCase.java
@@ -0,0 +1,180 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.benchmark;
+
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.handler.AbstractRequestHandler;
+import com.yahoo.jdisc.handler.CallableResponseDispatch;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.RequestDispatch;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.handler.ResponseDispatch;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.service.CurrentContainer;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ThroughputTestCase {
+
+ private static final int NUM_REQUESTS = 100;
+ private static final int MIN_THREADS = 1;
+ private static final int MAX_THREADS = 64;
+ private static final int MIN_LOOPS = 0;
+ private static final int MAX_LOOPS = 1024;
+
+ private static final String HANDLER_URI = "http://localhost/";
+ private static final URI REQUEST_URI = URI.create(HANDLER_URI);
+ private static final ExecutorService executor = Executors.newFixedThreadPool(MAX_THREADS * 2);
+ private static long preventOptimization = 0;
+
+ @Test
+ public void runUnthreadedMeasurementsWithWorkload() throws Exception {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ runMeasurements(driver, new UnthreadedHandler(MAX_LOOPS)); // warmup
+
+ StringBuilder out = new StringBuilder();
+ out.append("\n");
+ out.append(" | ");
+ for (int i = MIN_THREADS; i <= MAX_THREADS; i *= 2) {
+ out.append(String.format("%10d", i));
+ }
+ out.append("\n");
+ out.append("------+-");
+ for (int i = MIN_THREADS; i <= MAX_THREADS; i *= 2) {
+ out.append("----------");
+ }
+ out.append("\n");
+ for (int i = MIN_LOOPS; i <= MAX_LOOPS; i = Math.max(1, i * 2)) {
+ out.append(String.format("%5d | ", i));
+ RequestHandler handler = new UnthreadedHandler(i);
+ for (Long val : runMeasurements(driver, handler)) {
+ out.append(String.format("%10d", val));
+ }
+ out.append("\n");
+ }
+ System.err.println(out);
+ System.err.println(preventOptimization);
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void runThreadedMeasurements() throws Exception {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ runMeasurements(driver, new ThreadedHandler()); // warmup
+
+ Iterator<Long> it = runMeasurements(driver, new ThreadedHandler()).iterator();
+ for (int numThreads = MIN_THREADS; numThreads <= MAX_THREADS; numThreads *= 2) {
+ System.err.println(String.format("%2d threads: %10d", numThreads, it.next()));
+ }
+ assertTrue(driver.close());
+ }
+
+ private static List<Long> runMeasurements(TestDriver driver, RequestHandler handler) throws Exception {
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind(HANDLER_URI, handler);
+ driver.activateContainer(builder);
+ handler.release();
+ List<Long> ret = new LinkedList<>();
+ for (int i = MIN_THREADS; i <= MAX_THREADS; i *= 2) {
+ ret.add(measureThroughput(driver, i));
+ }
+ return ret;
+ }
+
+ private static long measureThroughput(CurrentContainer container, int numThreads) throws Exception {
+ List<RequestTask> tasks = new LinkedList<>();
+ for (int i = 0; i < numThreads; ++i) {
+ RequestTask task = new RequestTask(container);
+ tasks.add(task);
+ }
+ List<Future<Long>> results = executor.invokeAll(tasks);
+ long nanos = 0;
+ for (Future<Long> res : results) {
+ nanos = Math.max(nanos, res.get());
+ }
+ return (numThreads * NUM_REQUESTS * TimeUnit.SECONDS.toNanos(1)) / nanos;
+ }
+
+ private static class RequestTask implements Callable<Long> {
+
+ final CurrentContainer container;
+
+ RequestTask(CurrentContainer container) {
+ this.container = container;
+ }
+
+ @Override
+ public Long call() throws Exception {
+ long time = System.nanoTime();
+ for (int i = 0; i < NUM_REQUESTS; ++i) {
+ new RequestDispatch() {
+
+ @Override
+ protected Request newRequest() {
+ Request request = new Request(container, REQUEST_URI);
+ request.setTimeout(600, TimeUnit.SECONDS);
+ return request;
+ }
+ }.dispatch().get();
+ }
+ return System.nanoTime() - time;
+ }
+ }
+
+ private static class UnthreadedHandler extends AbstractRequestHandler {
+
+ final int numLoops;
+
+ UnthreadedHandler(int numLoops) {
+ this.numLoops = numLoops;
+ }
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ ResponseDispatch.newInstance(Response.Status.OK).dispatch(handler);
+ preventOptimization += nextLong();
+ return null;
+ }
+
+ long nextLong() {
+ Random rnd = new Random();
+ int k = 0;
+ for (int i = 0; i < numLoops; ++i) {
+ k += rnd.nextInt();
+ }
+ return k;
+ }
+ }
+
+ private static class ThreadedHandler extends AbstractRequestHandler {
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ executor.submit(new CallableResponseDispatch(handler) {
+
+ @Override
+ public Response newResponse() {
+ return new Response(Response.Status.OK);
+ }
+ });
+ return null;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/UriMatchingTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/UriMatchingTestCase.java
new file mode 100644
index 00000000000..df2402d1283
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/benchmark/UriMatchingTestCase.java
@@ -0,0 +1,81 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.benchmark;
+
+import com.yahoo.jdisc.application.UriPattern;
+import org.junit.Test;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class UriMatchingTestCase {
+
+ private static final int NUM_MATCHES = 100;
+ private static long preventOptimization = 0;
+
+ @Test
+ public void requireThatUriPatternMatchingIsFast() {
+ List<String> inputs = Arrays.asList(
+ "other://host/",
+ "scheme://other/",
+ "scheme://host/",
+ "scheme://host/foo",
+ "scheme://host/foo/bar",
+ "scheme://host/foo/bar/baz",
+ "scheme://host/other",
+ "scheme://host/other/bar",
+ "scheme://host/other/bar/baz",
+ "scheme://host/foo/other",
+ "scheme://host/foo/other/baz",
+ "scheme://host/foo/bar/other",
+ "scheme://host:69/",
+ "scheme://host:69/foo",
+ "scheme://host:69/foo/bar",
+ "scheme://host:69/foo/bar/baz",
+ "scheme://host:96/");
+ benchmarkMatch("*://*/*", inputs); // warmup
+
+ runBenchmark("*://*/*", inputs);
+ runBenchmark("scheme://*/*", inputs);
+ runBenchmark("scheme://host/*", inputs);
+ runBenchmark("scheme://host:69/*", inputs);
+ runBenchmark("scheme://host:69/foo", inputs);
+ runBenchmark("scheme://host:69/foo/bar", inputs);
+ runBenchmark("scheme://host:69/foo/bar/baz", inputs);
+ runBenchmark("*://host:69/foo/bar/baz", inputs);
+ runBenchmark("*://*/foo/*", inputs);
+ runBenchmark("*://*/foo/*/baz", inputs);
+ runBenchmark("*://*/foo/bar/*", inputs);
+ runBenchmark("*://*/foo/bar/baz", inputs);
+ runBenchmark("*://*/*/bar", inputs);
+ runBenchmark("*://*/*/bar/baz", inputs);
+ runBenchmark("*://*/*/*/baz", inputs);
+
+ System.out.println(">>>>> " + preventOptimization);
+ }
+
+ private static void runBenchmark(String pattern, List<String> inputs) {
+ System.out.format("%-30s %10d\n", pattern, benchmarkMatch(pattern, inputs));
+ }
+
+ private static long benchmarkMatch(String pattern, List<String> inputs) {
+ UriPattern compiled = new UriPattern(pattern);
+ List<URI> uriList = new ArrayList<>(inputs.size());
+ for (String input : inputs) {
+ uriList.add(URI.create(input));
+ }
+ long now = System.nanoTime();
+ for (int i = 0; i < NUM_MATCHES; ++i) {
+ for (URI uri : uriList) {
+ UriPattern.Match match = compiled.match(uri);
+ preventOptimization += match != null ? match.groupCount() : 1;
+ }
+ }
+ return TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - now);
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/client/AbstractClientApplicationTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/client/AbstractClientApplicationTestCase.java
new file mode 100644
index 00000000000..3f04d86c170
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/client/AbstractClientApplicationTestCase.java
@@ -0,0 +1,138 @@
+// 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.Inject;
+import com.yahoo.jdisc.application.BundleInstaller;
+import com.yahoo.jdisc.application.ContainerActivator;
+import com.yahoo.jdisc.service.CurrentContainer;
+import org.junit.Test;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class AbstractClientApplicationTestCase {
+
+ @Test
+ public void requireThatApplicationCanBeShutdown() throws Exception {
+ MyDriver driver = newDriver();
+ assertFalse(driver.awaitDone(100, TimeUnit.MILLISECONDS));
+ assertTrue(driver.awaitApp(600, TimeUnit.SECONDS));
+ driver.app.shutdown();
+ assertTrue(driver.app.isShutdown());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatShutdownCanBeWaitedForWithTimeout() throws Exception {
+ final MyDriver driver = newDriver();
+ assertFalse(driver.awaitDone(100, TimeUnit.MILLISECONDS));
+ assertTrue(driver.awaitApp(600, TimeUnit.SECONDS));
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ Executors.newSingleThreadExecutor().submit(new Callable<Boolean>() {
+
+ @Override
+ public Boolean call() throws Exception {
+ driver.app.awaitShutdown(600, TimeUnit.SECONDS);
+ latch.countDown();
+ return Boolean.TRUE;
+ }
+ });
+ assertFalse(latch.await(100, TimeUnit.MILLISECONDS));
+ driver.app.shutdown();
+ assertTrue(driver.close());
+ assertTrue(latch.await(600, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void requireThatShutdownCanBeWaitedForWithoutTimeout() throws Exception {
+ final MyDriver driver = newDriver();
+ assertFalse(driver.awaitDone(100, TimeUnit.MILLISECONDS));
+ assertTrue(driver.awaitApp(600, TimeUnit.SECONDS));
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ Executors.newSingleThreadExecutor().submit(new Callable<Boolean>() {
+
+ @Override
+ public Boolean call() throws Exception {
+ driver.app.awaitShutdown();
+ latch.countDown();
+ return Boolean.TRUE;
+ }
+ });
+ assertFalse(latch.await(100, TimeUnit.MILLISECONDS));
+ driver.app.shutdown();
+ assertTrue(driver.close());
+ assertTrue(latch.await(600, TimeUnit.SECONDS));
+ }
+
+ private static MyDriver newDriver() {
+ final MyDriver driver = new MyDriver();
+ driver.done = Executors.newSingleThreadExecutor().submit(new Callable<Boolean>() {
+
+ @Override
+ public Boolean call() throws Exception {
+ ClientDriver.runApplication(MyApplication.class, driver);
+ return Boolean.TRUE;
+ }
+ });
+ return driver;
+ }
+
+ private static class MyDriver extends AbstractModule {
+
+ final CountDownLatch appLatch = new CountDownLatch(1);
+ Future<Boolean> done;
+ MyApplication app;
+
+ @Override
+ protected void configure() {
+ bind(MyDriver.class).toInstance(this);
+ }
+
+ boolean awaitApp(int timeout, TimeUnit unit) throws InterruptedException {
+ return appLatch.await(timeout, unit);
+ }
+
+ boolean awaitDone(int timeout, TimeUnit unit) throws ExecutionException, InterruptedException {
+ try {
+ done.get(timeout, unit);
+ return app.isTerminated();
+ } catch (TimeoutException e) {
+ return false;
+ }
+ }
+
+ boolean close() throws ExecutionException, InterruptedException {
+ return awaitDone(600, TimeUnit.SECONDS);
+ }
+ }
+
+ private static class MyApplication extends AbstractClientApplication {
+
+ @Inject
+ MyApplication(BundleInstaller bundleInstaller, ContainerActivator activator,
+ CurrentContainer container, MyDriver driver) {
+ super(bundleInstaller, activator, container);
+ driver.app = this;
+ driver.appLatch.countDown();
+ }
+
+ @Override
+ public void start() {
+
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/client/ClientDriverTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/client/ClientDriverTestCase.java
new file mode 100644
index 00000000000..bef78a22de7
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/client/ClientDriverTestCase.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.client;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ClientDriverTestCase {
+
+ @Test
+ public void requireThatApplicationInstanceInjectionWorks() throws Exception {
+ MyModule module = new MyModule();
+ ClientDriver.runApplication(new MyApplication(module));
+ assertEquals(5, module.state);
+ }
+
+ @Test
+ public void requireThatApplicationClassInjectionWorks() throws Exception {
+ MyModule module = new MyModule();
+ ClientDriver.runApplication(MyApplication.class, module);
+ assertEquals(5, module.state);
+ }
+
+ private static class MyApplication implements ClientApplication {
+
+ final MyModule module;
+
+ @Inject
+ MyApplication(MyModule module) {
+ this.module = module;
+ module.state = 1;
+ }
+
+ @Override
+ public void start() {
+ if (++module.state != 2) {
+ throw new IllegalStateException();
+ }
+ }
+
+ @Override
+ public void run() {
+ if (++module.state != 3) {
+ throw new IllegalStateException();
+ }
+ }
+
+ @Override
+ public void stop() {
+ if (++module.state != 4) {
+ throw new IllegalStateException();
+ }
+ }
+
+ @Override
+ public void destroy() {
+ if (++module.state != 5) {
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ private static class MyModule extends AbstractModule {
+
+ int state = 0;
+
+ @Override
+ protected void configure() {
+ bind(MyModule.class).toInstance(this);
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ActiveContainerFinalizerTest.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ActiveContainerFinalizerTest.java
new file mode 100644
index 00000000000..b2fd357b30c
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ActiveContainerFinalizerTest.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.core;
+
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.test.TestDriver;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Test;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+@SuppressWarnings("UnusedAssignment")
+public class ActiveContainerFinalizerTest {
+
+ @Test
+ public void requireThatMissingContainerReleaseDoesNotPreventShutdown() throws InterruptedException {
+ final TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ driver.activateContainer(driver.newContainerBuilder());
+ Container container = driver.newReference(URI.create("scheme://host"));
+ assertNotNull(container);
+
+ final Termination termination = new Termination();
+ driver.activateContainer(null).notifyTermination(termination);
+ assertFalse(termination.await(100, TimeUnit.MILLISECONDS));
+
+ container = null; // intentionally doing this instead of container.release()
+ assertTrue(termination.await(600, TimeUnit.SECONDS));
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatMissingRequestReleaseDoesNotPreventShutdown() throws InterruptedException {
+ final TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ driver.activateContainer(driver.newContainerBuilder());
+ Request request = new Request(driver, URI.create("scheme://host"));
+ assertNotNull(request);
+
+ final Termination termination = new Termination();
+ driver.activateContainer(null).notifyTermination(termination);
+ assertFalse(termination.await(100, TimeUnit.MILLISECONDS));
+
+ request = null; // intentionally doing this instead of request.release()
+ assertTrue(termination.await(600, TimeUnit.SECONDS));
+ assertTrue(driver.close());
+ }
+
+ private static class Termination implements Runnable {
+
+ volatile boolean done;
+
+ @Override
+ public void run() {
+ done = true;
+ }
+
+ boolean await(final int timeout, final TimeUnit unit) throws InterruptedException {
+ final long timeoutAt = System.currentTimeMillis() + unit.toMillis(timeout);
+ while (!done) {
+ if (System.currentTimeMillis() > timeoutAt) {
+ return false;
+ }
+ Thread.sleep(10);
+ System.gc();
+ }
+ return true;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ActiveContainerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ActiveContainerTestCase.java
new file mode 100644
index 00000000000..5d61e55b7b4
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ActiveContainerTestCase.java
@@ -0,0 +1,160 @@
+// 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.yahoo.jdisc.application.BindingSet;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.application.UriPattern;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.service.ServerProvider;
+import com.yahoo.jdisc.test.NonWorkingRequestHandler;
+import com.yahoo.jdisc.test.NonWorkingServerProvider;
+import com.yahoo.jdisc.test.TestDriver;
+
+import java.util.Iterator;
+import java.util.Map;
+
+import org.junit.Test;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ActiveContainerTestCase {
+
+ @Test
+ public void requireThatGuiceAccessorWorks() {
+ final Object obj = new Object();
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(Object.class).toInstance(obj);
+ }
+ });
+ ActiveContainer container = new ActiveContainer(driver.newContainerBuilder());
+ assertSame(obj, container.guiceInjector().getInstance(Object.class));
+ driver.close();
+ }
+
+ @Test
+ public void requireThatServerAccessorWorks() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ ServerProvider foo = new NonWorkingServerProvider();
+ builder.serverProviders().install(foo);
+ ServerProvider bar = new NonWorkingServerProvider();
+ builder.serverProviders().install(bar);
+ ActiveContainer container = new ActiveContainer(builder);
+
+ Iterator<ServerProvider> it = container.serverProviders().iterator();
+ assertTrue(it.hasNext());
+ assertSame(foo, it.next());
+ assertTrue(it.hasNext());
+ assertSame(bar, it.next());
+ assertFalse(it.hasNext());
+ driver.close();
+ }
+
+ @Test
+ public void requireThatServerBindingAccessorWorks() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ RequestHandler foo = new NonWorkingRequestHandler();
+ RequestHandler bar = new NonWorkingRequestHandler();
+ builder.serverBindings().bind("http://host/foo", foo);
+ builder.serverBindings("bar").bind("http://host/bar", bar);
+ ActiveContainer container = new ActiveContainer(builder);
+
+ Map<String, BindingSet<RequestHandler>> bindings = container.serverBindings();
+ assertNotNull(bindings);
+ assertEquals(2, bindings.size());
+
+ BindingSet<RequestHandler> set = bindings.get(BindingSet.DEFAULT);
+ assertNotNull(set);
+ Iterator<Map.Entry<UriPattern, RequestHandler>> it = set.iterator();
+ assertNotNull(it);
+ assertTrue(it.hasNext());
+ Map.Entry<UriPattern, RequestHandler> entry = it.next();
+ assertNotNull(entry);
+ assertEquals(new UriPattern("http://host/foo"), entry.getKey());
+ assertSame(foo, entry.getValue());
+ assertFalse(it.hasNext());
+
+ assertNotNull(set = bindings.get("bar"));
+ assertNotNull(it = set.iterator());
+ assertTrue(it.hasNext());
+ assertNotNull(entry = it.next());
+ assertEquals(new UriPattern("http://host/bar"), entry.getKey());
+ assertSame(bar, entry.getValue());
+ assertFalse(it.hasNext());
+
+ assertNotNull(bindings = container.clientBindings());
+ assertEquals(1, bindings.size());
+ assertNotNull(set = bindings.get(BindingSet.DEFAULT));
+ assertNotNull(it = set.iterator());
+ assertFalse(it.hasNext());
+
+ driver.close();
+ }
+
+ @Test
+ public void requireThatClientBindingAccessorWorks() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ RequestHandler foo = new NonWorkingRequestHandler();
+ RequestHandler bar = new NonWorkingRequestHandler();
+ builder.clientBindings().bind("http://host/foo", foo);
+ builder.clientBindings("bar").bind("http://host/bar", bar);
+ ActiveContainer container = new ActiveContainer(builder);
+
+ Map<String, BindingSet<RequestHandler>> bindings = container.clientBindings();
+ assertNotNull(bindings);
+ assertEquals(2, bindings.size());
+
+ BindingSet<RequestHandler> set = bindings.get(BindingSet.DEFAULT);
+ assertNotNull(set);
+ Iterator<Map.Entry<UriPattern, RequestHandler>> it = set.iterator();
+ assertNotNull(it);
+ assertTrue(it.hasNext());
+ Map.Entry<UriPattern, RequestHandler> entry = it.next();
+ assertNotNull(entry);
+ assertEquals(new UriPattern("http://host/foo"), entry.getKey());
+ assertSame(foo, entry.getValue());
+ assertFalse(it.hasNext());
+
+ assertNotNull(set = bindings.get("bar"));
+ assertNotNull(it = set.iterator());
+ assertTrue(it.hasNext());
+ assertNotNull(entry = it.next());
+ assertEquals(new UriPattern("http://host/bar"), entry.getKey());
+ assertSame(bar, entry.getValue());
+ assertFalse(it.hasNext());
+
+ assertNotNull(bindings = container.serverBindings());
+ assertEquals(1, bindings.size());
+ assertNotNull(set = bindings.get(BindingSet.DEFAULT));
+ assertNotNull(it = set.iterator());
+ assertFalse(it.hasNext());
+
+ driver.close();
+ }
+
+ @Test
+ public void requireThatDefaultBindingsAreAlwaysCreated() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ ActiveContainer container = new ActiveContainer(builder);
+
+ Map<String, BindingSet<RequestHandler>> bindings = container.serverBindings();
+ assertNotNull(bindings);
+ assertEquals(1, bindings.size());
+ BindingSet<RequestHandler> set = bindings.get(BindingSet.DEFAULT);
+ assertFalse(set.iterator().hasNext());
+ driver.close();
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationConfigModuleTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationConfigModuleTestCase.java
new file mode 100644
index 00000000000..1d23d671e0f
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationConfigModuleTestCase.java
@@ -0,0 +1,114 @@
+// 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.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.name.Names;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import org.junit.Test;
+import static org.junit.Assert.fail;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class ApplicationConfigModuleTestCase {
+
+ @Test
+ public void requireThatEntriesAreBoundWithLowerCaseKeys() {
+ Map<String, String> config = new HashMap<>();
+ config.put("foo_key", "foo");
+ config.put("BAR_key", "bar");
+ config.put("BAZ_KEY", "baz");
+
+ Injector injector = Guice.createInjector(new ApplicationConfigModule(config));
+ assertBinding(injector, "foo_key", "foo");
+ assertBinding(injector, "bar_key", "bar");
+ assertBinding(injector, "baz_key", "baz");
+ }
+
+ @Test
+ public void requireThatEntriesAreBoundWithUnmodifiedValue() {
+ Map<String, String> config = new HashMap<>();
+ config.put("foo", "foo_val");
+ config.put("bar", "BAR_val");
+ config.put("baz", "BAZ_VAL");
+
+ Injector injector = Guice.createInjector(new ApplicationConfigModule(config));
+ assertBinding(injector, "foo", "foo_val");
+ assertBinding(injector, "bar", "BAR_val");
+ assertBinding(injector, "baz", "BAZ_VAL");
+ }
+
+ @Test
+ public void requireThatUpperCaseKeysPrecedeLowerCaseKeys() {
+ Map<String, String> config = new HashMap<>();
+ config.put("foo", "lower-case");
+ assertBinding(config, "foo", "lower-case");
+
+ config.put("Foo", "mixed-case 1");
+ assertBinding(config, "foo", "mixed-case 1");
+
+ config.put("FOO", "upper-case");
+ assertBinding(config, "foo", "upper-case");
+
+ config.put("FOo", "mixed-case 2");
+ assertBinding(config, "foo", "upper-case");
+ }
+
+ @Test
+ public void requireThatNullFileNameThrowsException() throws IOException {
+ try {
+ ApplicationConfigModule.newInstanceFromFile(null);
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatFileNotFoundThrowsException() throws IOException {
+ try {
+ ApplicationConfigModule.newInstanceFromFile("/file/not/found");
+ fail();
+ } catch (FileNotFoundException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatPropertieFilesCanBeRead() throws IOException {
+ Properties props = new Properties();
+ props.put("foo_key", "foo_val");
+
+ File file = File.createTempFile("config-", ".properties");
+ file.deleteOnExit();
+ FileOutputStream out = new FileOutputStream(file);
+ props.store(out, null);
+ out.close();
+
+ assertBinding(ApplicationConfigModule.newInstanceFromFile(file.getAbsolutePath()), "foo_key", "foo_val");
+ }
+
+ private static void assertBinding(Map<String, String> config, String stringName, String expected) {
+ assertBinding(new ApplicationConfigModule(config), stringName, expected);
+ }
+
+ private static void assertBinding(Module module, String stringName, String expected) {
+ assertBinding(Guice.createInjector(module), stringName, expected);
+ }
+
+ private static void assertBinding(Injector injector, String stringName, String expected) {
+ assertEquals(expected, injector.getInstance(Key.get(String.class, Names.named(stringName))));
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationEnvironmentModuleTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationEnvironmentModuleTestCase.java
new file mode 100644
index 00000000000..77af705cfac
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationEnvironmentModuleTestCase.java
@@ -0,0 +1,57 @@
+// 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.*;
+import com.yahoo.jdisc.application.ContainerActivator;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.application.OsgiFramework;
+import com.yahoo.jdisc.service.CurrentContainer;
+import com.yahoo.jdisc.test.NonWorkingOsgiFramework;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ThreadFactory;
+
+import org.junit.Test;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ApplicationEnvironmentModuleTestCase {
+
+ @Test
+ public void requireThatBindingsExist() {
+ List<Class> expected = new LinkedList<>();
+ expected.add(ContainerActivator.class);
+ expected.add(ContainerBuilder.class);
+ expected.add(CurrentContainer.class);
+ expected.add(OsgiFramework.class);
+ expected.add(ThreadFactory.class);
+
+ Injector injector = Guice.createInjector();
+ for (Map.Entry<Key<?>, Binding<?>> entry : injector.getBindings().entrySet()) {
+ expected.add(entry.getKey().getTypeLiteral().getRawType());
+ }
+
+ ApplicationLoader loader = new ApplicationLoader(new NonWorkingOsgiFramework(),
+ Collections.<Module>emptyList());
+ injector = Guice.createInjector(new ApplicationEnvironmentModule(loader));
+ for (Map.Entry<Key<?>, Binding<?>> entry : injector.getBindings().entrySet()) {
+ assertNotNull(expected.remove(entry.getKey().getTypeLiteral().getRawType()));
+ }
+ assertTrue(expected.isEmpty());
+ }
+
+ @Test
+ public void requireThatContainerBuilderCanBeInjected() {
+ ApplicationLoader loader = new ApplicationLoader(new NonWorkingOsgiFramework(),
+ Collections.<Module>emptyList());
+ assertNotNull(new ApplicationEnvironmentModule(loader).containerBuilder());
+ assertNotNull(Guice.createInjector(new ApplicationEnvironmentModule(loader))
+ .getInstance(ContainerBuilder.class));
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationLoaderTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationLoaderTestCase.java
new file mode 100644
index 00000000000..398fbcba839
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationLoaderTestCase.java
@@ -0,0 +1,259 @@
+// 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.ConfigurationException;
+import com.google.inject.Module;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.Application;
+import com.yahoo.jdisc.application.ApplicationNotReadyException;
+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.CurrentContainer;
+import com.yahoo.jdisc.test.NonWorkingOsgiFramework;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+import org.osgi.framework.BundleContext;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ApplicationLoaderTestCase {
+
+ @Test
+ public void requireThatStartFailsWithoutApplication() throws Exception {
+ ApplicationLoader loader = new ApplicationLoader(new NonWorkingOsgiFramework(),
+ Collections.<Module>emptyList());
+ try {
+ loader.init(null, false);
+ loader.start();
+ fail();
+ } catch (ConfigurationException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatStopDoesNotFailWithoutStart() throws Exception {
+ ApplicationLoader loader = new ApplicationLoader(new NonWorkingOsgiFramework(),
+ Collections.<Module>emptyList());
+ loader.stop();
+ loader.destroy();
+ }
+
+ @Test
+ public void requireThatDestroyDoesNotFailWithActiveContainer() throws Exception {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ assertNull(driver.activateContainer(driver.newContainerBuilder()));
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatApplicationStartExceptionUnsetsAndDestroysApplication() throws Exception {
+ MyApplication app = MyApplication.newStartException();
+ ApplicationLoader loader = new ApplicationLoader(new NonWorkingOsgiFramework(),
+ Arrays.asList(new MyApplicationModule(app)));
+ loader.init(null, false);
+ try {
+ loader.start();
+ fail();
+ } catch (MyException e) {
+
+ }
+ assertNull(loader.application());
+ assertFalse(app.stop.await(100, TimeUnit.MILLISECONDS));
+ assertTrue(app.destroy.await(600, TimeUnit.SECONDS));
+ try {
+ loader.activateContainer(loader.newContainerBuilder());
+ fail();
+ } catch (ApplicationNotReadyException e) {
+
+ }
+ loader.stop();
+ loader.destroy();
+ }
+
+ @Test
+ public void requireThatApplicationStopExceptionDestroysApplication() throws Exception {
+ MyApplication app = MyApplication.newStopException();
+ ApplicationLoader loader = new ApplicationLoader(new NonWorkingOsgiFramework(),
+ Arrays.asList(new MyApplicationModule(app)));
+ loader.init(null, false);
+ loader.start();
+ try {
+ loader.stop();
+ } catch (MyException e) {
+
+ }
+ assertTrue(app.destroy.await(600, TimeUnit.SECONDS));
+ loader.destroy();
+ }
+
+ @Test
+ public void requireThatApplicationDestroyIsCalledAfterContainerTermination() throws InterruptedException {
+ MyApplication app = MyApplication.newInstance();
+ TestDriver driver = TestDriver.newInjectedApplicationInstance(app);
+ ContainerBuilder builder = driver.newContainerBuilder();
+ MyRequestHandler requestHandler = new MyRequestHandler();
+ builder.serverBindings().bind("scheme://host/path", requestHandler);
+ driver.activateContainer(builder);
+ driver.dispatchRequest("scheme://host/path", new MyResponseHandler());
+ driver.scheduleClose();
+ assertFalse(app.destroy.await(100, TimeUnit.MILLISECONDS));
+ requestHandler.responseHandler.handleResponse(new Response(Response.Status.OK)).close(null);
+ assertTrue(app.destroy.await(600, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void requireThatContainerActivatorReturnsPrev() throws Exception {
+ TestDriver driver = TestDriver.newInjectedApplicationInstance(MyApplication.newInstance());
+ assertNull(driver.activateContainer(driver.newContainerBuilder()));
+ assertNotNull(driver.activateContainer(null));
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatOsgiServicesAreRegistered() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstance();
+ BundleContext ctx = driver.osgiFramework().bundleContext();
+ Object service = ctx.getService(ctx.getServiceReference(CurrentContainer.class.getName()));
+ assertTrue(service instanceof CurrentContainer);
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatThreadFactoryCanBeBound() {
+ final ThreadFactory factory = Executors.defaultThreadFactory();
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(ThreadFactory.class).toInstance(factory);
+ }
+ });
+ ContainerBuilder builder = driver.newContainerBuilder();
+ assertSame(factory, builder.getInstance(ThreadFactory.class));
+ assertTrue(driver.close());
+ }
+
+ private static class MyApplicationModule extends AbstractModule {
+
+ final Application application;
+
+ public MyApplicationModule(Application application) {
+ this.application = application;
+ }
+
+ @Override
+ protected void configure() {
+ bind(Application.class).toInstance(application);
+ }
+ }
+
+ private static class MyApplication implements Application {
+
+ final CountDownLatch start = new CountDownLatch(1);
+ final CountDownLatch stop = new CountDownLatch(1);
+ final CountDownLatch destroy = new CountDownLatch(1);
+ final boolean startException;
+ final boolean stopException;
+
+ MyApplication(boolean startException, boolean stopException) {
+ this.startException = startException;
+ this.stopException = stopException;
+ }
+
+ @Override
+ public void start() {
+ start.countDown();
+ if (startException) {
+ throw new MyException();
+ }
+ }
+
+ @Override
+ public void stop() {
+ stop.countDown();
+ if (stopException) {
+ throw new MyException();
+ }
+ }
+
+ @Override
+ public void destroy() {
+ destroy.countDown();
+ }
+
+ public static MyApplication newInstance() {
+ return new MyApplication(false, false);
+ }
+
+ public static MyApplication newStartException() {
+ return new MyApplication(true, false);
+ }
+
+ public static MyApplication newStopException() {
+ return new MyApplication(false, true);
+ }
+ }
+
+ private static class MyRequestHandler extends AbstractRequestHandler {
+
+ ResponseHandler responseHandler;
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ responseHandler = handler;
+ return new MyContentChannel();
+ }
+ }
+
+ private static class MyResponseHandler implements ResponseHandler {
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ return new MyContentChannel();
+ }
+ }
+
+ private static class MyContentChannel implements ContentChannel {
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ if (handler != null) {
+ handler.completed();
+ }
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ if (handler != null) {
+ handler.completed();
+ }
+ }
+ }
+
+ private static class MyException extends RuntimeException {
+
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationRestartTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationRestartTestCase.java
new file mode 100644
index 00000000000..2943e44bc4c
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationRestartTestCase.java
@@ -0,0 +1,153 @@
+// 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.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.Application;
+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.test.NonWorkingOsgiFramework;
+import org.junit.Test;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ApplicationRestartTestCase {
+
+ @Test
+ public void requireThatStopStartDoesNotBreakShutdown() throws Exception {
+ ApplicationLoader loader = newApplicationLoader();
+ loader.init(null, false);
+ loader.start();
+ assertGracefulStop(loader);
+ loader.start();
+ assertGracefulStop(loader);
+ loader.destroy();
+ }
+
+ @Test
+ public void requireThatDestroyInitDoesNotBreakShutdown() throws Exception {
+ ApplicationLoader loader = newApplicationLoader();
+ loader.init(null, false);
+ loader.start();
+ assertGracefulStop(loader);
+ loader.destroy();
+ loader.init(null, false);
+ loader.start();
+ assertGracefulStop(loader);
+ loader.destroy();
+ }
+
+ private static ApplicationLoader newApplicationLoader() {
+ return new ApplicationLoader(new NonWorkingOsgiFramework(),
+ Arrays.asList(new AbstractModule() {
+ @Override
+ public void configure() {
+ bind(Application.class).to(SimpleApplication.class);
+ }
+ }));
+ }
+
+ private static void assertGracefulStop(ApplicationLoader loader) throws Exception {
+ MyRequestHandler requestHandler = new MyRequestHandler();
+ ContainerBuilder builder = loader.newContainerBuilder();
+ builder.serverBindings().bind("http://host/path", requestHandler);
+ loader.activateContainer(builder);
+
+ MyResponseHandler responseHandler = new MyResponseHandler();
+ Request request = new Request(loader, URI.create("http://host/path"));
+ request.connect(responseHandler).close(null);
+ request.release();
+
+ StopTask task = new StopTask(loader);
+ task.start();
+ assertFalse(task.latch.await(100, TimeUnit.MILLISECONDS));
+ requestHandler.responseHandler.handleResponse(new Response(Response.Status.OK)).close(null);
+ assertTrue(task.latch.await(600, TimeUnit.SECONDS));
+ }
+
+ private static class StopTask extends Thread {
+
+ final ApplicationLoader loader;
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ StopTask(ApplicationLoader loader) {
+ this.loader = loader;
+ }
+
+ @Override
+ public void run() {
+ try {
+ loader.stop();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return;
+ }
+ latch.countDown();
+ }
+ }
+
+ private static class SimpleApplication implements Application {
+
+ @Override
+ public void start() {
+
+ }
+
+ @Override
+ public void stop() {
+
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+ }
+
+ private static class MyRequestHandler extends AbstractRequestHandler {
+
+ ResponseHandler responseHandler;
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ this.responseHandler = handler;
+ return new MyContentChannel();
+ }
+ }
+
+ private static class MyResponseHandler implements ResponseHandler {
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ return new MyContentChannel();
+ }
+ }
+
+ private static class MyContentChannel implements ContentChannel {
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ handler.completed();
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationShutdownTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationShutdownTestCase.java
new file mode 100644
index 00000000000..986ceddb3e4
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ApplicationShutdownTestCase.java
@@ -0,0 +1,122 @@
+// 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.Request;
+import com.yahoo.jdisc.Response;
+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.RequestHandler;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ApplicationShutdownTestCase {
+
+ @Test
+ public void requireThatStopWaitsForPreviousContainer() throws Exception {
+ Context ctx = new Context();
+ MyRequestHandler requestHandler = new MyRequestHandler();
+ ctx.activateContainer(requestHandler);
+ ctx.dispatchRequest();
+ ctx.activateContainer(null);
+ ctx.driver.scheduleClose();
+ assertFalse(ctx.driver.awaitClose(100, TimeUnit.MILLISECONDS));
+ requestHandler.respond();
+ assertTrue(ctx.driver.awaitClose(600, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void requireThatStopWaitsForAllPreviousContainers() {
+ Context ctx = new Context();
+ MyRequestHandler requestHandlerA = new MyRequestHandler();
+ ctx.activateContainer(requestHandlerA);
+ ctx.dispatchRequest();
+
+ MyRequestHandler requestHandlerB = new MyRequestHandler();
+ ctx.activateContainer(requestHandlerB);
+ ctx.dispatchRequest();
+
+ MyRequestHandler requestHandlerC = new MyRequestHandler();
+ ctx.activateContainer(requestHandlerC);
+ ctx.dispatchRequest();
+
+ ctx.driver.scheduleClose();
+ assertFalse(ctx.driver.awaitClose(100, TimeUnit.MILLISECONDS));
+ requestHandlerB.respond();
+ assertFalse(ctx.driver.awaitClose(100, TimeUnit.MILLISECONDS));
+ requestHandlerC.respond();
+ assertFalse(ctx.driver.awaitClose(100, TimeUnit.MILLISECONDS));
+ requestHandlerA.respond();
+ assertTrue(ctx.driver.awaitClose(600, TimeUnit.SECONDS));
+ }
+
+ private static class Context {
+
+ final TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+
+ void activateContainer(RequestHandler requestHandler) {
+ ContainerBuilder builder;
+ if (requestHandler != null) {
+ builder = driver.newContainerBuilder();
+ builder.serverBindings().bind("http://host/path", requestHandler);
+ } else {
+ builder = null;
+ }
+ driver.activateContainer(builder);
+ }
+
+ void dispatchRequest() {
+ Request request = new Request(driver, URI.create("http://host/path"));
+ request.connect(new MyResponseHandler()).close(null);
+ request.release();
+ }
+ }
+
+ private static class MyRequestHandler extends AbstractRequestHandler {
+
+ ResponseHandler handler = null;
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ this.handler = handler;
+ return new MyContent();
+ }
+
+ void respond() {
+ handler.handleResponse(new Response(Response.Status.OK)).close(null);
+ }
+ }
+
+ private static class MyResponseHandler implements ResponseHandler {
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ return new MyContent();
+ }
+ }
+
+ private static class MyContent implements ContentChannel {
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ handler.completed();
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/BootstrapDaemonTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/BootstrapDaemonTestCase.java
new file mode 100644
index 00000000000..bc80358b760
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/BootstrapDaemonTestCase.java
@@ -0,0 +1,154 @@
+// 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.commons.daemon.DaemonContext;
+import org.apache.commons.daemon.DaemonController;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class BootstrapDaemonTestCase {
+
+ @Test
+ public void requireThatPrivilegedLifecycleWorks() throws Exception {
+ MyLoader loader = new MyLoader();
+ BootstrapDaemon daemon = new BootstrapDaemon(loader, true);
+ daemon.init(new MyContext("foo"));
+ assertTrue(loader.hasState(true, false, false, false));
+ assertTrue(loader.privileged);
+ daemon.start();
+ assertTrue(loader.hasState(true, true, false, false));
+ daemon.stop();
+ assertTrue(loader.hasState(true, true, true, false));
+ daemon.destroy();
+ assertTrue(loader.hasState(true, true, true, true));
+ }
+
+ @Test
+ public void requireThatNonPrivilegedLifecycleWorks() throws Exception {
+ MyLoader loader = new MyLoader();
+ BootstrapDaemon daemon = new BootstrapDaemon(loader, false);
+ daemon.init(new MyContext("foo"));
+ assertTrue(loader.hasState(false, false, false, false));
+ daemon.start();
+ assertTrue(loader.hasState(true, true, false, false));
+ assertFalse(loader.privileged);
+ daemon.stop();
+ assertTrue(loader.hasState(true, true, true, false));
+ daemon.destroy();
+ assertTrue(loader.hasState(true, true, true, true));
+ }
+
+ @Test
+ public void requireThatBundleLocationIsRequired() throws Exception {
+ MyLoader loader = new MyLoader();
+ BootstrapDaemon daemon = new BootstrapDaemon(loader, true);
+ try {
+ daemon.init(new MyContext((String[])null));
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertNull(loader.bundleLocation);
+ }
+ try {
+ daemon.init(new MyContext());
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertNull(loader.bundleLocation);
+ }
+ try {
+ daemon.init(new MyContext((String)null));
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertNull(loader.bundleLocation);
+ }
+ try {
+ daemon.init(new MyContext("foo", "bar"));
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertNull(loader.bundleLocation);
+ }
+
+ daemon.init(new MyContext("foo"));
+ daemon.start();
+
+ assertNotNull(loader.bundleLocation);
+ assertEquals("foo", loader.bundleLocation);
+
+ daemon.stop();
+ daemon.destroy();
+ }
+
+ @Test
+ public void requireThatEnvironmentIsRequired() {
+ try {
+ new BootstrapDaemon();
+ fail();
+ } catch (IllegalStateException e) {
+
+ }
+ }
+
+ private static class MyLoader implements BootstrapLoader {
+
+ String bundleLocation = null;
+ boolean privileged = false;
+ boolean initCalled = false;
+ boolean startCalled = false;
+ boolean stopCalled = false;
+ boolean destroyCalled = false;
+
+ boolean hasState(boolean initCalled, boolean startCalled, boolean stopCalled, boolean destroyCalled) {
+ return this.initCalled == initCalled && this.startCalled == startCalled &&
+ this.stopCalled == stopCalled && this.destroyCalled == destroyCalled;
+ }
+
+ @Override
+ public void init(String bundleLocation, boolean privileged) throws Exception {
+ this.bundleLocation = bundleLocation;
+ this.privileged = privileged;
+ initCalled = true;
+ }
+
+ @Override
+ public void start() throws Exception {
+ startCalled = true;
+ }
+
+ @Override
+ public void stop() throws Exception {
+ stopCalled = true;
+ }
+
+ @Override
+ public void destroy() {
+ destroyCalled = true;
+ }
+ }
+
+ private static class MyContext implements DaemonContext {
+
+ final String[] args;
+
+ MyContext(String... args) {
+ this.args = args;
+ }
+
+ @Override
+ public DaemonController getController() {
+ return null;
+ }
+
+ @Override
+ public String[] getArguments() {
+ return args;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/BundleLocationResolverTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/BundleLocationResolverTestCase.java
new file mode 100644
index 00000000000..843ca91db68
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/BundleLocationResolverTestCase.java
@@ -0,0 +1,87 @@
+// 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.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class BundleLocationResolverTestCase {
+
+ @Test
+ public void requireThatDollarsAreIncludedInLocation() {
+ assertLocation("scheme:$foo", "scheme:$foo");
+ assertLocation("scheme:foo$bar", "scheme:foo$bar");
+ }
+
+ @Test
+ public void requireThatCurlyBracesAreIncludedInLocation() {
+ assertLocation("scheme:{foo", "scheme:{foo");
+ assertLocation("scheme:foo{", "scheme:foo{");
+ assertLocation("scheme:foo{bar", "scheme:foo{bar");
+ assertLocation("scheme:}foo", "scheme:}foo");
+ assertLocation("scheme:foo}", "scheme:foo}");
+ assertLocation("scheme:foo}bar", "scheme:foo}bar");
+ assertLocation("scheme:{foo}bar", "scheme:{foo}bar");
+ assertLocation("scheme:foo{bar}", "scheme:foo{bar}");
+ }
+
+ @Test
+ public void requireThatUnterminatedPropertiesAreIncludedInLocation() {
+ assertLocation("scheme:${foo", "scheme:${foo");
+ assertLocation("scheme:foo${", "scheme:foo${");
+ assertLocation("scheme:foo${bar", "scheme:foo${bar");
+ }
+
+ @Test
+ public void requireThatAllSystemPropertiesAreExpanded() throws IOException {
+ assertCanonicalPath("", "${foo}");
+ assertCanonicalPath("barcox", "${foo}bar${baz}cox");
+ assertCanonicalPath("foobaz", "foo${bar}baz${cox}");
+
+ System.setProperty("requireThatAllSystemPropertiesAreExpanded.foo", "FOO");
+ System.setProperty("requireThatAllSystemPropertiesAreExpanded.bar", "BAR");
+ System.setProperty("requireThatAllSystemPropertiesAreExpanded.baz", "BAZ");
+ System.setProperty("requireThatAllSystemPropertiesAreExpanded.cox", "COX");
+ assertCanonicalPath("FOO", "${requireThatAllSystemPropertiesAreExpanded.foo}");
+ assertCanonicalPath("FOObarBAZcox", "${requireThatAllSystemPropertiesAreExpanded.foo}bar" +
+ "${requireThatAllSystemPropertiesAreExpanded.baz}cox");
+ assertCanonicalPath("fooBARbazCOX", "foo${requireThatAllSystemPropertiesAreExpanded.bar}" +
+ "baz${requireThatAllSystemPropertiesAreExpanded.cox}");
+ }
+
+ @Test
+ public void requireThatUnschemedLocationsAreExpandedToBundleLocationProperty() throws IOException {
+ assertCanonicalPath(BundleLocationResolver.BUNDLE_PATH + "foo", "foo");
+ }
+
+ @Test
+ public void requireThatFileSchemedLocationsAreCanonicalized() throws IOException {
+ assertCanonicalPath("", "file:");
+ assertCanonicalPath("foo", "file:foo");
+ assertCanonicalPath("foo", "file:./foo");
+ assertCanonicalPath("foo/bar", "file:foo/bar");
+ assertCanonicalPath("foo/bar", "file:./foo/../foo/./bar");
+ assertCanonicalPath("foo", " \f\n\r\tfile:foo");
+ }
+
+ @Test
+ public void requireThatOtherSchemedLocationsAreUntouched() {
+ assertLocation("foo:", "foo:");
+ assertLocation("foo:bar", "foo:bar");
+ assertLocation("foo:bar/baz", "foo:bar/baz");
+ }
+
+ private static void assertCanonicalPath(String expected, String bundleLocation) throws IOException {
+ assertLocation("file:" + new File(expected).getCanonicalPath(), bundleLocation);
+ }
+
+ private static void assertLocation(String expected, String bundleLocation) {
+ assertEquals(expected, BundleLocationResolver.resolve(bundleLocation));
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogFormatterTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogFormatterTestCase.java
new file mode 100644
index 00000000000..901817dbd26
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogFormatterTestCase.java
@@ -0,0 +1,270 @@
+// 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.junit.Test;
+import org.mockito.Mockito;
+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;
+
+import static org.junit.Assert.assertEquals;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class ConsoleLogFormatterTestCase {
+
+ private static final ConsoleLogFormatter SIMPLE_FORMATTER = new ConsoleLogFormatter(null, null, null);
+ private static final LogEntry SIMPLE_ENTRY = new MyEntry(0, 0, null);
+
+ // TODO: Should (at least) use ConsoleLogFormatter.ABSENCE_REPLACEMENT instead of literal '-'. See ticket 7128315.
+
+ @Test
+ public void requireThatMillisecondsArePadded() {
+ for (int i = 0; i < 10000; ++i) {
+ LogEntry entry = new MyEntry(i, 0, null);
+ assertEquals(String.format("%d.%03d\t-\t-\t-\t-\tunknown\t", i / 1000, i % 1000),
+ SIMPLE_FORMATTER.formatEntry(entry));
+ }
+ }
+
+ @Test
+ public void requireThatHostNameIsIncluded() {
+ assertEquals("0.000\thostName\t-\t-\t-\tunknown\t",
+ new ConsoleLogFormatter("hostName", null, null).formatEntry(SIMPLE_ENTRY));
+ }
+
+ @Test
+ public void requireThatHostNameIsOptional() {
+ assertEquals("0.000\t-\t-\t-\t-\tunknown\t",
+ new ConsoleLogFormatter(null, null, null).formatEntry(SIMPLE_ENTRY));
+ assertEquals("0.000\t-\t-\t-\t-\tunknown\t",
+ new ConsoleLogFormatter("", null, null).formatEntry(SIMPLE_ENTRY));
+ assertEquals("0.000\t-\t-\t-\t-\tunknown\t",
+ new ConsoleLogFormatter(" ", null, null).formatEntry(SIMPLE_ENTRY));
+ }
+
+ @Test
+ public void requireThatProcessIdIsIncluded() {
+ assertEquals("0.000\t-\tprocessId\t-\t-\tunknown\t",
+ new ConsoleLogFormatter(null, "processId", null).formatEntry(SIMPLE_ENTRY));
+ }
+
+ @Test
+ public void requireThatProcessIdIsOptional() {
+ assertEquals("0.000\t-\t-\t-\t-\tunknown\t",
+ new ConsoleLogFormatter(null, null, null).formatEntry(SIMPLE_ENTRY));
+ assertEquals("0.000\t-\t-\t-\t-\tunknown\t",
+ new ConsoleLogFormatter(null, "", null).formatEntry(SIMPLE_ENTRY));
+ assertEquals("0.000\t-\t-\t-\t-\tunknown\t",
+ new ConsoleLogFormatter(null, " ", null).formatEntry(SIMPLE_ENTRY));
+ }
+
+ @Test
+ public void requireThatProcessIdIncludesThreadIdWhenAvailable() {
+ LogEntry entry = new MyEntry(0, 0, null).putProperty("THREAD_ID", "threadId");
+ assertEquals("0.000\t-\tprocessId/threadId\t-\t-\tunknown\t",
+ new ConsoleLogFormatter(null, "processId", null).formatEntry(entry));
+ }
+
+ @Test
+ public void requireThatServiceNameIsIncluded() {
+ assertEquals("0.000\t-\t-\tserviceName\t-\tunknown\t",
+ new ConsoleLogFormatter(null, null, "serviceName").formatEntry(SIMPLE_ENTRY));
+ }
+
+ @Test
+ public void requireThatServiceNameIsOptional() {
+ assertEquals("0.000\t-\t-\t-\t-\tunknown\t",
+ new ConsoleLogFormatter(null, null, null).formatEntry(SIMPLE_ENTRY));
+ assertEquals("0.000\t-\t-\t-\t-\tunknown\t",
+ new ConsoleLogFormatter(null, null, "").formatEntry(SIMPLE_ENTRY));
+ assertEquals("0.000\t-\t-\t-\t-\tunknown\t",
+ new ConsoleLogFormatter(null, null, " ").formatEntry(SIMPLE_ENTRY));
+ }
+
+ @Test
+ public void requireThatBundleNameIsIncluded() {
+ LogEntry entry = new MyEntry(0, 0, null).setBundleSymbolicName("bundleName");
+ assertEquals("0.000\t-\t-\t-\tbundleName\tunknown\t",
+ SIMPLE_FORMATTER.formatEntry(entry));
+ }
+
+ @Test
+ public void requireThatBundleNameIsOptional() {
+ assertEquals("0.000\t-\t-\t-\t-\tunknown\t",
+ SIMPLE_FORMATTER.formatEntry(SIMPLE_ENTRY));
+ }
+
+ @Test
+ public void requireThatLoggerNameIsIncluded() {
+ LogEntry entry = new MyEntry(0, 0, null).putProperty("LOGGER_NAME", "loggerName");
+ assertEquals("0.000\t-\t-\t-\t/loggerName\tunknown\t",
+ SIMPLE_FORMATTER.formatEntry(entry));
+ }
+
+ @Test
+ public void requireThatLoggerNameIsOptional() {
+ assertEquals("0.000\t-\t-\t-\t-\tunknown\t",
+ SIMPLE_FORMATTER.formatEntry(SIMPLE_ENTRY));
+ }
+
+ @Test
+ public void requireThatBundleAndLoggerNameIsCombined() {
+ LogEntry entry = new MyEntry(0, 0, null).setBundleSymbolicName("bundleName")
+ .putProperty("LOGGER_NAME", "loggerName");
+ assertEquals("0.000\t-\t-\t-\tbundleName/loggerName\tunknown\t",
+ SIMPLE_FORMATTER.formatEntry(entry));
+ }
+
+ @Test
+ public void requireThatLevelNameIsIncluded() {
+ ConsoleLogFormatter formatter = SIMPLE_FORMATTER;
+ assertEquals("0.000\t-\t-\t-\t-\terror\t",
+ formatter.formatEntry(new MyEntry(0, LogService.LOG_ERROR, null)));
+ assertEquals("0.000\t-\t-\t-\t-\twarning\t",
+ formatter.formatEntry(new MyEntry(0, LogService.LOG_WARNING, null)));
+ assertEquals("0.000\t-\t-\t-\t-\tinfo\t",
+ formatter.formatEntry(new MyEntry(0, LogService.LOG_INFO, null)));
+ assertEquals("0.000\t-\t-\t-\t-\tdebug\t",
+ formatter.formatEntry(new MyEntry(0, LogService.LOG_DEBUG, null)));
+ assertEquals("0.000\t-\t-\t-\t-\tunknown\t",
+ formatter.formatEntry(new MyEntry(0, 69, null)));
+ }
+
+ @Test
+ public void requireThatMessageIsIncluded() {
+ LogEntry entry = new MyEntry(0, 0, "message");
+ assertEquals("0.000\t-\t-\t-\t-\tunknown\tmessage",
+ SIMPLE_FORMATTER.formatEntry(entry));
+ }
+
+ @Test
+ public void requireThatMessageIsOptional() {
+ LogEntry entry = new MyEntry(0, 0, null);
+ assertEquals("0.000\t-\t-\t-\t-\tunknown\t",
+ SIMPLE_FORMATTER.formatEntry(entry));
+ }
+
+ @Test
+ public void requireThatMessageIsEscaped() {
+ LogEntry entry = new MyEntry(0, 0, "\\\n\r\t");
+ assertEquals("0.000\t-\t-\t-\t-\tunknown\t\\\\\\n\\r\\t",
+ SIMPLE_FORMATTER.formatEntry(entry));
+ }
+
+ @Test
+ public void requireThatExceptionIsIncluded() {
+ Throwable t = new Throwable();
+ LogEntry entry = new MyEntry(0, 0, null).setException(t);
+ assertEquals("0.000\t-\t-\t-\t-\tunknown\t\\n" + formatThrowable(t),
+ SIMPLE_FORMATTER.formatEntry(entry));
+ }
+
+ @Test
+ public void requireThatExceptionIsEscaped() {
+ Throwable t = new Throwable("\\\n\r\t");
+ LogEntry entry = new MyEntry(0, 0, null).setException(t);
+ assertEquals("0.000\t-\t-\t-\t-\tunknown\t\\n" + formatThrowable(t),
+ SIMPLE_FORMATTER.formatEntry(entry));
+ }
+
+ @Test
+ public void requireThatExceptionIsSimplifiedForInfoEntries() {
+ Throwable t = new Throwable("exception");
+ LogEntry entry = new MyEntry(0, LogService.LOG_INFO, "entry").setException(t);
+ assertEquals("0.000\t-\t-\t-\t-\tinfo\tentry: exception",
+ SIMPLE_FORMATTER.formatEntry(entry));
+ }
+
+ @Test
+ public void requireThatSimplifiedExceptionIsEscaped() {
+ Throwable t = new Throwable("\\\n\r\t");
+ LogEntry entry = new MyEntry(0, LogService.LOG_INFO, "entry").setException(t);
+ assertEquals("0.000\t-\t-\t-\t-\tinfo\tentry: \\\\\\n\\r\\t",
+ SIMPLE_FORMATTER.formatEntry(entry));
+ }
+
+ @Test
+ public void requireThatSimplifiedExceptionMessageIsOptional() {
+ Throwable t = new Throwable();
+ LogEntry entry = new MyEntry(0, LogService.LOG_INFO, "entry").setException(t);
+ assertEquals("0.000\t-\t-\t-\t-\tinfo\tentry: java.lang.Throwable",
+ SIMPLE_FORMATTER.formatEntry(entry));
+ }
+
+ private static String formatThrowable(Throwable t) {
+ Writer out = new StringWriter();
+ t.printStackTrace(new PrintWriter(out));
+ return out.toString().replace("\\", "\\\\").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
+ }
+
+ private static class MyEntry implements LogEntry {
+
+ final String message;
+ final int level;
+ final long time;
+ Bundle bundle = null;
+ ServiceReference serviceReference = null;
+ Throwable exception;
+
+ MyEntry(long time, int level, String message) {
+ this.message = message;
+ this.level = level;
+ this.time = time;
+ }
+
+ MyEntry setBundleSymbolicName(String symbolicName) {
+ this.bundle = Mockito.mock(Bundle.class);
+ Mockito.doReturn(symbolicName).when(this.bundle).getSymbolicName();
+ return this;
+ }
+
+ MyEntry setException(Throwable exception) {
+ this.exception = exception;
+ return this;
+ }
+
+ MyEntry putProperty(String key, String val) {
+ this.serviceReference = Mockito.mock(ServiceReference.class);
+ Mockito.doReturn(val).when(this.serviceReference).getProperty(key);
+ return this;
+ }
+
+ @Override
+ public long getTime() {
+ return time;
+ }
+
+ @Override
+ public int getLevel() {
+ return level;
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ @Override
+ public Throwable getException() {
+ return exception;
+ }
+
+ @Override
+ public Bundle getBundle() {
+ return bundle;
+ }
+
+ @Override
+ public ServiceReference getServiceReference() {
+ return serviceReference;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogListenerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogListenerTestCase.java
new file mode 100644
index 00000000000..3ac30f0456e
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogListenerTestCase.java
@@ -0,0 +1,115 @@
+// 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.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.log.LogEntry;
+import org.osgi.service.log.LogListener;
+import org.osgi.service.log.LogService;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a>
+ */
+public class ConsoleLogListenerTestCase {
+
+ private static final String HOSTNAME = ConsoleLogFormatter.formatOptional(ConsoleLogListener.getHostname());
+ private static final String PROCESS_ID = ConsoleLogListener.getProcessId();
+
+ @Test
+ public void requireThatLogLevelParserKnowsOsgiLogLevels() {
+ assertEquals(LogService.LOG_ERROR, ConsoleLogListener.parseLogLevel("ERROR"));
+ assertEquals(LogService.LOG_WARNING, ConsoleLogListener.parseLogLevel("WARNING"));
+ assertEquals(LogService.LOG_INFO, ConsoleLogListener.parseLogLevel("INFO"));
+ assertEquals(LogService.LOG_DEBUG, ConsoleLogListener.parseLogLevel("DEBUG"));
+ }
+
+ @Test
+ public void requireThatLogLevelParserKnowsOff() {
+ assertEquals(Integer.MIN_VALUE, ConsoleLogListener.parseLogLevel("OFF"));
+ }
+
+ @Test
+ public void requireThatLogLevelParserKnowsAll() {
+ assertEquals(Integer.MAX_VALUE, ConsoleLogListener.parseLogLevel("ALL"));
+ }
+
+ @Test
+ public void requireThatLogLevelParserKnowsIntegers() {
+ for (int i = -69; i < 69; ++i) {
+ assertEquals(i, ConsoleLogListener.parseLogLevel(String.valueOf(i)));
+ }
+ }
+
+ @Test
+ public void requireThatLogLevelParserErrorsReturnDefault() {
+ assertEquals(ConsoleLogListener.DEFAULT_LOG_LEVEL, ConsoleLogListener.parseLogLevel(null));
+ assertEquals(ConsoleLogListener.DEFAULT_LOG_LEVEL, ConsoleLogListener.parseLogLevel(""));
+ assertEquals(ConsoleLogListener.DEFAULT_LOG_LEVEL, ConsoleLogListener.parseLogLevel("foo"));
+ }
+
+ @Test
+ public void requireThatLogEntryWithLevelAboveThresholdIsNotOutput() {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ LogListener listener = new ConsoleLogListener(new PrintStream(out), null, "5");
+ for (int i = 0; i < 10; ++i) {
+ listener.logged(new MyEntry(0, i, "message"));
+ }
+ // TODO: Should use ConsoleLogFormatter.ABSENCE_REPLACEMENT instead of literal '-'. See ticket 7128315.
+ assertEquals("0.000\t" + HOSTNAME + "\t" + PROCESS_ID + "\t-\t-\tunknown\tmessage\n" +
+ "0.000\t" + HOSTNAME + "\t" + PROCESS_ID + "\t-\t-\terror\tmessage\n" +
+ "0.000\t" + HOSTNAME + "\t" + PROCESS_ID + "\t-\t-\twarning\tmessage\n" +
+ "0.000\t" + HOSTNAME + "\t" + PROCESS_ID + "\t-\t-\tinfo\tmessage\n" +
+ "0.000\t" + HOSTNAME + "\t" + PROCESS_ID + "\t-\t-\tdebug\tmessage\n" +
+ "0.000\t" + HOSTNAME + "\t" + PROCESS_ID + "\t-\t-\tunknown\tmessage\n",
+ out.toString());
+ }
+
+ private static class MyEntry implements LogEntry {
+
+ final String message;
+ final int level;
+ final long time;
+
+ MyEntry(long time, int level, String message) {
+ this.message = message;
+ this.level = level;
+ this.time = time;
+ }
+
+ @Override
+ public long getTime() {
+ return time;
+ }
+
+ @Override
+ public int getLevel() {
+ return level;
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ @Override
+ public Throwable getException() {
+ return null;
+ }
+
+ @Override
+ public Bundle getBundle() {
+ return null;
+ }
+
+ @Override
+ public ServiceReference getServiceReference() {
+ return null;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogManagerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogManagerTestCase.java
new file mode 100644
index 00000000000..5bc1a29e2f7
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ConsoleLogManagerTestCase.java
@@ -0,0 +1,90 @@
+// 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.test.TestDriver;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.service.log.LogListener;
+import org.osgi.service.log.LogReaderService;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class ConsoleLogManagerTestCase {
+
+ @Test
+ public void requireThatManagerCanNotBeInstalledTwice() throws BundleException {
+ FelixFramework felix = TestDriver.newOsgiFramework();
+ felix.start();
+
+ ConsoleLogManager manager = new ConsoleLogManager();
+ manager.install(felix.bundleContext());
+ try {
+ manager.install(felix.bundleContext());
+ fail();
+ } catch (IllegalStateException e) {
+ assertEquals("ConsoleLogManager already installed.", e.getMessage());
+ }
+
+ felix.stop();
+ }
+
+ @Test
+ public void requireThatManagerCanBeUninstalledTwice() throws BundleException {
+ FelixFramework felix = TestDriver.newOsgiFramework();
+ felix.start();
+
+ ConsoleLogManager manager = new ConsoleLogManager();
+ assertFalse(manager.uninstall());
+ manager.install(felix.bundleContext());
+ assertTrue(manager.uninstall());
+ assertFalse(manager.uninstall());
+
+ felix.stop();
+ }
+
+ @Test
+ public void requireThatLogReaderServicesAreTracked() throws BundleException {
+ FelixFramework felix = TestDriver.newOsgiFramework();
+ felix.start();
+ BundleContext ctx = felix.bundleContext();
+
+ LogReaderService foo = Mockito.mock(LogReaderService.class);
+ ctx.registerService(LogReaderService.class.getName(), foo, null);
+ Mockito.verify(foo).addLogListener(Mockito.any(LogListener.class));
+
+ LogReaderService bar = Mockito.mock(LogReaderService.class);
+ ctx.registerService(LogReaderService.class.getName(), bar, null);
+ Mockito.verify(bar).addLogListener(Mockito.any(LogListener.class));
+
+ ConsoleLogManager manager = new ConsoleLogManager();
+ manager.install(felix.bundleContext());
+
+ Mockito.verify(foo, Mockito.times(2)).addLogListener(Mockito.any(LogListener.class));
+ Mockito.verify(bar, Mockito.times(2)).addLogListener(Mockito.any(LogListener.class));
+
+ LogReaderService baz = Mockito.mock(LogReaderService.class);
+ ctx.registerService(LogReaderService.class.getName(), baz, null);
+ Mockito.verify(baz, Mockito.times(2)).addLogListener(Mockito.any(LogListener.class));
+
+ assertTrue(manager.uninstall());
+
+ Mockito.verify(foo).removeLogListener(Mockito.any(LogListener.class));
+ Mockito.verify(bar).removeLogListener(Mockito.any(LogListener.class));
+ Mockito.verify(baz).removeLogListener(Mockito.any(LogListener.class));
+
+ felix.stop();
+
+ Mockito.verify(foo, Mockito.times(2)).removeLogListener(Mockito.any(LogListener.class));
+ Mockito.verify(bar, Mockito.times(2)).removeLogListener(Mockito.any(LogListener.class));
+ Mockito.verify(baz, Mockito.times(2)).removeLogListener(Mockito.any(LogListener.class));
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerResourceTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerResourceTestCase.java
new file mode 100644
index 00000000000..11b6f27296d
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerResourceTestCase.java
@@ -0,0 +1,162 @@
+// 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.Container;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.ResourceReference;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.service.ServerProvider;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ContainerResourceTestCase {
+
+ @Test
+ public void requireThatBoundRequestHandlersAreRetainedOnActivate() {
+ MyRequestHandler foo = new MyRequestHandler();
+ MyRequestHandler bar = new MyRequestHandler();
+
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings("foo").bind("http://foo/", foo);
+ builder.serverBindings("bar").bind("http://bar/", bar);
+ assertEquals(0, foo.retainCnt.get());
+ assertEquals(0, bar.retainCnt.get());
+
+ driver.activateContainer(builder);
+ assertEquals(1, foo.retainCnt.get());
+ assertEquals(1, bar.retainCnt.get());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatBoundRequestHandlersAreReleasedOnTermination() {
+ MyRequestHandler handler = new MyRequestHandler();
+
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind("http://localhost/", handler);
+ driver.activateContainer(builder);
+
+ Container container = driver.newReference(URI.create("http://localhost/"));
+ assertEquals(1, handler.retainCnt.get());
+ driver.activateContainer(null);
+ assertEquals(1, handler.retainCnt.get());
+ container.release();
+ assertEquals(0, handler.retainCnt.get());
+
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatServerProvidersAreRetainedOnActivate() {
+ MyServerProvider foo = new MyServerProvider();
+ MyServerProvider bar = new MyServerProvider();
+
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverProviders().install(foo);
+ builder.serverProviders().install(bar);
+ assertEquals(0, foo.retainCnt.get());
+ assertEquals(0, bar.retainCnt.get());
+
+ driver.activateContainer(builder);
+ assertEquals(1, foo.retainCnt.get());
+ assertEquals(1, bar.retainCnt.get());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatServerProvidersAreReleasedOnTermination() {
+ MyServerProvider server = new MyServerProvider();
+
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverProviders().install(server);
+ driver.activateContainer(builder);
+
+ Container container = driver.newReference(URI.create("http://localhost/"));
+ assertEquals(1, server.retainCnt.get());
+ driver.activateContainer(null);
+ assertEquals(1, server.retainCnt.get());
+ container.release();
+ assertEquals(0, server.retainCnt.get());
+
+ assertTrue(driver.close());
+ }
+
+ private static class MyRequestHandler implements RequestHandler {
+
+ final AtomicInteger retainCnt = new AtomicInteger(0);
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void handleTimeout(Request request, ResponseHandler handler) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ResourceReference refer() {
+ retainCnt.incrementAndGet();
+ return new ResourceReference() {
+ @Override
+ public void close() {
+ retainCnt.decrementAndGet();
+ }
+ };
+ }
+
+ @Override
+ public void release() {
+ retainCnt.decrementAndGet();
+ }
+ }
+
+ private static class MyServerProvider implements ServerProvider {
+
+ final AtomicInteger retainCnt = new AtomicInteger(0);
+
+ @Override
+ public void start() {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public ResourceReference refer() {
+ retainCnt.incrementAndGet();
+ return new ResourceReference() {
+ @Override
+ public void close() {
+ retainCnt.decrementAndGet();
+ }
+ };
+ }
+
+ @Override
+ public void release() {
+ retainCnt.decrementAndGet();
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerShutdownTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerShutdownTestCase.java
new file mode 100644
index 00000000000..488628867b4
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerShutdownTestCase.java
@@ -0,0 +1,848 @@
+// 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.Container;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+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.RequestHandler;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ContainerShutdownTestCase {
+
+ @Test
+ public void requireThatContainerBlocksTermination() {
+ Context ctx = Context.newInstance();
+ Container container = ctx.driver.newReference(URI.create("http://host/path"));
+ assertFalse(ctx.shutdown());
+ container.release();
+ assertTrue(ctx.terminated);
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatNewRequestBlocksTermination() {
+ Context ctx = Context.newPendingRequest(MyRequestHandler.newInstance());
+ assertFalse(ctx.shutdown());
+ ctx.request.release();
+ assertTrue(ctx.terminated);
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatOpenRequestBlocksTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion());
+ ctx.request.release();
+ requestHandler.respond().close(null);
+ assertFalse(ctx.shutdown());
+ requestContent.close(null);
+ assertTrue(ctx.terminated);
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatResponsePendingBlocksTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ ctx.request.connect(MyResponseHandler.newEagerCompletion()).close(null);
+ ctx.request.release();
+ assertFalse(ctx.shutdown());
+ requestHandler.respond().close(null);
+ assertTrue(ctx.terminated);
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatOpenResponseBlocksTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ ctx.request.connect(MyResponseHandler.newEagerCompletion()).close(null);
+ ctx.request.release();
+ ContentChannel responseContent = requestHandler.respond();
+ assertFalse(ctx.shutdown());
+ responseContent.close(null);
+ assertTrue(ctx.terminated);
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatRequestExceptionDoesNotBlockTermination() {
+ Context ctx = Context.newPendingRequest(MyRequestHandler.newRequestException());
+ try {
+ ctx.request.connect(MyResponseHandler.newEagerCompletion());
+ fail();
+ } catch (MyException e) {
+ // ignore
+ }
+ ctx.request.release();
+ assertTrue(ctx.shutdown());
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatRequestExceptionWithEagerHandleResponseBlocksTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newRequestExceptionWithEagerHandleResponse();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ try {
+ ctx.request.connect(MyResponseHandler.newEagerCompletion());
+ fail();
+ } catch (MyException e) {
+ // ignore
+ }
+ ctx.request.release();
+ assertFalse(ctx.shutdown());
+ requestHandler.responseContent.close(null);
+ assertTrue(ctx.terminated);
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatRequestExceptionWithEagerCloseResponseDoesNotBlockTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newRequestExceptionWithEagerCloseResponse();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ try {
+ ctx.request.connect(MyResponseHandler.newEagerCompletion());
+ fail();
+ } catch (MyException e) {
+ // ignore
+ }
+ ctx.request.release();
+ assertTrue(ctx.shutdown());
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatNullRequestContentBlocksTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newNullContent();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ ctx.request.connect(MyResponseHandler.newEagerCompletion()).close(null);
+ ctx.request.release();
+ assertFalse(ctx.shutdown());
+ requestHandler.respond();
+ requestHandler.responseContent.close(null);
+ assertTrue(ctx.terminated);
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatNullRequestContentWithEagerHandleResponseBlocksTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newNullContentWithEagerHandleResponse();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ ctx.request.connect(MyResponseHandler.newEagerCompletion()).close(null);
+ ctx.request.release();
+ assertFalse(ctx.shutdown());
+ requestHandler.responseContent.close(null);
+ assertTrue(ctx.terminated);
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatNullRequestContentWithEagerCloseResponseBlocksTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newNulContentWithEagerCloseResponse();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion());
+ ctx.request.release();
+ assertFalse(ctx.shutdown());
+ requestContent.close(null);
+ assertTrue(ctx.terminated);
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatRequestContentWriteFailedDoesNotBlockTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerFail();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion());
+ requestContent.write(ByteBuffer.allocate(69), null);
+ requestContent.close(null);
+ ctx.request.release();
+ requestHandler.respond().close(null);
+ assertTrue(ctx.shutdown());
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatRequestContentWriteExceptionDoesNotBlockTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newContentWriteExceptionWithEagerCompletion();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion());
+ try {
+ requestContent.write(ByteBuffer.allocate(69), null);
+ fail();
+ } catch (MyException e) {
+ // ignore
+ }
+ requestContent.close(null);
+ ctx.request.release();
+ requestHandler.respond().close(null);
+ assertTrue(ctx.shutdown());
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatRequestContentWriteExceptionDoesNotForceTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newContentWriteExceptionWithEagerCompletion();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion());
+ try {
+ requestContent.write(ByteBuffer.allocate(69), null);
+ fail();
+ } catch (MyException e) {
+ // ignore
+ }
+ ctx.request.release();
+ requestHandler.respond().close(null);
+ assertFalse(ctx.shutdown());
+ requestContent.close(null);
+ assertTrue(ctx.terminated);
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatRequestContentWriteExceptionWithCompletionDoesNotBlockTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newContentWriteExceptionWithEagerCompletion();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion());
+ try {
+ requestContent.write(ByteBuffer.allocate(69), MyCompletion.newInstance());
+ fail();
+ } catch (MyException e) {
+ // ignore
+ }
+ requestContent.close(null);
+ ctx.request.release();
+ requestHandler.respond().close(null);
+ assertTrue(ctx.shutdown());
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatRequestContentCloseFailedDoesNotBlockTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerFail();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion());
+ requestContent.close(null);
+ ctx.request.release();
+ requestHandler.respond().close(null);
+ assertTrue(ctx.shutdown());
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatRequestContentCloseExceptionDoesNotBlockTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newContentCloseException();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion());
+ try {
+ requestContent.close(null);
+ fail();
+ } catch (MyException e) {
+ // ignore
+ }
+ ctx.request.release();
+ requestHandler.respond().close(null);
+ assertTrue(ctx.shutdown());
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatRequestContentCloseExceptionWithCompletionDoesNotBlockTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newContentCloseException();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion());
+ try {
+ requestContent.close(MyCompletion.newInstance());
+ fail();
+ } catch (MyException e) {
+ // ignore
+ }
+ ctx.request.release();
+ requestHandler.respond().close(null);
+ assertTrue(ctx.shutdown());
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatRequestWriteCompletionBlocksTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCloseResponse();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion());
+ ctx.request.release();
+ requestContent.write(null, MyCompletion.newInstance());
+ requestContent.close(null);
+ requestHandler.requestContent.closeCompletion.completed();
+ assertFalse(ctx.shutdown());
+ requestHandler.requestContent.writeCompletion.completed();
+ assertTrue(ctx.terminated);
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatRequestWriteCompletionExceptionDoesNotBlockTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCloseResponse();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion());
+ ctx.request.release();
+ requestContent.write(null, MyCompletion.newException());
+ requestContent.close(null);
+ requestHandler.requestContent.closeCompletion.completed();
+ assertFalse(ctx.shutdown());
+ try {
+ requestHandler.requestContent.writeCompletion.completed();
+ fail();
+ } catch (MyException e) {
+ // ignore
+ }
+ assertTrue(ctx.terminated);
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatRequestCloseCompletionBlocksTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCloseResponse();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion());
+ ctx.request.release();
+ requestContent.close(MyCompletion.newInstance());
+ assertFalse(ctx.shutdown());
+ requestHandler.requestContent.closeCompletion.completed();
+ assertTrue(ctx.terminated);
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatRequestCloseCompletionExceptionDoesNotBlockTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCloseResponse();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ ContentChannel requestContent = ctx.request.connect(MyResponseHandler.newEagerCompletion());
+ ctx.request.release();
+ requestContent.close(MyCompletion.newException());
+ assertFalse(ctx.shutdown());
+ try {
+ requestHandler.requestContent.closeCompletion.completed();
+ fail();
+ } catch (MyException e) {
+ // ignore
+ }
+ assertTrue(ctx.terminated);
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatNullResponseContentBlocksTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerRespondWithEagerCompletion();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ MyResponseHandler responseHandler = MyResponseHandler.newNullContent();
+ ctx.request.connect(responseHandler).close(null);
+ ctx.request.release();
+
+ assertFalse(ctx.shutdown());
+ requestHandler.responseContent.close(MyCompletion.newInstance());
+ assertTrue(ctx.terminated);
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatResponseExceptionDoesNotBlockTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ ctx.request.connect(MyResponseHandler.newResponseException()).close(null);
+ ctx.request.release();
+ try {
+ requestHandler.respond();
+ fail();
+ } catch (MyException e) {
+ // ignore
+ }
+ assertTrue(ctx.shutdown());
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatResponseContentWriteFailedDoesNotBlockTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ MyResponseHandler responseHandler = MyResponseHandler.newEagerFail();
+ ctx.request.connect(responseHandler).close(null);
+ ctx.request.release();
+ requestHandler.respond();
+ requestHandler.responseContent.write(ByteBuffer.allocate(69), null);
+ requestHandler.responseContent.close(null);
+ assertTrue(ctx.shutdown());
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatResponseContentCloseFailedDoesNotBlockTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ MyResponseHandler responseHandler = MyResponseHandler.newEagerFail();
+ ctx.request.connect(responseHandler).close(null);
+ ctx.request.release();
+ requestHandler.respond();
+ requestHandler.responseContent.close(null);
+ assertTrue(ctx.shutdown());
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatResponseContentWriteExceptionDoesNotBlockTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ MyResponseHandler responseHandler = MyResponseHandler.newContentWriteException();
+ ctx.request.connect(responseHandler).close(null);
+ ctx.request.release();
+ requestHandler.respond();
+ try {
+ requestHandler.responseContent.write(ByteBuffer.allocate(69), null);
+ fail();
+ } catch (MyException e) {
+ // ignore
+ }
+ requestHandler.responseContent.close(null);
+ responseHandler.content.closeCompletion.completed();
+ assertTrue(ctx.shutdown());
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatResponseContentWriteExceptionDoesNotForceTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ MyResponseHandler responseHandler = MyResponseHandler.newContentWriteException();
+ ctx.request.connect(responseHandler).close(null);
+ ctx.request.release();
+ requestHandler.respond();
+ try {
+ requestHandler.responseContent.write(ByteBuffer.allocate(69), null);
+ fail();
+ } catch (MyException e) {
+ // ignore
+ }
+ assertFalse(ctx.shutdown());
+ requestHandler.responseContent.close(null);
+ responseHandler.content.closeCompletion.completed();
+ assertTrue(ctx.terminated);
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatResponseContentWriteExceptionWithCompletionDoesNotBlockTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ MyResponseHandler responseHandler = MyResponseHandler.newContentWriteException();
+ ctx.request.connect(responseHandler).close(null);
+ ctx.request.release();
+ requestHandler.respond();
+ try {
+ requestHandler.responseContent.write(ByteBuffer.allocate(69), MyCompletion.newInstance());
+ fail();
+ } catch (MyException e) {
+ // ignore
+ }
+ requestHandler.responseContent.close(null);
+ responseHandler.content.closeCompletion.completed();
+ assertTrue(ctx.shutdown());
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatResponseContentCloseExceptionDoesNotBlockTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ ctx.request.connect(MyResponseHandler.newContentCloseException()).close(null);
+ ctx.request.release();
+ try {
+ requestHandler.respond().close(null);
+ fail();
+ } catch (MyException e) {
+ // ignore
+ }
+ assertTrue(ctx.shutdown());
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatResponseContentCloseExceptionWithCompletionDoesNotBlockTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerCompletion();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ ctx.request.connect(MyResponseHandler.newContentCloseException()).close(null);
+ ctx.request.release();
+ try {
+ requestHandler.respond().close(MyCompletion.newInstance());
+ fail();
+ } catch (MyException e) {
+ // ignore
+ }
+ assertTrue(ctx.shutdown());
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatResponseWriteCompletionBlocksTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerRespondWithEagerCompletion();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ MyResponseHandler responseHandler = MyResponseHandler.newInstance();
+ ctx.request.connect(responseHandler).close(null);
+ ctx.request.release();
+
+ requestHandler.responseContent.write(null, MyCompletion.newInstance());
+ requestHandler.responseContent.close(null);
+ responseHandler.content.closeCompletion.completed();
+ assertFalse(ctx.shutdown());
+ responseHandler.content.writeCompletion.completed();
+ assertTrue(ctx.terminated);
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatResponseWriteCompletionExceptionDoesNotBlockTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerRespondWithEagerCompletion();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ MyResponseHandler responseHandler = MyResponseHandler.newInstance();
+ ctx.request.connect(responseHandler).close(null);
+ ctx.request.release();
+
+ requestHandler.responseContent.write(null, MyCompletion.newException());
+ requestHandler.responseContent.close(null);
+ responseHandler.content.closeCompletion.completed();
+ assertFalse(ctx.shutdown());
+ try {
+ responseHandler.content.writeCompletion.completed();
+ fail();
+ } catch (MyException e) {
+ // ignore
+ }
+ assertTrue(ctx.terminated);
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatResponseCloseCompletionBlocksTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerRespondWithEagerCompletion();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ MyResponseHandler responseHandler = MyResponseHandler.newInstance();
+ ctx.request.connect(responseHandler).close(null);
+ ctx.request.release();
+
+ requestHandler.responseContent.close(MyCompletion.newInstance());
+ assertFalse(ctx.shutdown());
+ responseHandler.content.closeCompletion.completed();
+ assertTrue(ctx.terminated);
+ assertTrue(ctx.driver.close());
+ }
+
+ @Test
+ public void requireThatResponseCloseCompletionExceptionDoesNotBlockTermination() {
+ MyRequestHandler requestHandler = MyRequestHandler.newEagerRespondWithEagerCompletion();
+ Context ctx = Context.newPendingRequest(requestHandler);
+ MyResponseHandler responseHandler = MyResponseHandler.newInstance();
+ ctx.request.connect(responseHandler).close(null);
+ ctx.request.release();
+
+ requestHandler.responseContent.close(MyCompletion.newException());
+ assertFalse(ctx.shutdown());
+ try {
+ responseHandler.content.closeCompletion.completed();
+ fail();
+ } catch (MyException e) {
+ // ignore
+ }
+ assertTrue(ctx.terminated);
+ assertTrue(ctx.driver.close());
+ }
+
+ private static class Context {
+
+ final TestDriver driver;
+ final Request request;
+ boolean terminated = false;
+
+ Context(TestDriver driver, Request request) {
+ this.driver = driver;
+ this.request = request;
+ }
+
+ boolean shutdown() {
+ driver.activateContainer(null).notifyTermination(new Runnable() {
+
+ @Override
+ public void run() {
+ terminated = true;
+ }
+ });
+ return terminated;
+ }
+
+ static Context newInstance() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ driver.activateContainer(driver.newContainerBuilder());
+ return new Context(driver, null);
+ }
+
+ static Context newPendingRequest(RequestHandler requestHandler) {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind("http://host/path", requestHandler);
+ driver.activateContainer(builder);
+ return new Context(driver, new Request(driver, URI.create("http://host/path")));
+ }
+ }
+
+ private static class MyCompletion implements CompletionHandler {
+
+ final boolean throwException;
+
+ MyCompletion(boolean throwException) {
+ this.throwException = throwException;
+ }
+
+ @Override
+ public void completed() {
+ if (throwException) {
+ throw new MyException();
+ }
+ }
+
+ @Override
+ public void failed(Throwable t) {
+ if (throwException) {
+ throw new MyException();
+ }
+ }
+
+ static MyCompletion newInstance() {
+ return new MyCompletion(false);
+ }
+
+ static MyCompletion newException() {
+ return new MyCompletion(true);
+ }
+ }
+
+ private static class MyContent implements ContentChannel {
+
+ final boolean eagerCompletion;
+ final boolean eagerFail;
+ final boolean writeException;
+ final boolean closeException;
+ CompletionHandler writeCompletion = null;
+ CompletionHandler closeCompletion = null;
+
+ MyContent(boolean eagerCompletion, boolean eagerFail, boolean writeException, boolean closeException) {
+ this.eagerCompletion = eagerCompletion;
+ this.eagerFail = eagerFail;
+ this.writeException = writeException;
+ this.closeException = closeException;
+ }
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ writeCompletion = handler;
+ if (eagerCompletion) {
+ writeCompletion.completed();
+ } else if (eagerFail) {
+ writeCompletion.failed(new MyException());
+ }
+ if (writeException) {
+ throw new MyException();
+ }
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ closeCompletion = handler;
+ if (eagerCompletion) {
+ closeCompletion.completed();
+ } else if (eagerFail) {
+ closeCompletion.failed(new MyException());
+ }
+ if (closeException) {
+ throw new MyException();
+ }
+ }
+
+ static MyContent newInstance() {
+ return new MyContent(false, false, false, false);
+ }
+
+ static MyContent newEagerCompletion() {
+ return new MyContent(true, false, false, false);
+ }
+
+ static MyContent newEagerFail() {
+ return new MyContent(false, true, false, false);
+ }
+
+ static MyContent newWriteException() {
+ return new MyContent(false, false, true, false);
+ }
+
+ static MyContent newWriteExceptionWithEagerCompletion() {
+ return new MyContent(true, false, true, false);
+ }
+
+ static MyContent newCloseException() {
+ return new MyContent(false, false, false, true);
+ }
+ }
+
+ private static class MyRequestHandler extends AbstractRequestHandler {
+
+ final MyContent requestContent;
+ final boolean eagerRespond;
+ final boolean closeResponse;
+ final boolean throwException;
+ ContentChannel responseContent = null;
+ ResponseHandler handler = null;
+
+ MyRequestHandler(MyContent requestContent, boolean eagerRespond, boolean closeResponse,
+ boolean throwException)
+ {
+ this.requestContent = requestContent;
+ this.eagerRespond = eagerRespond;
+ this.closeResponse = closeResponse;
+ this.throwException = throwException;
+ }
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ this.handler = handler;
+ if (eagerRespond) {
+ respond();
+ }
+ if (throwException) {
+ throw new MyException();
+ }
+ return requestContent;
+ }
+
+ ContentChannel respond() {
+ responseContent = handler.handleResponse(new Response(Response.Status.OK));
+ if (responseContent != null && closeResponse) {
+ responseContent.close(null);
+ }
+ return responseContent;
+ }
+
+ static MyRequestHandler newInstance() {
+ return new MyRequestHandler(MyContent.newInstance(), false, false, false);
+ }
+
+ static MyRequestHandler newEagerCompletion() {
+ return new MyRequestHandler(MyContent.newEagerCompletion(), false, false, false);
+ }
+
+ static MyRequestHandler newEagerFail() {
+ return new MyRequestHandler(MyContent.newEagerFail(), false, false, false);
+ }
+
+ static RequestHandler newRequestException() {
+ return new MyRequestHandler(null, false, false, true);
+ }
+
+ static MyRequestHandler newNullContent() {
+ return new MyRequestHandler(null, false, false, false);
+ }
+
+ static MyRequestHandler newNullContentWithEagerHandleResponse() {
+ return new MyRequestHandler(null, true, false, false);
+ }
+
+ static MyRequestHandler newNulContentWithEagerCloseResponse() {
+ return new MyRequestHandler(null, true, true, false);
+ }
+
+ static MyRequestHandler newRequestExceptionWithEagerHandleResponse() {
+ return new MyRequestHandler(null, true, false, true);
+ }
+
+ static MyRequestHandler newRequestExceptionWithEagerCloseResponse() {
+ return new MyRequestHandler(null, true, true, true);
+ }
+
+ static MyRequestHandler newContentWriteExceptionWithEagerCompletion() {
+ return new MyRequestHandler(MyContent.newWriteExceptionWithEagerCompletion(), true, true, false);
+ }
+
+ static MyRequestHandler newContentCloseException() {
+ return new MyRequestHandler(MyContent.newCloseException(), true, true, false);
+ }
+
+ static MyRequestHandler newEagerRespondWithEagerCompletion() {
+ return new MyRequestHandler(MyContent.newEagerCompletion(), true, false, false);
+ }
+
+ static MyRequestHandler newEagerCloseResponse() {
+ return new MyRequestHandler(MyContent.newInstance(), true, true, false);
+ }
+ }
+
+ private static class MyResponseHandler implements ResponseHandler {
+
+ final MyContent content;
+ final boolean throwException;
+
+ MyResponseHandler(MyContent content, boolean throwException) {
+ this.content = content;
+ this.throwException = throwException;
+ }
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ if (throwException) {
+ throw new MyException();
+ }
+ return content;
+ }
+
+ static MyResponseHandler newInstance() {
+ return new MyResponseHandler(MyContent.newInstance(), false);
+ }
+
+ static MyResponseHandler newEagerCompletion() {
+ return new MyResponseHandler(MyContent.newEagerCompletion(), false);
+ }
+
+ static MyResponseHandler newEagerFail() {
+ return new MyResponseHandler(MyContent.newEagerFail(), false);
+ }
+
+ static MyResponseHandler newNullContent() {
+ return new MyResponseHandler(null, false);
+ }
+
+ static MyResponseHandler newResponseException() {
+ return new MyResponseHandler(null, true);
+ }
+
+ static MyResponseHandler newContentWriteException() {
+ return new MyResponseHandler(MyContent.newWriteException(), false);
+ }
+
+ static MyResponseHandler newContentCloseException() {
+ return new MyResponseHandler(MyContent.newCloseException(), false);
+ }
+ }
+
+ private static final class MyException extends RuntimeException {
+
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerSnapshotTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerSnapshotTestCase.java
new file mode 100644
index 00000000000..15bd3b050d4
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerSnapshotTestCase.java
@@ -0,0 +1,211 @@
+// 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.Key;
+import com.google.inject.name.Names;
+import com.yahoo.jdisc.AbstractResource;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.application.BindingMatch;
+import com.yahoo.jdisc.application.ContainerBuilder;
+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 com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ContainerSnapshotTestCase {
+
+ @Test
+ public void requireThatServerHandlerCanBeResolved() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind("http://foo/*", MyRequestHandler.newInstance());
+ driver.activateContainer(builder);
+
+ Request request = new Request(driver, URI.create("http://foo/"));
+ assertNotNull(request.container().resolveHandler(request));
+ assertNotNull(request.getBindingMatch());
+ request.release();
+
+ request = new Request(driver, URI.create("http://foo/"));
+ request.setServerRequest(false);
+ assertNull(request.container().resolveHandler(request));
+ assertNull(request.getBindingMatch());
+ request.release();
+
+ request = new Request(driver, URI.create("http://bar/"));
+ assertNull(request.container().resolveHandler(request));
+ assertNull(request.getBindingMatch());
+ request.release();
+
+ request = new Request(driver, URI.create("http://bar/"));
+ request.setServerRequest(false);
+ assertNull(request.container().resolveHandler(request));
+ assertNull(request.getBindingMatch());
+ request.release();
+
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatClientHandlerCanBeResolved() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.clientBindings().bind("http://foo/*", MyRequestHandler.newInstance());
+ driver.activateContainer(builder);
+
+ Request request = new Request(driver, URI.create("http://foo/"));
+ assertNull(request.container().resolveHandler(request));
+ assertNull(request.getBindingMatch());
+ request.release();
+
+ request = new Request(driver, URI.create("http://foo/"));
+ request.setServerRequest(false);
+ assertNotNull(request.container().resolveHandler(request));
+ assertNotNull(request.getBindingMatch());
+ request.release();
+
+ request = new Request(driver, URI.create("http://bar/"));
+ assertNull(request.container().resolveHandler(request));
+ assertNull(request.getBindingMatch());
+ request.release();
+
+ request = new Request(driver, URI.create("http://bar/"));
+ request.setServerRequest(false);
+ assertNull(request.container().resolveHandler(request));
+ assertNull(request.getBindingMatch());
+ request.release();
+
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatClientBindingsAreUsed() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.clientBindings().bind("http://host/path", MyRequestHandler.newInstance());
+ driver.activateContainer(builder);
+ Request request = new Request(driver, URI.create("http://host/path"));
+ assertNull(request.container().resolveHandler(request));
+ request.setServerRequest(false);
+ assertNotNull(request.container().resolveHandler(request));
+ request.release();
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatBindingMatchIsSetByResolveHandler() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind("http://*/*", MyRequestHandler.newInstance());
+ driver.activateContainer(builder);
+
+ Request request = new Request(driver, URI.create("http://localhost:69/status.html"));
+ assertNotNull(request.container().resolveHandler(request));
+ BindingMatch<RequestHandler> match = request.getBindingMatch();
+ assertNotNull(match);
+ assertEquals(3, match.groupCount());
+ assertEquals("localhost", match.group(0));
+ assertEquals("69", match.group(1));
+ assertEquals("status.html", match.group(2));
+ request.release();
+
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatNewRequestHasSameSnapshot() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ driver.activateContainer(driver.newContainerBuilder());
+ Request foo = new Request(driver, URI.create("http://host/foo"));
+ Request bar = new Request(foo, URI.create("http://host/bar"));
+ assertSame(foo.container(), bar.container());
+ foo.release();
+ bar.release();
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatActiveInjectorIsUsed() {
+ final Object obj = new Object();
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(Object.class).toInstance(obj);
+ bind(String.class).annotatedWith(Names.named("foo")).toInstance("foo");
+ }
+ });
+ ActiveContainer active = new ActiveContainer(driver.newContainerBuilder());
+ ContainerSnapshot snapshot = new ContainerSnapshot(active, null, null);
+ assertSame(obj, snapshot.getInstance(Object.class));
+ assertEquals("foo", snapshot.getInstance(Key.get(String.class, Names.named("foo"))));
+ snapshot.release();
+ assertTrue(driver.close());
+ }
+
+ private static class MyContent implements ContentChannel {
+
+ CompletionHandler writeCompletion = null;
+ CompletionHandler closeCompletion = null;
+ ByteBuffer writeBuf = null;
+ boolean closed = false;
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ writeBuf = buf;
+ writeCompletion = handler;
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ closed = true;
+ closeCompletion = handler;
+ }
+ }
+
+ private static class MyRequestHandler extends AbstractResource implements RequestHandler {
+
+ final MyContent content = new MyContent();
+ Request request = null;
+ ResponseHandler handler = null;
+ boolean timeout = false;
+ boolean destroyed = false;
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ this.request = request;
+ this.handler = handler;
+ return content;
+ }
+
+ @Override
+ public void handleTimeout(Request request, ResponseHandler handler) {
+ timeout = true;
+ }
+
+ @Override
+ public void destroy() {
+ destroyed = true;
+ }
+
+ static MyRequestHandler newInstance() {
+ return new MyRequestHandler();
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerTerminationTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerTerminationTestCase.java
new file mode 100644
index 00000000000..bc2591d94b0
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ContainerTerminationTestCase.java
@@ -0,0 +1,76 @@
+// 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.ContainerBuilder;
+import com.yahoo.jdisc.application.DeactivatedContainer;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ContainerTerminationTestCase {
+
+ @Test
+ public void requireThatAccessorsWork() {
+ Object obj = new Object();
+ ContainerTermination termination = new ContainerTermination(obj);
+ assertSame(obj, termination.appContext());
+ }
+
+ @Test
+ public void requireThatAppContextIsFromBuilder() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ Object obj = new Object();
+ builder.setAppContext(obj);
+ driver.activateContainer(builder);
+ DeactivatedContainer container = driver.activateContainer(null);
+ assertSame(obj, container.appContext());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatEarlyTerminationIsNotified() {
+ ContainerTermination termination = new ContainerTermination(null);
+ termination.run();
+ MyTask task = new MyTask();
+ termination.notifyTermination(task);
+ assertTrue(task.done);
+ }
+
+ @Test
+ public void requireThatLaterTerminationIsNotified() {
+ ContainerTermination termination = new ContainerTermination(null);
+ MyTask task = new MyTask();
+ termination.notifyTermination(task);
+ assertFalse(task.done);
+ termination.run();
+ assertTrue(task.done);
+ }
+
+ @Test
+ public void requireThatNotifyCanOnlyBeCalledOnce() {
+ ContainerTermination termination = new ContainerTermination(null);
+ termination.notifyTermination(new MyTask());
+ try {
+ termination.notifyTermination(new MyTask());
+ } catch (IllegalStateException e) {
+
+ }
+ }
+
+ private static class MyTask implements Runnable {
+
+ boolean done = false;
+
+ @Override
+ public void run() {
+ done = true;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/DefaultBindingSelectorTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/DefaultBindingSelectorTestCase.java
new file mode 100644
index 00000000000..1154d01dfe5
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/DefaultBindingSelectorTestCase.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.core;
+
+import com.google.inject.Guice;
+import com.yahoo.jdisc.application.BindingSet;
+import com.yahoo.jdisc.application.BindingSetSelector;
+import org.junit.Test;
+
+import java.net.URI;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class DefaultBindingSelectorTestCase {
+
+ @Test
+ public void requireThatClassIsInjectedByDefault() {
+ BindingSetSelector selector = Guice.createInjector().getInstance(BindingSetSelector.class);
+ assertTrue(selector instanceof DefaultBindingSelector);
+ }
+
+ @Test
+ public void requireThatDefaultSetIsAlwaysSelected() {
+ DefaultBindingSelector selector = new DefaultBindingSelector();
+ assertEquals(BindingSet.DEFAULT, selector.select(null));
+ for (int i = 0; i < 69; ++i) {
+ assertEquals(BindingSet.DEFAULT, selector.select(newUri()));
+ }
+ }
+
+ private static URI newUri() {
+ return URI.create("foo" + System.nanoTime() + "://bar" + System.nanoTime() + "/baz" + System.nanoTime());
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ExportPackagesTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ExportPackagesTestCase.java
new file mode 100644
index 00000000000..a61ee4efc2d
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ExportPackagesTestCase.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.core;
+
+import org.junit.Test;
+
+import java.io.File;
+import java.io.FileReader;
+import java.util.Properties;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class ExportPackagesTestCase {
+
+ @Test
+ public void requireThatPropertiesAreWritten() throws Exception {
+ File file = new File("target", ExportPackages.PROPERTIES_FILE);
+ file.deleteOnExit();
+ ExportPackages.main(new String[] { file.getAbsolutePath() });
+ assertTrue(file.exists());
+ Properties props = new Properties();
+ try (FileReader reader = new FileReader(file)) {
+ props.load(reader);
+ assertNotNull(props.getProperty(ExportPackages.EXPORT_PACKAGES));
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/FelixFrameworkTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/FelixFrameworkTestCase.java
new file mode 100644
index 00000000000..69696ff62ec
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/FelixFrameworkTestCase.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.core;
+
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+import org.osgi.framework.BundleException;
+
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class FelixFrameworkTestCase {
+
+ @Test
+ public void requireThatLifecycleWorks() throws BundleException {
+ FelixFramework felix = TestDriver.newOsgiFramework();
+ felix.start();
+ felix.stop();
+ }
+
+ @Test
+ public void requireThatStopWithoutStartDoesNotThrowException() throws BundleException {
+ FelixFramework felix = TestDriver.newOsgiFramework();
+ felix.stop();
+ }
+
+ @Test
+ public void requireThatInstallCanThrowException() throws BundleException {
+ FelixFramework felix = TestDriver.newOsgiFramework();
+ felix.start();
+ try {
+ felix.installBundle("file:notfound.jar");
+ fail();
+ } catch (BundleException e) {
+
+ }
+ felix.stop();
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/FelixParamsTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/FelixParamsTestCase.java
new file mode 100644
index 00000000000..216b79c1d7f
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/FelixParamsTestCase.java
@@ -0,0 +1,67 @@
+// 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.junit.Test;
+import org.osgi.framework.Constants;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class FelixParamsTestCase {
+
+ @Test
+ public void requireThatAccessorsWork() {
+ FelixParams params = new FelixParams();
+ params.setCachePath("foo");
+ assertEquals("foo", params.getCachePath());
+ params.setLoggerEnabled(true);
+ assertTrue(params.isLoggerEnabled());
+ }
+
+ @Test
+ public void requireThatSystemPackagesAreNotReplaced() {
+ FelixParams params = new FelixParams();
+ Map<String, String> config = params.toConfig();
+ assertNotNull(config);
+ String str = config.get(Constants.FRAMEWORK_SYSTEMPACKAGES);
+ assertNotNull(str);
+ assertTrue(str.contains(ExportPackages.getSystemPackages()));
+
+ params.exportPackage("foo");
+ assertNotNull(config = params.toConfig());
+ assertNotNull(str = config.get(Constants.FRAMEWORK_SYSTEMPACKAGES));
+ assertTrue(str.contains(ExportPackages.getSystemPackages()));
+ assertTrue(str.contains("foo"));
+ }
+
+ @Test
+ public void requireThatExportsAreIncludedInConfig() {
+ FelixParams params = new FelixParams();
+ Map<String, String> config = params.toConfig();
+ assertNotNull(config);
+ String[] prev = config.get(Constants.FRAMEWORK_SYSTEMPACKAGES).split(",");
+
+ params.exportPackage("foo");
+ params.exportPackage("bar");
+ assertNotNull(config = params.toConfig());
+ String[] next = config.get(Constants.FRAMEWORK_SYSTEMPACKAGES).split(",");
+
+ assertEquals(prev.length + 2, next.length);
+
+ List<String> diff = new LinkedList<>();
+ diff.addAll(Arrays.asList(next));
+ diff.removeAll(Arrays.asList(prev));
+ assertEquals(2, diff.size());
+ assertTrue(diff.contains("foo"));
+ assertTrue(diff.contains("bar"));
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogHandlerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogHandlerTestCase.java
new file mode 100644
index 00000000000..eb18e6f8e49
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogHandlerTestCase.java
@@ -0,0 +1,192 @@
+// 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.junit.Test;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.log.LogService;
+
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.ResourceBundle;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class OsgiLogHandlerTestCase {
+
+ @Test
+ public void requireThatLogRecordsArePublishedToLogService() {
+ MyLogService logService = new MyLogService();
+ Logger log = newLogger(logService);
+
+ log.log(Level.INFO, "foo");
+ assertEquals(OsgiLogHandler.toServiceLevel(Level.INFO), logService.lastLevel);
+ assertEquals("foo", logService.lastMessage);
+ assertNull(logService.lastThrowable);
+
+ Throwable t = new Throwable();
+ log.log(Level.SEVERE, "bar", t);
+ assertEquals(OsgiLogHandler.toServiceLevel(Level.SEVERE), logService.lastLevel);
+ assertEquals("bar", logService.lastMessage);
+ assertEquals(t, logService.lastThrowable);
+ }
+
+ @Test
+ public void requireThatStadardLogLevelsAreConverted() {
+ assertLogLevel(LogService.LOG_ERROR, Level.SEVERE);
+ assertLogLevel(LogService.LOG_WARNING, Level.WARNING);
+ assertLogLevel(LogService.LOG_INFO, Level.INFO);
+ assertLogLevel(LogService.LOG_DEBUG, Level.CONFIG);
+ assertLogLevel(LogService.LOG_DEBUG, Level.FINE);
+ assertLogLevel(LogService.LOG_DEBUG, Level.FINER);
+ assertLogLevel(LogService.LOG_DEBUG, Level.FINEST);
+ }
+
+ @Test
+ public void requireThatCustomLogLevelsAreConverted() {
+ for (int i = Level.ALL.intValue() - 69; i < Level.OFF.intValue() + 69; ++i) {
+ int expectedLevel;
+ if (i >= Level.SEVERE.intValue()) {
+ expectedLevel = LogService.LOG_ERROR;
+ } else if (i >= Level.WARNING.intValue()) {
+ expectedLevel = LogService.LOG_WARNING;
+ } else if (i >= Level.INFO.intValue()) {
+ expectedLevel = LogService.LOG_INFO;
+ } else {
+ expectedLevel = LogService.LOG_DEBUG;
+ }
+ assertLogLevel(expectedLevel, new MyLogLevel(i));
+ }
+ }
+
+ @Test
+ public void requireThatJdk14PropertiesAreAvailableThroughServiceReference() {
+ MyLogService logService = new MyLogService();
+
+ Logger log = newLogger(logService);
+ LogRecord record = new LogRecord(Level.INFO, "message");
+ record.setLoggerName("loggerName");
+ record.setMillis(69);
+ Object[] parameters = new Object[0];
+ record.setParameters(parameters);
+ ResourceBundle resouceBundle = new MyResourceBundle();
+ record.setResourceBundle(resouceBundle);
+ record.setResourceBundleName("resourceBundleName");
+ record.setSequenceNumber(69);
+ record.setSourceClassName("sourceClassName");
+ record.setSourceMethodName("sourceMethodName");
+ record.setThreadID(69);
+ Throwable thrown = new Throwable();
+ record.setThrown(thrown);
+ log.log(record);
+
+ ServiceReference ref = logService.lastServiceReference;
+ assertNotNull(ref);
+ assertTrue(Arrays.equals(new String[] { "LEVEL",
+ "LOGGER_NAME",
+ "MESSAGE",
+ "MILLIS",
+ "PARAMETERS",
+ "RESOURCE_BUNDLE",
+ "RESOURCE_BUNDLE_NAME",
+ "SEQUENCE_NUMBER",
+ "SOURCE_CLASS_NAME",
+ "SOURCE_METHOD_NAME",
+ "THREAD_ID",
+ "THROWN" },
+ ref.getPropertyKeys()));
+ assertEquals(Level.INFO, ref.getProperty("LEVEL"));
+ assertEquals("loggerName", ref.getProperty("LOGGER_NAME"));
+ assertEquals("message", ref.getProperty("MESSAGE"));
+ assertEquals(69L, ref.getProperty("MILLIS"));
+ assertSame(parameters, ref.getProperty("PARAMETERS"));
+ assertSame(resouceBundle, ref.getProperty("RESOURCE_BUNDLE"));
+ assertEquals("resourceBundleName", ref.getProperty("RESOURCE_BUNDLE_NAME"));
+ assertEquals(69L, ref.getProperty("SEQUENCE_NUMBER"));
+ assertEquals("sourceClassName", ref.getProperty("SOURCE_CLASS_NAME"));
+ assertEquals("sourceMethodName", ref.getProperty("SOURCE_METHOD_NAME"));
+ assertEquals(69, ref.getProperty("THREAD_ID"));
+ assertSame(thrown, ref.getProperty("THROWN"));
+ assertNull(ref.getProperty("unknown"));
+ }
+
+ private static void assertLogLevel(int expectedLevel, Level level) {
+ MyLogService logService = new MyLogService();
+ Logger log = newLogger(logService);
+ log.log(level, "message");
+ assertEquals(expectedLevel, logService.lastLevel);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Logger newLogger(LogService logService) {
+ Logger log = Logger.getAnonymousLogger();
+ log.setUseParentHandlers(false);
+ log.setLevel(Level.ALL);
+ for (Handler handler : log.getHandlers()) {
+ log.removeHandler(handler);
+ }
+ log.addHandler(new OsgiLogHandler(logService));
+ return log;
+ }
+
+ private static class MyLogLevel extends Level {
+
+ protected MyLogLevel(int val) {
+ super("foo", val);
+ }
+ }
+
+ private static class MyLogService implements LogService {
+
+ ServiceReference lastServiceReference;
+ int lastLevel;
+ String lastMessage;
+ Throwable lastThrowable;
+
+ @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);
+ }
+
+ @Override
+ public void log(ServiceReference serviceReference, int level, String message) {
+ log(serviceReference, level, message, null);
+ }
+
+ @Override
+ public void log(ServiceReference serviceReference, int level, String message, Throwable throwable) {
+ lastServiceReference = serviceReference;
+ lastLevel = level;
+ lastMessage = message;
+ lastThrowable = throwable;
+ }
+ }
+
+ private static class MyResourceBundle extends ResourceBundle {
+
+ @Override
+ protected Object handleGetObject(String key) {
+ return null;
+ }
+
+ @Override
+ public Enumeration<String> getKeys() {
+ return null;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogManagerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogManagerTestCase.java
new file mode 100644
index 00000000000..32e4b37aca8
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogManagerTestCase.java
@@ -0,0 +1,184 @@
+// 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.test.TestDriver;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.log.LogService;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class OsgiLogManagerTestCase {
+
+ @Test
+ public void requireThatAllLogMethodsAreImplemented() throws BundleException {
+ FelixFramework felix = TestDriver.newOsgiFramework();
+ felix.start();
+
+ BundleContext ctx = felix.bundleContext();
+ OsgiLogManager manager = new OsgiLogManager(true);
+ manager.install(ctx);
+ MyLogService service = new MyLogService();
+ ctx.registerService(LogService.class.getName(), service, null);
+
+ manager.log(2, "a");
+ assertLast(service, null, 2, "a", null);
+
+ Throwable t1 = new Throwable();
+ manager.log(4, "b", t1);
+ assertLast(service, null, 4, "b", t1);
+
+ ServiceReference ref1 = Mockito.mock(ServiceReference.class);
+ manager.log(ref1, 8, "c");
+ assertLast(service, ref1, 8, "c", null);
+
+ ServiceReference ref2 = Mockito.mock(ServiceReference.class);
+ Throwable t2 = new Throwable();
+ manager.log(ref2, 16, "d", t2);
+ assertLast(service, ref2, 16, "d", t2);
+
+ manager.uninstall();
+ felix.stop();
+ }
+
+ @Test
+ public void requireThatLogManagerWritesToAllRegisteredLogServices() throws BundleException {
+ FelixFramework felix = TestDriver.newOsgiFramework();
+ felix.start();
+
+ BundleContext ctx = felix.bundleContext();
+ MyLogService foo = new MyLogService();
+ ServiceRegistration<LogService> fooReg = ctx.registerService(LogService.class, foo, null);
+
+ OsgiLogManager manager = new OsgiLogManager(true);
+ manager.install(ctx);
+
+ ServiceReference ref1 = Mockito.mock(ServiceReference.class);
+ Throwable t1 = new Throwable();
+ manager.log(ref1, 2, "a", t1);
+ assertLast(foo, ref1, 2, "a", t1);
+
+ MyLogService bar = new MyLogService();
+ ServiceRegistration<LogService> barReg = ctx.registerService(LogService.class, bar, null);
+
+ ServiceReference ref2 = Mockito.mock(ServiceReference.class);
+ Throwable t2 = new Throwable();
+ manager.log(ref2, 4, "b", t2);
+ assertLast(foo, ref2, 4, "b", t2);
+ assertLast(bar, ref2, 4, "b", t2);
+
+ MyLogService baz = new MyLogService();
+ ServiceRegistration<LogService> bazReg = ctx.registerService(LogService.class, baz, null);
+
+ ServiceReference ref3 = Mockito.mock(ServiceReference.class);
+ Throwable t3 = new Throwable();
+ manager.log(ref3, 8, "c", t3);
+ assertLast(foo, ref3, 8, "c", t3);
+ assertLast(bar, ref3, 8, "c", t3);
+ assertLast(baz, ref3, 8, "c", t3);
+
+ fooReg.unregister();
+
+ ServiceReference ref4 = Mockito.mock(ServiceReference.class);
+ Throwable t4 = new Throwable();
+ manager.log(ref4, 16, "d", t4);
+ assertLast(foo, ref3, 8, "c", t3);
+ assertLast(bar, ref4, 16, "d", t4);
+ assertLast(baz, ref4, 16, "d", t4);
+
+ barReg.unregister();
+
+ ServiceReference ref5 = Mockito.mock(ServiceReference.class);
+ Throwable t5 = new Throwable();
+ manager.log(ref5, 32, "e", t5);
+ assertLast(foo, ref3, 8, "c", t3);
+ assertLast(bar, ref4, 16, "d", t4);
+ assertLast(baz, ref5, 32, "e", t5);
+
+ bazReg.unregister();
+
+ ServiceReference ref6 = Mockito.mock(ServiceReference.class);
+ Throwable t6 = new Throwable();
+ manager.log(ref6, 64, "f", t6);
+ assertLast(foo, ref3, 8, "c", t3);
+ assertLast(bar, ref4, 16, "d", t4);
+ assertLast(baz, ref5, 32, "e", t5);
+
+ manager.uninstall();
+ felix.stop();
+ }
+
+ @Test
+ public void requireThatRootLoggerModificationCanBeDisabled() throws BundleException {
+ Logger logger = Logger.getLogger("");
+ logger.setLevel(Level.WARNING);
+
+ new OsgiLogManager(false).install(Mockito.mock(BundleContext.class));
+ assertEquals(Level.WARNING, logger.getLevel());
+
+ new OsgiLogManager(true).install(Mockito.mock(BundleContext.class));
+ assertEquals(Level.ALL, logger.getLevel());
+ }
+
+ @Test
+ public void requireThatRootLoggerLevelIsModifiedIfNoLoggerConfigIsGiven() {
+ Logger logger = Logger.getLogger("");
+ logger.setLevel(Level.WARNING);
+
+ OsgiLogManager.newInstance().install(Mockito.mock(BundleContext.class));
+
+ assertNull(System.getProperty("java.util.logging.config.file"));
+ assertEquals(Level.ALL, logger.getLevel());
+ }
+
+ private static void assertLast(MyLogService service, ServiceReference ref, int level, String message, Throwable t) {
+ assertSame(ref, service.lastServiceReference);
+ assertEquals(level, service.lastLevel);
+ assertEquals(message, service.lastMessage);
+ assertSame(t, service.lastThrowable);
+ }
+
+ private static class MyLogService implements LogService {
+
+ ServiceReference lastServiceReference;
+ int lastLevel;
+ String lastMessage;
+ Throwable lastThrowable;
+
+ @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);
+ }
+
+ @Override
+ public void log(ServiceReference serviceReference, int level, String message) {
+ log(serviceReference, level, message, null);
+ }
+
+ @Override
+ public void log(ServiceReference serviceReference, int level, String message, Throwable throwable) {
+ lastServiceReference = serviceReference;
+ lastLevel = level;
+ lastMessage = message;
+ lastThrowable = throwable;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogServiceTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogServiceTestCase.java
new file mode 100644
index 00000000000..fbd6f5a3f88
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/OsgiLogServiceTestCase.java
@@ -0,0 +1,105 @@
+// 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.OsgiFramework;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.service.log.LogReaderService;
+import org.osgi.service.log.LogService;
+import org.osgi.util.tracker.ServiceTracker;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class OsgiLogServiceTestCase {
+
+ @Test
+ public void requireThatLogServiceIsRegistered() throws BundleException, InterruptedException {
+ OsgiFramework osgi = TestDriver.newOsgiFramework();
+ osgi.start();
+
+ ServiceTracker logs = newTracker(osgi, LogService.class);
+ ServiceTracker logReaders = newTracker(osgi, LogReaderService.class);
+ assertEquals(1, logs.getTrackingCount());
+ assertEquals(1, logReaders.getTrackingCount());
+
+ OsgiLogService service = new OsgiLogService();
+ service.start(osgi.bundleContext());
+
+ assertEquals(2, logs.getTrackingCount());
+ assertEquals(2, logReaders.getTrackingCount());
+ osgi.stop();
+ }
+
+ @Test
+ public void requireThatLogServiceCanNotBeStartedTwice() throws BundleException {
+ OsgiFramework osgi = TestDriver.newOsgiFramework();
+ osgi.start();
+
+ BundleContext ctx = osgi.bundleContext();
+ OsgiLogService service = new OsgiLogService();
+ service.start(ctx);
+
+ try {
+ service.start(ctx);
+ fail();
+ } catch (IllegalStateException e) {
+
+ }
+
+ osgi.stop();
+ }
+
+ @Test
+ public void requireThatLogServiceCanNotBeStoppedTwice() throws BundleException {
+ OsgiFramework osgi = TestDriver.newOsgiFramework();
+ osgi.start();
+
+ BundleContext ctx = osgi.bundleContext();
+ OsgiLogService service = new OsgiLogService();
+ service.start(ctx);
+ service.stop();
+
+ try {
+ service.stop();
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+
+ osgi.stop();
+ }
+
+ @Test
+ public void requireThatUnstartedLogServiceCanNotBeStopped() throws BundleException {
+ try {
+ new OsgiLogService().stop();
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatLogServiceCanNotStartWithoutBundleContext() throws BundleException {
+ try {
+ new OsgiLogService().start(null);
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static ServiceTracker newTracker(OsgiFramework osgi, Class trackedClass) {
+ ServiceTracker tracker = new ServiceTracker(osgi.bundleContext(), trackedClass, null);
+ tracker.open();
+ return tracker;
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/ScheduledQueueTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ScheduledQueueTestCase.java
new file mode 100644
index 00000000000..b208d5e5b9d
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/ScheduledQueueTestCase.java
@@ -0,0 +1,149 @@
+// 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.junit.Test;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+
+import static com.yahoo.jdisc.core.ScheduledQueue.MILLIS_PER_SLOT;
+import static com.yahoo.jdisc.core.ScheduledQueue.NUM_SLOTS;
+import static com.yahoo.jdisc.core.ScheduledQueue.NUM_SLOTS_UNDILATED;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class ScheduledQueueTestCase {
+
+ @Test
+ public void requireThatSlotMaskPreventsOverflow() {
+ for (int slot = 0; slot < NUM_SLOTS * 2; ++slot) {
+ assertTrue((slot & ScheduledQueue.SLOT_MASK) < NUM_SLOTS);
+ }
+ }
+
+ @Test
+ public void requireThatIterShiftDiscardsSlotBits() {
+ for (int slot = 0; slot < NUM_SLOTS * 2; ++slot) {
+ assertEquals(slot / NUM_SLOTS, slot >> ScheduledQueue.ITER_SHIFT);
+ }
+ }
+
+ @Test
+ public void requireThatNewEntryDoesNotAcceptNull() {
+ ScheduledQueue queue = new ScheduledQueue(0);
+ try {
+ queue.newEntry(null);
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatEntriesCanBeScheduled() {
+ ScheduledQueue queue = new ScheduledQueue(0);
+ Object foo = new Object();
+ ScheduledQueue.Entry entry = queue.newEntry(foo);
+ entry.scheduleAt(200);
+
+ assertDrainTo(queue, 150);
+ assertDrainTo(queue, 250, foo);
+ }
+
+ @Test
+ public void requireThatEntriesCanBeRescheduled() {
+ ScheduledQueue queue = new ScheduledQueue(0);
+ Object foo = new Object();
+ ScheduledQueue.Entry entry = queue.newEntry(foo);
+ entry.scheduleAt(200);
+ entry.scheduleAt(100);
+
+ assertDrainTo(queue, 150, foo);
+ assertDrainTo(queue, 250);
+ }
+
+ @Test
+ public void requireThatEntriesCanBeUnscheduled() {
+ ScheduledQueue queue = new ScheduledQueue(0);
+ Object foo = new Object();
+ ScheduledQueue.Entry entry = queue.newEntry(foo);
+ entry.scheduleAt(100);
+ entry.unschedule();
+
+ assertDrainTo(queue, 150);
+ }
+
+ @Test
+ public void requireThatDrainToOnlyDrainsExpiredEntries() {
+ ScheduledQueue queue = new ScheduledQueue(0);
+ Object foo = scheduleAt(queue, 100);
+ Object bar = scheduleAt(queue, 300);
+ Object baz = scheduleAt(queue, 200);
+
+ assertDrainTo(queue, 150, foo);
+ assertDrainTo(queue, 250, baz);
+ assertDrainTo(queue, 350, bar);
+ assertDrainTo(queue, 450);
+ }
+
+ @Test
+ public void requireThatEntriesDoNotExpireMoreThanOnce() {
+ ScheduledQueue queue = new ScheduledQueue(0);
+ Object foo = scheduleAt(queue, NUM_SLOTS * MILLIS_PER_SLOT + 50);
+
+ long now = 0;
+ for (int i = 0; i < NUM_SLOTS; ++i, now += MILLIS_PER_SLOT) {
+ assertDrainTo(queue, now);
+ }
+ assertDrainTo(queue, now += MILLIS_PER_SLOT, foo);
+ for (int i = 0; i < NUM_SLOTS; ++i, now += MILLIS_PER_SLOT) {
+ assertDrainTo(queue, now);
+ }
+ }
+
+ @Test
+ public void requireThatNegativeScheduleTranslatesToNow() {
+ ScheduledQueue queue = new ScheduledQueue(0);
+ Object foo = scheduleAt(queue, -100);
+
+ assertDrainTo(queue, 0, foo);
+ }
+
+ @Test
+ public void requireThatDrainToPerformsTimeDilationWhenOverloaded() {
+ ScheduledQueue queue = new ScheduledQueue(0);
+ List<Object> payloads = new LinkedList<>();
+ for (int i = 1; i <= NUM_SLOTS_UNDILATED + 1; ++i) {
+ payloads.add(scheduleAt(queue, i * MILLIS_PER_SLOT));
+ }
+
+ Queue<Object> expired = new LinkedList<>();
+ long currentTimeMillis = payloads.size() * MILLIS_PER_SLOT;
+ queue.drainTo(currentTimeMillis, expired);
+ assertEquals(NUM_SLOTS_UNDILATED, expired.size());
+
+ expired = new LinkedList<>();
+ currentTimeMillis += MILLIS_PER_SLOT;
+ queue.drainTo(currentTimeMillis, expired);
+ assertEquals(1, expired.size());
+ }
+
+ private static Object scheduleAt(ScheduledQueue queue, long expireAtMillis) {
+ Object obj = new Object();
+ queue.newEntry(obj).scheduleAt(expireAtMillis);
+ return obj;
+ }
+
+ private static void assertDrainTo(ScheduledQueue queue, long currentTimeMillis, Object... expected) {
+ Queue<Object> expired = new LinkedList<>();
+ queue.drainTo(currentTimeMillis, expired);
+ assertEquals(expected.length, expired.size());
+ assertEquals(Arrays.asList(expected), expired);
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/SystemTimerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/SystemTimerTestCase.java
new file mode 100644
index 00000000000..e4d0df36260
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/SystemTimerTestCase.java
@@ -0,0 +1,31 @@
+// 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.Guice;
+import com.yahoo.jdisc.Timer;
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class SystemTimerTestCase {
+
+ @Test
+ public void requireThatClassIsInjectedByDefault() {
+ Timer timer = Guice.createInjector().getInstance(Timer.class);
+ assertTrue(timer instanceof SystemTimer);
+ }
+
+ @Test
+ public void requireThatSystemTimerIsSane() {
+ long before = System.currentTimeMillis();
+ long millis = new SystemTimer().currentTimeMillis();
+ long after = System.currentTimeMillis();
+
+ assertTrue(before <= millis);
+ assertTrue(after >= millis);
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/TimeoutManagerImplTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/TimeoutManagerImplTestCase.java
new file mode 100644
index 00000000000..46464a9b05a
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/core/TimeoutManagerImplTestCase.java
@@ -0,0 +1,579 @@
+// 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.Binder;
+import com.google.inject.Module;
+import com.yahoo.jdisc.AbstractResource;
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.TimeoutManager;
+import com.yahoo.jdisc.Timer;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.RequestDeniedException;
+import com.yahoo.jdisc.handler.RequestDispatch;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.service.CurrentContainer;
+import com.yahoo.jdisc.test.NonWorkingRequest;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class TimeoutManagerImplTestCase {
+
+ private static final String REQUEST_URI = "http://host/path";
+
+ @Test
+ public void requireThatDefaultIsNoTimeout() {
+ Context ctx = new Context(MyRequestHandler.newEagerResponse());
+ assertNull(ctx.dispatchRequest(null, MyResponseHandler.newInstance()));
+ assertTrue(ctx.close());
+ }
+
+ @Test
+ public void requireThatTimeoutCanBeSetByServerProvider() {
+ Context ctx = new Context(MyRequestHandler.newEagerResponse());
+ assertEquals(Long.valueOf(69), ctx.dispatchRequest(69L, MyResponseHandler.newInstance()));
+ assertTrue(ctx.close());
+ }
+
+ @Test
+ public void requireThatTimeoutCanBeSetByRequestHandler() {
+ Context ctx = new Context(MyRequestHandler.newTimeoutWithEagerResponse(69));
+ assertEquals(Long.valueOf(69), ctx.dispatchRequest(null, MyResponseHandler.newInstance()));
+ assertTrue(ctx.close());
+ }
+
+ @Test
+ public void requireThatTimeoutRequestHandlerTimeoutHasPrecedence() {
+ Context ctx = new Context(MyRequestHandler.newTimeoutWithEagerResponse(6));
+ assertEquals(Long.valueOf(6), ctx.dispatchRequest(9L, MyResponseHandler.newInstance()));
+ assertTrue(ctx.close());
+ }
+
+ @Test
+ public void requireThatResponseCancelsTimeout() throws InterruptedException {
+ Context ctx = new Context(MyRequestHandler.newEagerResponse());
+ assertEquals(Response.Status.OK, ctx.awaitResponse(69L, MyResponseHandler.newInstance()));
+ assertEquals(Response.Status.OK, ctx.awaitResponse(69L, MyResponseHandler.newInstance()));
+ assertTrue(ctx.close());
+ }
+
+ @Test
+ public void requireThatNullRequestContentCanTimeout() throws InterruptedException {
+ Context ctx = new Context(MyRequestHandler.newNullContent());
+ assertEquals(Response.Status.REQUEST_TIMEOUT, ctx.awaitResponse(69L, MyResponseHandler.newInstance()));
+ assertEquals(Response.Status.REQUEST_TIMEOUT, ctx.awaitResponse(69L, MyResponseHandler.newInstance()));
+ assertTrue(ctx.close());
+ }
+
+ @Test
+ public void requireThatTimeoutWorksAfterRequestDenied() throws InterruptedException {
+ Context ctx = new Context(MyRequestHandler.newFirstRequestDenied());
+ try {
+ ctx.dispatchRequest(null, MyResponseHandler.newInstance());
+ fail();
+ } catch (RequestDeniedException e) {
+
+ }
+ assertEquals(Response.Status.REQUEST_TIMEOUT, ctx.awaitResponse(69L, MyResponseHandler.newInstance()));
+ assertTrue(ctx.close());
+ }
+
+ @Test
+ public void requireThatTimeoutWorksAfterResponseDenied() throws InterruptedException {
+ Context ctx = new Context(MyRequestHandler.newInstance());
+ assertEquals(Response.Status.REQUEST_TIMEOUT, ctx.awaitResponse(69L, MyResponseHandler.newResponseDenied()));
+ assertEquals(Response.Status.REQUEST_TIMEOUT, ctx.awaitResponse(69L, MyResponseHandler.newInstance()));
+ assertTrue(ctx.close());
+ }
+
+ @Test
+ public void requireThatTimeoutWorksAfterResponseThrowsException() throws InterruptedException {
+ Context ctx = new Context(MyRequestHandler.newInstance());
+ assertEquals(Response.Status.REQUEST_TIMEOUT, ctx.awaitResponse(69L, MyResponseHandler.newThrowException()));
+ assertEquals(Response.Status.REQUEST_TIMEOUT, ctx.awaitResponse(69L, MyResponseHandler.newInstance()));
+ assertTrue(ctx.close());
+ }
+
+ @Test
+ public void requireThatTimeoutWorksAfterResponseInterruptsThread() throws InterruptedException {
+ Context ctx = new Context(MyRequestHandler.newInstance());
+ assertEquals(Response.Status.REQUEST_TIMEOUT, ctx.awaitResponse(69L, MyResponseHandler.newInterruptThread()));
+ assertEquals(Response.Status.REQUEST_TIMEOUT, ctx.awaitResponse(69L, MyResponseHandler.newInstance()));
+ assertTrue(ctx.close());
+ }
+
+ @Test
+ public void requireThatTimeoutOccursInOrder() throws InterruptedException {
+ Context ctx = new Context(MyRequestHandler.newInstance());
+ MyResponseHandler foo = MyResponseHandler.newInstance();
+ ctx.dispatchRequest(300L, foo);
+
+ MyResponseHandler bar = MyResponseHandler.newInstance();
+ ctx.dispatchRequest(100L, bar);
+
+ MyResponseHandler baz = MyResponseHandler.newInstance();
+ ctx.dispatchRequest(200L, baz);
+
+ ctx.forwardToTime(100);
+ assertFalse(foo.await(10, TimeUnit.MILLISECONDS));
+ assertTrue(bar.await(600, TimeUnit.SECONDS));
+ assertFalse(baz.await(10, TimeUnit.MILLISECONDS));
+
+ ctx.forwardToTime(200);
+ assertFalse(foo.await(10, TimeUnit.MILLISECONDS));
+ assertTrue(baz.await(600, TimeUnit.SECONDS));
+
+ ctx.forwardToTime(300);
+ assertTrue(foo.await(600, TimeUnit.SECONDS));
+
+ assertTrue(ctx.close());
+ }
+
+ @Test
+ public void requireThatResponseHandlerIsWellBehavedAfterTimeout() throws InterruptedException {
+ Context ctx = new Context(MyRequestHandler.newInstance());
+ assertEquals(Response.Status.REQUEST_TIMEOUT, ctx.awaitResponse(69L, MyResponseHandler.newInstance()));
+
+ ContentChannel content = ctx.requestHandler.responseHandler.handleResponse(new Response(Response.Status.OK));
+ assertNotNull(content);
+
+ content.write(ByteBuffer.allocate(69), null);
+ MyCompletion completion = new MyCompletion();
+ content.write(ByteBuffer.allocate(69), completion);
+ assertTrue(completion.completed.await(600, TimeUnit.SECONDS));
+
+ completion = new MyCompletion();
+ content.close(completion);
+ assertTrue(completion.completed.await(600, TimeUnit.SECONDS));
+
+ assertTrue(ctx.close());
+ }
+
+ @Test
+ public void requireThatManagedHandlerForwardsAllCalls() throws InterruptedException {
+ Request request = NonWorkingRequest.newInstance(REQUEST_URI);
+ MyRequestHandler requestHandler = MyRequestHandler.newInstance();
+ TimeoutManagerImpl timeoutManager = new TimeoutManagerImpl(Executors.defaultThreadFactory(),
+ new SystemTimer());
+ RequestHandler managedHandler = timeoutManager.manageHandler(requestHandler);
+
+ MyResponseHandler responseHandler = MyResponseHandler.newInstance();
+ ContentChannel requestContent = managedHandler.handleRequest(request, responseHandler);
+ assertNotNull(requestContent);
+
+ ByteBuffer buf = ByteBuffer.allocate(69);
+ requestContent.write(buf, null);
+ assertSame(buf, requestHandler.content.buf);
+ MyCompletion writeCompletion = new MyCompletion();
+ requestContent.write(buf = ByteBuffer.allocate(69), writeCompletion);
+ assertSame(buf, requestHandler.content.buf);
+ requestHandler.content.writeCompletion.completed();
+ assertTrue(writeCompletion.completed.await(600, TimeUnit.SECONDS));
+
+ MyCompletion closeCompletion = new MyCompletion();
+ requestContent.close(closeCompletion);
+ requestHandler.content.closeCompletion.completed();
+ assertTrue(closeCompletion.completed.await(600, TimeUnit.SECONDS));
+
+ managedHandler.release();
+ assertTrue(requestHandler.destroyed);
+
+ Response response = new Response(Response.Status.OK);
+ ContentChannel responseContent = requestHandler.responseHandler.handleResponse(response);
+ assertNotNull(responseContent);
+
+ responseContent.write(buf = ByteBuffer.allocate(69), null);
+ assertSame(buf, responseHandler.content.buf);
+ responseContent.write(buf = ByteBuffer.allocate(69), writeCompletion = new MyCompletion());
+ assertSame(buf, responseHandler.content.buf);
+ responseHandler.content.writeCompletion.completed();
+ assertTrue(writeCompletion.completed.await(600, TimeUnit.SECONDS));
+
+ responseContent.close(closeCompletion = new MyCompletion());
+ responseHandler.content.closeCompletion.completed();
+ assertTrue(closeCompletion.completed.await(600, TimeUnit.SECONDS));
+
+ assertSame(response, responseHandler.response.get());
+ }
+
+ @Test
+ public void requireThatTimeoutOccursAtExpectedTime() throws InterruptedException {
+ final Context ctx = new Context(MyRequestHandler.newInstance());
+ final MyResponseHandler responseHandler = MyResponseHandler.newInstance();
+
+ ctx.forwardToTime(100);
+ new RequestDispatch() {
+
+ @Override
+ protected Request newRequest() {
+ Request request = new Request(ctx.driver, URI.create(REQUEST_URI));
+ request.setTimeout(300, TimeUnit.MILLISECONDS);
+ return request;
+ }
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ return responseHandler.handleResponse(response);
+ }
+ }.dispatch();
+
+ ctx.forwardToTime(300);
+ assertFalse(responseHandler.await(100, TimeUnit.MILLISECONDS));
+ ctx.forwardToTime(400);
+ assertTrue(responseHandler.await(600, TimeUnit.SECONDS));
+
+ Response response = responseHandler.response.get();
+ assertNotNull(response);
+ assertEquals(Response.Status.REQUEST_TIMEOUT, response.getStatus());
+ assertTrue(ctx.close());
+ }
+
+ @Test
+ public void requireThatQueueEntryIsRemovedWhenResponseHandlerIsCalledBeforeTimeout() {
+ Context ctx = new Context(MyRequestHandler.newInstance());
+ ctx.dispatchRequest(69L, MyResponseHandler.newInstance());
+ assertTrue(ctx.awaitQueueSize(1, 600, TimeUnit.SECONDS));
+ ctx.requestHandler.respond();
+ assertTrue(ctx.awaitQueueSize(0, 600, TimeUnit.SECONDS));
+ assertTrue(ctx.close());
+ }
+
+ @Test
+ public void requireThatNoEntryIsMadeIfTimeoutIsNull() {
+ Context ctx = new Context(MyRequestHandler.newInstance());
+ ctx.dispatchRequest(null, MyResponseHandler.newInstance());
+ assertFalse(ctx.awaitQueueSize(1, 100, TimeUnit.MILLISECONDS));
+ assertTrue(ctx.awaitQueueSize(0, 600, TimeUnit.SECONDS));
+ ctx.requestHandler.respond();
+ assertTrue(ctx.close());
+ }
+
+ @Test
+ public void requireThatNoEntryIsMadeIfHandleRequestCallsHandleResponse() {
+ Context ctx = new Context(MyRequestHandler.newEagerResponse());
+ ctx.dispatchRequest(69L, MyResponseHandler.newInstance());
+ assertFalse(ctx.awaitQueueSize(1, 100, TimeUnit.MILLISECONDS));
+ assertTrue(ctx.awaitQueueSize(0, 600, TimeUnit.SECONDS));
+ assertTrue(ctx.close());
+ }
+
+ @Test
+ public void requireThatNoEntryIsMadeIfTimeoutHandlerHasBeenSet() {
+ final Context ctx = new Context(MyRequestHandler.newInstance());
+ new RequestDispatch() {
+
+ @Override
+ protected Request newRequest() {
+ Request request = new Request(ctx.driver, URI.create(REQUEST_URI));
+ request.setTimeout(10, TimeUnit.MILLISECONDS);
+ request.setTimeoutManager(new TimeoutManager() {
+
+ @Override
+ public void scheduleTimeout(Request request) {
+
+ }
+ });
+ return request;
+ }
+ }.dispatch();
+
+ assertFalse(ctx.awaitQueueSize(1, 100, TimeUnit.MILLISECONDS));
+ assertTrue(ctx.awaitQueueSize(0, 600, TimeUnit.SECONDS));
+ ctx.requestHandler.respond();
+ assertTrue(ctx.close());
+ }
+
+ private static class Context implements Module, Timer {
+
+ final MyRequestHandler requestHandler;
+ final TimeoutManagerImpl timeoutManager;
+ final TestDriver driver;
+ long millis = 0;
+
+ Context(MyRequestHandler requestHandler) {
+ this.requestHandler = requestHandler;
+ this.driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(this);
+
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind(REQUEST_URI, requestHandler);
+ driver.activateContainer(builder);
+
+ Container ref = driver.newReference(URI.create(REQUEST_URI));
+ timeoutManager = ref.getInstance(TimeoutManagerImpl.class);
+ ref.release();
+ }
+
+ void forwardToTime(long millis) {
+ while (this.millis < millis) {
+ this.millis += ScheduledQueue.MILLIS_PER_SLOT;
+ timeoutManager.checkTasks(this.millis);
+ }
+ }
+
+ boolean close() {
+ return driver.close();
+ }
+
+ @Override
+ public void configure(Binder binder) {
+ binder.bind(Timer.class).toInstance(this);
+ }
+
+ @Override
+ public long currentTimeMillis() {
+ return millis;
+ }
+
+ int awaitResponse(Long serverProviderTimeout, MyResponseHandler responseHandler) throws InterruptedException {
+ Long timeout = new MyServerProvider(serverProviderTimeout).dispatchRequest(driver, responseHandler);
+ long timeoutAt;
+ if (timeout == null) {
+ timeoutAt = millis + TimeUnit.SECONDS.toMillis(120);
+ } else {
+ timeoutAt = millis + timeout;
+ }
+ forwardToTime(timeoutAt);
+ if (!responseHandler.await(600, TimeUnit.SECONDS)) {
+ fail("Request handler failed to respond within allocated time.");
+ }
+ return responseHandler.response.get().getStatus();
+ }
+
+ boolean awaitQueueSize(int expectedSize, int timeout, TimeUnit unit) {
+ for (long i = 0, len = unit.toMillis(timeout) / 100; i < len; ++i) {
+ if (timeoutManager.queueSize() == expectedSize) {
+ return true;
+ }
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ fail();
+ }
+ }
+ return false;
+ }
+
+ public Long dispatchRequest(Long serverProviderTimeout, MyResponseHandler responseHandler) {
+ return new MyServerProvider(serverProviderTimeout).dispatchRequest(driver, responseHandler);
+ }
+ }
+
+ private static class MyServerProvider {
+
+ final Long timeout;
+
+ MyServerProvider(Long timeout) {
+ this.timeout = timeout;
+ }
+
+ Long dispatchRequest(CurrentContainer container, ResponseHandler responseHandler) {
+ Request request = null;
+ ContentChannel content = null;
+ try {
+ request = new Request(container, URI.create(REQUEST_URI));
+ if (timeout != null) {
+ request.setTimeout(timeout, TimeUnit.MILLISECONDS);
+ }
+ content = request.connect(responseHandler);
+ } finally {
+ if (request != null) {
+ request.release();
+ }
+ if (content != null) {
+ content.close(null);
+ }
+ }
+ return request.getTimeout(TimeUnit.MILLISECONDS);
+ }
+ }
+
+ private static class MyCompletion implements CompletionHandler {
+
+ final CountDownLatch completed = new CountDownLatch(1);
+
+ @Override
+ public void completed() {
+ completed.countDown();
+ }
+
+ @Override
+ public void failed(Throwable t) {
+
+ }
+ }
+
+ private static class MyContent implements ContentChannel {
+
+ ByteBuffer buf;
+ CompletionHandler writeCompletion;
+ CompletionHandler closeCompletion;
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ this.buf = buf;
+ this.writeCompletion = handler;
+ if (handler != null) {
+ handler.completed();
+ }
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ this.closeCompletion = handler;
+ if (handler != null) {
+ handler.completed();
+ }
+ }
+
+ static MyContent newInstance() {
+ return new MyContent();
+ }
+ }
+
+ private static class MyResponseHandler implements ResponseHandler {
+
+ final AtomicReference<CountDownLatch> latch = new AtomicReference<>(new CountDownLatch(1));
+ final AtomicReference<Response> response = new AtomicReference<>();
+ final MyContent content;
+ final boolean throwException;
+ final boolean interruptThread;
+
+ MyResponseHandler(MyContent content, boolean throwException, boolean interruptThread) {
+ this.content = content;
+ this.throwException = throwException;
+ this.interruptThread = interruptThread;
+ }
+
+ boolean await(long timeout, TimeUnit unit) throws InterruptedException {
+ return latch.get().await(timeout, unit);
+ }
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ if (this.response.getAndSet(response) != null) {
+ throw new IllegalStateException("Response already received.");
+ }
+ latch.get().countDown();
+ if (interruptThread) {
+ Thread.currentThread().interrupt();
+ }
+ if (throwException) {
+ throw new MyException();
+ }
+ return content;
+ }
+
+ static MyResponseHandler newInstance() {
+ return new MyResponseHandler(MyContent.newInstance(), false, false);
+ }
+
+ static MyResponseHandler newResponseDenied() {
+ return new MyResponseHandler(null, false, false);
+ }
+
+ static MyResponseHandler newThrowException() {
+ return new MyResponseHandler(null, true, false);
+ }
+
+ static MyResponseHandler newInterruptThread() {
+ return new MyResponseHandler(MyContent.newInstance(), false, true);
+ }
+ }
+
+ private static class MyRequestHandler extends AbstractResource implements RequestHandler {
+
+ final MyContent content;
+ final Long timeout;
+ int numDenied;
+ int numEager;
+ Request request = null;
+ ResponseHandler responseHandler = null;
+ boolean destroyed = false;
+
+ MyRequestHandler(int numDenied, MyContent content, Long timeout, int numEager) {
+ this.numDenied = numDenied;
+ this.content = content;
+ this.timeout = timeout;
+ this.numEager = numEager;
+ }
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ if (--numDenied >= 0) {
+ throw new RequestDeniedException(request);
+ }
+ this.request = request;
+ this.responseHandler = handler;
+ if (timeout != null) {
+ request.setTimeout(timeout, TimeUnit.MILLISECONDS);
+ }
+ if (--numEager >= 0) {
+ respond();
+ }
+ return content;
+ }
+
+ @Override
+ public void handleTimeout(Request request, ResponseHandler handler) {
+ Response.dispatchTimeout(handler);
+ }
+
+ @Override
+ protected void destroy() {
+ destroyed = true;
+ }
+
+ void respond() {
+ ContentChannel content = responseHandler.handleResponse(new Response(Response.Status.OK));
+ if (content != null) {
+ content.close(null);
+ }
+ }
+
+ static MyRequestHandler newInstance() {
+ return new MyRequestHandler(0, MyContent.newInstance(), null, 0);
+ }
+
+ static MyRequestHandler newTimeoutWithEagerResponse(long millis) {
+ return new MyRequestHandler(0, MyContent.newInstance(), millis, Integer.MAX_VALUE);
+ }
+
+ static MyRequestHandler newFirstRequestDenied() {
+ return new MyRequestHandler(1, MyContent.newInstance(), null, 0);
+ }
+
+ static MyRequestHandler newEagerResponse() {
+ return new MyRequestHandler(0, MyContent.newInstance(), null, Integer.MAX_VALUE);
+ }
+
+ public static MyRequestHandler newNullContent() {
+ return new MyRequestHandler(0, null, null, 0);
+ }
+ }
+
+ private static class MyException extends RuntimeException {
+
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/AbstractContentOutputStreamTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/AbstractContentOutputStreamTestCase.java
new file mode 100644
index 00000000000..d314b303bd4
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/AbstractContentOutputStreamTestCase.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.handler;
+
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class AbstractContentOutputStreamTestCase {
+
+ @Test
+ public void requireThatStreamCanBeWrittenTo() throws IOException {
+ MyOutputStream out = new MyOutputStream();
+ int len = 2 * AbstractContentOutputStream.BUFFERSIZE;
+ for (int i = 0; i < len; ++i) {
+ out.write(69);
+ out.write(new byte[] { });
+ out.write(new byte[] { 6, 9 });
+ out.write(new byte[] { 6, 69, 9 }, 1, 0); // zero length
+ out.write(new byte[] { 6, 69, 9 }, 1, 1);
+ }
+ out.close();
+
+ InputStream in = out.toInputStream();
+ for (int i = 0; i < len; ++i) {
+ assertEquals(69, in.read());
+ assertEquals(6, in.read());
+ assertEquals(9, in.read());
+ assertEquals(69, in.read());
+ }
+ assertEquals(-1, in.read());
+ assertTrue(out.closed);
+ }
+
+ @Test
+ public void requireThatBigBuffersAreWrittenInOrder() throws IOException {
+ MyOutputStream out = new MyOutputStream();
+ out.write(6);
+ out.write(new byte[2 * AbstractContentOutputStream.BUFFERSIZE]);
+ out.write(9);
+ out.close();
+ InputStream in = out.toInputStream();
+ assertEquals(6, in.read());
+ for (int i = 0, len = 2 * AbstractContentOutputStream.BUFFERSIZE; i < len; ++i) {
+ assertEquals(0, in.read());
+ }
+ assertEquals(9, in.read());
+ assertEquals(-1, in.read());
+ assertTrue(out.closed);
+ }
+
+ @Test
+ public void requireThatEmptyBuffersAreNotFlushed() throws Exception {
+ MyOutputStream out = new MyOutputStream();
+ out.close();
+ assertTrue(out.writes.isEmpty());
+ assertTrue(out.closed);
+ }
+
+ @Test
+ public void requireThatNoExcessiveBytesAreWritten() throws Exception {
+ MyOutputStream out = new MyOutputStream();
+ out.write(new byte[] { 6, 9 });
+ out.close();
+
+ InputStream in = out.toInputStream();
+ assertEquals(2, in.available());
+ assertEquals(6, in.read());
+ assertEquals(9, in.read());
+ assertEquals(0, in.available());
+ assertEquals(-1, in.read());
+ assertTrue(out.closed);
+ }
+
+ @Test
+ public void requireThatWrittenArraysAreCopied() throws Exception {
+ MyOutputStream out = new MyOutputStream();
+ byte[] buf = new byte[1];
+ for (byte b = 0; b < 127; ++b) {
+ buf[0] = b;
+ out.write(buf);
+ }
+ out.close();
+
+ InputStream in = out.toInputStream();
+ for (byte b = 0; b < 127; ++b) {
+ assertEquals(b, in.read());
+ }
+ }
+
+ private static class MyOutputStream extends AbstractContentOutputStream {
+
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ final List<ByteBuffer> writes = new ArrayList<>();
+ boolean closed;
+
+ @Override
+ protected void doFlush(ByteBuffer buf) {
+ writes.add(buf);
+ buf = buf.slice();
+ while (buf.hasRemaining()) {
+ out.write(buf.get());
+ }
+ }
+
+ @Override
+ protected void doClose() {
+ closed = true;
+ }
+
+ InputStream toInputStream() {
+ return new ByteArrayInputStream(out.toByteArray());
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/AbstractRequestHandlerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/AbstractRequestHandlerTestCase.java
new file mode 100644
index 00000000000..661165ac5e8
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/AbstractRequestHandlerTestCase.java
@@ -0,0 +1,187 @@
+// 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.application.ContainerBuilder;
+import com.yahoo.jdisc.test.NonWorkingRequest;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class AbstractRequestHandlerTestCase {
+
+ private static final Charset UTF8 = Charset.forName("utf-8");
+ private static int NUM_REQUESTS = 666;
+
+ @Test
+ public void requireThatHandleTimeoutIsImplemented() throws Exception {
+ FutureResponse handler = new FutureResponse();
+ new AbstractRequestHandler() {
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ return null;
+ }
+ }.handleTimeout(NonWorkingRequest.newInstance("http://localhost/"), handler);
+ Response response = handler.get(600, TimeUnit.SECONDS);
+ assertNotNull(response);
+ assertEquals(Response.Status.REQUEST_TIMEOUT, response.getStatus());
+ }
+
+ @Test
+ public void requireThatHelloWorldWorks() throws InterruptedException {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind("http://localhost/", new HelloWorldHandler());
+ driver.activateContainer(builder);
+
+ for (int i = 0; i < NUM_REQUESTS; ++i) {
+ MyResponseHandler responseHandler = new MyResponseHandler();
+ driver.newRequestDispatch("http://localhost/", responseHandler).dispatch();
+
+ ByteBuffer buf = responseHandler.content.read();
+ assertNotNull(buf);
+ assertEquals("Hello World!", new String(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining(), UTF8));
+ assertNull(responseHandler.content.read());
+ }
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatEchoWorks() throws InterruptedException {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind("http://localhost/", new EchoHandler());
+ driver.activateContainer(builder);
+
+ for (int i = 0; i < NUM_REQUESTS; ++i) {
+ MyResponseHandler responseHandler = new MyResponseHandler();
+ RequestDispatch dispatch = driver.newRequestDispatch("http://localhost/", responseHandler);
+ FastContentWriter requestContent = dispatch.connectFastWriter();
+ ByteBuffer buf = ByteBuffer.allocate(69);
+ requestContent.write(buf);
+ requestContent.close();
+
+ assertSame(buf, responseHandler.content.read());
+ assertNull(responseHandler.content.read());
+ }
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatForwardWorks() throws InterruptedException {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind("http://localhost/", new ForwardHandler());
+ builder.clientBindings().bind("http://remotehost/", new EchoHandler());
+ driver.activateContainer(builder);
+
+ for (int i = 0; i < NUM_REQUESTS; ++i) {
+ MyResponseHandler responseHandler = new MyResponseHandler();
+ RequestDispatch dispatch = driver.newRequestDispatch("http://localhost/", responseHandler);
+ FastContentWriter requestContent = dispatch.connectFastWriter();
+ ByteBuffer buf = ByteBuffer.allocate(69);
+ requestContent.write(buf);
+ requestContent.close();
+
+ assertSame(buf, responseHandler.content.read());
+ assertNull(responseHandler.content.read());
+ }
+ assertTrue(driver.close());
+ }
+
+ private static class HelloWorldHandler extends AbstractRequestHandler {
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ FastContentWriter writer = ResponseDispatch.newInstance(Response.Status.OK).connectFastWriter(handler);
+ try {
+ writer.write("Hello World!");
+ } finally {
+ writer.close();
+ }
+ return null;
+ }
+ }
+
+ private static class EchoHandler extends AbstractRequestHandler {
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ return new WritingContentChannel(new FastContentWriter(ResponseDispatch.newInstance(Response.Status.OK).connect(handler)));
+ }
+ }
+
+ private static class ForwardHandler extends AbstractRequestHandler {
+
+ @Override
+ public ContentChannel handleRequest(final Request request, final ResponseHandler handler) {
+ return new WritingContentChannel(new FastContentWriter(new RequestDispatch() {
+
+ @Override
+ public Request newRequest() {
+ return new Request(request, URI.create("http://remotehost/"));
+ }
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ return handler.handleResponse(response);
+ }
+ }.connect()));
+ }
+ }
+
+ private static class WritingContentChannel implements ContentChannel {
+
+ final FastContentWriter writer;
+
+ WritingContentChannel(FastContentWriter writer) {
+ this.writer = writer;
+ }
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ try {
+ writer.write(buf);
+ handler.completed();
+ } catch (Exception e) {
+ handler.failed(e);
+ }
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ try {
+ writer.close();
+ handler.completed();
+ } catch (Exception e) {
+ handler.failed(e);
+ }
+ }
+ }
+
+ private static class MyResponseHandler implements ResponseHandler {
+
+ final ReadableContentChannel content = new ReadableContentChannel();
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ return content;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/BindingNotFoundTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/BindingNotFoundTestCase.java
new file mode 100644
index 00000000000..58900174ee2
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/BindingNotFoundTestCase.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.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class BindingNotFoundTestCase {
+
+ @Test
+ public void requireThatAccessorsWork() {
+ URI uri = URI.create("http://host/path");
+ BindingNotFoundException e = new BindingNotFoundException(uri);
+ assertEquals(uri, e.uri());
+ }
+
+ @Test
+ public void requireThatBindingNotFoundIsThrown() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ driver.activateContainer(driver.newContainerBuilder());
+ Request request = new Request(driver, URI.create("http://host/path"));
+ try {
+ request.connect(new MyResponseHandler());
+ fail();
+ } catch (BindingNotFoundException e) {
+ assertEquals(request.getUri(), e.uri());
+ }
+ request.release();
+ driver.close();
+ }
+
+ private class MyResponseHandler implements ResponseHandler {
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ return null;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/BlockingContentWriterTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/BlockingContentWriterTestCase.java
new file mode 100644
index 00000000000..b539c135edb
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/BlockingContentWriterTestCase.java
@@ -0,0 +1,210 @@
+// 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 org.junit.Test;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.*;
+
+import static org.junit.Assert.fail;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class BlockingContentWriterTestCase {
+
+ @Test
+ public void requireThatContentChannelIsNotNull() {
+ try {
+ new BlockingContentWriter(null);
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatWriteDeliversBuffer() throws InterruptedException {
+ MyContent content = MyContent.newNonBlockingContent();
+ BlockingContentWriter writer = new BlockingContentWriter(content);
+ ByteBuffer buf = ByteBuffer.allocate(69);
+ writer.write(buf);
+ assertSame(buf, content.writeBuf);
+ }
+
+ @Test
+ public void requireThatWriteIsBlocking() throws Exception {
+ MyContent content = MyContent.newBlockingContent();
+ BlockingContentWriter writer = new BlockingContentWriter(content);
+ FutureTask<Boolean> task = new FutureTask<>(new WriteTask(writer, ByteBuffer.allocate(69)));
+ Executors.newSingleThreadExecutor().submit(task);
+ content.writeLatch.await(600, TimeUnit.SECONDS);
+ try {
+ task.get(100, TimeUnit.MILLISECONDS);
+ fail();
+ } catch (TimeoutException e) {
+
+ }
+ content.writeCompletion.completed();
+ assertTrue(task.get(600, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void requireThatWriteExceptionIsThrown() throws Exception {
+ Throwable throwMe = new RuntimeException();
+ try {
+ new BlockingContentWriter(MyContent.newFailedContent(throwMe)).write(ByteBuffer.allocate(69));
+ } catch (Throwable t) {
+ assertSame(throwMe, t);
+ }
+ throwMe = new Error();
+ try {
+ new BlockingContentWriter(MyContent.newFailedContent(throwMe)).write(ByteBuffer.allocate(69));
+ } catch (Throwable t) {
+ assertSame(throwMe, t);
+ }
+ throwMe = new Exception();
+ try {
+ new BlockingContentWriter(MyContent.newFailedContent(throwMe)).write(ByteBuffer.allocate(69));
+ } catch (Throwable t) {
+ assertNotSame(throwMe, t);
+ assertSame(throwMe, t.getCause());
+ }
+ }
+
+ @Test
+ public void requireThatCloseIsBlocking() throws Exception {
+ MyContent content = MyContent.newBlockingContent();
+ BlockingContentWriter writer = new BlockingContentWriter(content);
+ FutureTask<Boolean> task = new FutureTask<>(new CloseTask(writer));
+ Executors.newSingleThreadExecutor().submit(task);
+ content.closeLatch.await(600, TimeUnit.SECONDS);
+ try {
+ task.get(100, TimeUnit.MILLISECONDS);
+ fail();
+ } catch (TimeoutException e) {
+
+ }
+ content.closeCompletion.completed();
+ assertTrue(task.get(600, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void requireThatCloseExceptionIsThrown() throws Exception {
+ Throwable throwMe = new RuntimeException();
+ try {
+ new BlockingContentWriter(MyContent.newFailedContent(throwMe)).close();
+ } catch (Throwable t) {
+ assertSame(throwMe, t);
+ }
+ throwMe = new Error();
+ try {
+ new BlockingContentWriter(MyContent.newFailedContent(throwMe)).close();
+ } catch (Throwable t) {
+ assertSame(throwMe, t);
+ }
+ throwMe = new Exception();
+ try {
+ new BlockingContentWriter(MyContent.newFailedContent(throwMe)).close();
+ } catch (Throwable t) {
+ assertNotSame(throwMe, t);
+ assertSame(throwMe, t.getCause());
+ }
+ }
+
+ private static class MyContent implements ContentChannel {
+
+ final CountDownLatch writeLatch = new CountDownLatch(1);
+ final CountDownLatch closeLatch = new CountDownLatch(1);
+ final Throwable eagerFailure;
+ final boolean eagerCompletion;
+ CompletionHandler writeCompletion;
+ CompletionHandler closeCompletion;
+ ByteBuffer writeBuf;
+
+ MyContent(boolean eagerCompletion, Throwable eagerFailure) {
+ this.eagerCompletion = eagerCompletion;
+ this.eagerFailure = eagerFailure;
+ }
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ writeBuf = buf;
+ if (eagerFailure != null) {
+ handler.failed(eagerFailure);
+ } else if (eagerCompletion) {
+ handler.completed();
+ } else {
+ writeCompletion = handler;
+ }
+ writeLatch.countDown();
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ if (eagerFailure != null) {
+ handler.failed(eagerFailure);
+ } else if (eagerCompletion) {
+ handler.completed();
+ } else {
+ closeCompletion = handler;
+ }
+ closeLatch.countDown();
+ }
+
+ static MyContent newBlockingContent() {
+ return new MyContent(false, null);
+ }
+
+ static MyContent newNonBlockingContent() {
+ return new MyContent(true, null);
+ }
+
+ static MyContent newFailedContent(Throwable e) {
+ return new MyContent(false, e);
+ }
+ }
+
+ private static class WriteTask implements Callable<Boolean> {
+
+ final BlockingContentWriter writer;
+ final ByteBuffer buf;
+
+ WriteTask(BlockingContentWriter writer, ByteBuffer buf) {
+ this.writer = writer;
+ this.buf = buf;
+ }
+
+ @Override
+ public Boolean call() {
+ try {
+ writer.write(buf);
+ } catch (Throwable t) {
+ return false;
+ }
+ return true;
+ }
+ }
+
+ private static class CloseTask implements Callable<Boolean> {
+
+ final BlockingContentWriter writer;
+
+ CloseTask(BlockingContentWriter writer) {
+ this.writer = writer;
+ }
+
+ @Override
+ public Boolean call() {
+ try {
+ writer.close();
+ } catch (Throwable t) {
+ return false;
+ }
+ return true;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/BufferedContentChannelTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/BufferedContentChannelTestCase.java
new file mode 100644
index 00000000000..c6714f11203
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/BufferedContentChannelTestCase.java
@@ -0,0 +1,257 @@
+// 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 org.junit.Test;
+
+import java.nio.ByteBuffer;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.*;
+
+import static org.junit.Assert.fail;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class BufferedContentChannelTestCase {
+
+ @Test
+ public void requireThatIsConnectedWorks() {
+ MyContent target = new MyContent();
+ BufferedContentChannel content = new BufferedContentChannel();
+ assertFalse(content.isConnected());
+ content.connectTo(target);
+ assertTrue(content.isConnected());
+ }
+
+ @Test
+ public void requireThatConnectToNullThrowsException() {
+ BufferedContentChannel content = new BufferedContentChannel();
+ try {
+ content.connectTo(null);
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatWriteAfterCloseThrowsException() {
+ BufferedContentChannel content = new BufferedContentChannel();
+ content.close(null);
+ try {
+ content.write(ByteBuffer.allocate(69), null);
+ fail();
+ } catch (IllegalStateException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatCloseAfterCloseThrowsException() {
+ BufferedContentChannel content = new BufferedContentChannel();
+ content.close(null);
+ try {
+ content.close(null);
+ fail();
+ } catch (IllegalStateException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatConnecToAfterConnecToThrowsException() {
+ BufferedContentChannel content = new BufferedContentChannel();
+ content.connectTo(new MyContent());
+ try {
+ content.connectTo(new MyContent());
+ fail();
+ } catch (IllegalStateException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatWriteBeforeConnectToWritesToTarget() {
+ BufferedContentChannel content = new BufferedContentChannel();
+ ByteBuffer buf = ByteBuffer.allocate(69);
+ MyCompletion completion = new MyCompletion();
+ content.write(buf, completion);
+ MyContent target = new MyContent();
+ content.connectTo(target);
+ assertSame(buf, target.writeBuf);
+ assertSame(completion, target.writeCompletion);
+ }
+
+ @Test
+ public void requireThatWriteAfterConnectToWritesToTarget() {
+ MyContent target = new MyContent();
+ BufferedContentChannel content = new BufferedContentChannel();
+ content.connectTo(target);
+ ByteBuffer buf = ByteBuffer.allocate(69);
+ MyCompletion completion = new MyCompletion();
+ content.write(buf, completion);
+ assertSame(buf, target.writeBuf);
+ assertSame(completion, target.writeCompletion);
+ }
+
+ @Test
+ public void requireThatCloseBeforeConnectToClosesTarget() {
+ BufferedContentChannel content = new BufferedContentChannel();
+ MyCompletion completion = new MyCompletion();
+ content.close(completion);
+ MyContent target = new MyContent();
+ content.connectTo(target);
+ assertTrue(target.closed);
+ assertSame(completion, target.closeCompletion);
+ }
+
+ @Test
+ public void requireThatCloseAfterConnectToClosesTarget() {
+ MyContent target = new MyContent();
+ BufferedContentChannel content = new BufferedContentChannel();
+ content.connectTo(target);
+ MyCompletion completion = new MyCompletion();
+ content.close(completion);
+ assertTrue(target.closed);
+ assertSame(completion, target.closeCompletion);
+ }
+
+ @Test
+ public void requireThatIsConnectedIsTrueWhenConnectedBeforeClose() {
+ BufferedContentChannel content = new BufferedContentChannel();
+ assertFalse(content.isConnected());
+ content.connectTo(new MyContent());
+ assertTrue(content.isConnected());
+ content.close(null);
+ assertTrue(content.isConnected());
+ }
+
+ @Test
+ public void requireThatIsConnectedIsTrueWhenClosedBeforeConnected() {
+ BufferedContentChannel content = new BufferedContentChannel();
+ assertFalse(content.isConnected());
+ content.close(null);
+ assertFalse(content.isConnected());
+ content.connectTo(new MyContent());
+ assertTrue(content.isConnected());
+ }
+
+ @Test
+ public void requireThatContentIsThreadSafe() throws Exception {
+ ExecutorService executor = Executors.newFixedThreadPool(101);
+ for (int run = 0; run < 69; ++run) {
+ List<ByteBuffer> bufs = new LinkedList<>();
+ for (int buf = 0; buf < 100; ++buf) {
+ bufs.add(ByteBuffer.allocate(buf));
+ }
+ BufferedContentChannel content = new BufferedContentChannel();
+ List<Callable<Boolean>> tasks = new LinkedList<>();
+ for (ByteBuffer buf : bufs) {
+ tasks.add(new WriteTask(content, buf));
+ }
+ MyConcurrentContent target = new MyConcurrentContent();
+ tasks.add(new ConnectTask(content, target));
+ List<Future<Boolean>> results = executor.invokeAll(tasks);
+ for (Future<Boolean> result : results) {
+ assertTrue(result.get());
+ }
+ assertEquals(bufs.size(), target.bufs.size());
+ for (ByteBuffer buf : target.bufs) {
+ assertTrue(bufs.remove(buf));
+ }
+ assertTrue(bufs.isEmpty());
+ }
+ }
+
+ private static class WriteTask implements Callable<Boolean> {
+
+ final Random rnd = new Random();
+ final BufferedContentChannel content;
+ final ByteBuffer buf;
+
+ WriteTask(BufferedContentChannel content, ByteBuffer buf) {
+ this.content = content;
+ this.buf = buf;
+ }
+
+ @Override
+ public Boolean call() throws Exception {
+ if (rnd.nextBoolean()) {
+ Thread.sleep(rnd.nextInt(5));
+ }
+ content.write(buf, null);
+ return Boolean.TRUE;
+ }
+ }
+
+ private static class ConnectTask implements Callable<Boolean> {
+
+ final BufferedContentChannel content;
+ final ContentChannel target;
+
+ ConnectTask(BufferedContentChannel content, ContentChannel target) {
+ this.content = content;
+ this.target = target;
+ }
+
+ @Override
+ public Boolean call() throws Exception {
+ content.connectTo(target);
+ return Boolean.TRUE;
+ }
+ }
+
+ private static class MyContent implements ContentChannel {
+
+ ByteBuffer writeBuf = null;
+ CompletionHandler writeCompletion;
+ CompletionHandler closeCompletion;
+ boolean closed = false;
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ writeBuf = buf;
+ writeCompletion = handler;
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ closeCompletion = handler;
+ closed = true;
+ }
+ }
+
+ private static class MyConcurrentContent implements ContentChannel {
+
+ ConcurrentLinkedQueue<ByteBuffer> bufs = new ConcurrentLinkedQueue<>();
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ bufs.add(buf);
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+
+ }
+ }
+
+ private static class MyCompletion implements CompletionHandler {
+
+ @Override
+ public void completed() {
+
+ }
+
+ @Override
+ public void failed(Throwable throwable) {
+
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/CallableRequestDispatchTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/CallableRequestDispatchTestCase.java
new file mode 100644
index 00000000000..d2768707528
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/CallableRequestDispatchTestCase.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.handler;
+
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class CallableRequestDispatchTestCase {
+
+ @Test
+ public void requireThatDispatchIsCalled() throws Exception {
+ final TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ Response response = new Response(Response.Status.OK);
+ builder.serverBindings().bind("http://host/path", new MyRequestHandler(response));
+ driver.activateContainer(builder);
+ assertSame(response, new CallableRequestDispatch() {
+
+ @Override
+ protected Request newRequest() {
+ return new Request(driver, URI.create("http://host/path"));
+ }
+ }.call());
+ assertTrue(driver.close());
+ }
+
+ private static class MyRequestHandler extends AbstractRequestHandler {
+
+ final Response response;
+
+ MyRequestHandler(Response response) {
+ this.response = response;
+ }
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ ResponseDispatch.newInstance(response).dispatch(handler);
+ return null;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/CallableResponseDispatchTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/CallableResponseDispatchTestCase.java
new file mode 100644
index 00000000000..9b107f93178
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/CallableResponseDispatchTestCase.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.handler;
+
+import com.yahoo.jdisc.Response;
+import org.junit.Test;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertSame;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class CallableResponseDispatchTestCase {
+
+ @Test
+ public void requireThatDispatchIsCalled() throws Exception {
+ final Response response = new Response(Response.Status.OK);
+ FutureResponse handler = new FutureResponse();
+ new CallableResponseDispatch(handler) {
+
+ @Override
+ protected Response newResponse() {
+ return response;
+ }
+ }.call();
+ assertSame(response, handler.get(600, TimeUnit.SECONDS));
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ContentInputStreamTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ContentInputStreamTestCase.java
new file mode 100644
index 00000000000..618c1b1ed1c
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ContentInputStreamTestCase.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 org.junit.Test;
+
+import java.util.concurrent.Future;
+
+import static org.junit.Assert.assertTrue;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ContentInputStreamTestCase {
+
+ @Test
+ public void requireThatContentInputStreamExtendsUnsafeContentInputStream() {
+ assertTrue(UnsafeContentInputStream.class.isAssignableFrom(ContentInputStream.class));
+ }
+
+ @Test
+ @SuppressWarnings("FinalizeCalledExplicitly")
+ public void requireThatFinalizerClosesStream() throws Throwable {
+ BufferedContentChannel channel = new BufferedContentChannel();
+ FastContentWriter writer = new FastContentWriter(channel);
+ writer.write("foo");
+ writer.close();
+
+ new ContentInputStream(channel.toReadable()).finalize();
+ }
+
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FastContentOutputStreamTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FastContentOutputStreamTestCase.java
new file mode 100644
index 00000000000..00ea92ff246
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FastContentOutputStreamTestCase.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.handler;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class FastContentOutputStreamTestCase {
+
+ @Test
+ public void requireThatNullConstructorArgumentThrows() {
+ try {
+ new FastContentOutputStream((ContentChannel)null);
+ fail();
+ } catch (NullPointerException e) {
+ assertEquals("out", e.getMessage());
+ }
+ try {
+ new FastContentOutputStream((FastContentWriter)null);
+ fail();
+ } catch (NullPointerException e) {
+ assertEquals("out", e.getMessage());
+ }
+ }
+
+ @Test
+ public void requireThatAllMethodsDelegateToWriter() throws Exception {
+ FastContentWriter writer = Mockito.mock(FastContentWriter.class);
+ FastContentOutputStream out = new FastContentOutputStream(writer);
+
+ out.write(new byte[] { 6, 9 });
+ out.flush();
+ Mockito.verify(writer).write(Mockito.any(ByteBuffer.class));
+
+ out.close();
+ Mockito.verify(writer).close();
+
+ out.cancel(true);
+ Mockito.verify(writer).cancel(true);
+ out.cancel(false);
+ Mockito.verify(writer).cancel(false);
+
+ out.isCancelled();
+ Mockito.verify(writer).isCancelled();
+
+ out.isDone();
+ Mockito.verify(writer).isDone();
+
+ out.get();
+ Mockito.verify(writer).get();
+
+ out.get(600, TimeUnit.SECONDS);
+ Mockito.verify(writer).get(600, TimeUnit.SECONDS);
+
+ Runnable listener = Mockito.mock(Runnable.class);
+ Executor executor = Mockito.mock(Executor.class);
+ out.addListener(listener, executor);
+ Mockito.verify(writer).addListener(listener, executor);
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FastContentWriterTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FastContentWriterTestCase.java
new file mode 100644
index 00000000000..e3bedaf5c2a
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FastContentWriterTestCase.java
@@ -0,0 +1,241 @@
+// 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.MoreExecutors;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Random;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static org.junit.Assert.fail;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertArrayEquals;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class FastContentWriterTestCase {
+
+ @Test
+ public void requireThatContentCanBeWritten() throws ExecutionException, InterruptedException {
+ ReadableContentChannel content = new ReadableContentChannel();
+ FastContentWriter out = new FastContentWriter(content);
+
+ ByteBuffer foo = ByteBuffer.allocate(69);
+ out.write(foo);
+ ByteBuffer bar = ByteBuffer.allocate(69);
+ out.write(bar);
+ out.close();
+
+ assertFalse(out.isDone());
+ assertSame(foo, content.read());
+ assertFalse(out.isDone());
+ assertSame(bar, content.read());
+ assertFalse(out.isDone());
+ assertNull(content.read());
+ assertTrue(out.isDone());
+ }
+
+ @Test
+ public void requireThatStringsAreUtf8Encoded() {
+ ReadableContentChannel content = new ReadableContentChannel();
+ FastContentWriter out = new FastContentWriter(content);
+
+ String in = "\u6211\u80FD\u541E\u4E0B\u73BB\u7483\u800C\u4E0D\u4F24\u8EAB\u4F53\u3002";
+ out.write(in);
+ out.close();
+
+ ByteBuffer buf = content.read();
+ byte[] arr = new byte[buf.remaining()];
+ buf.get(arr);
+ assertArrayEquals(in.getBytes(StandardCharsets.UTF_8), arr);
+ }
+
+ @Test
+ public void requireThatCancelThrowsUnsupportedOperation() {
+ try {
+ new FastContentWriter(Mockito.mock(ContentChannel.class)).cancel(true);
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatCancelIsAlwaysFalse() {
+ FastContentWriter writer = new FastContentWriter(Mockito.mock(ContentChannel.class));
+ assertFalse(writer.isCancelled());
+ try {
+ writer.cancel(true);
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ assertFalse(writer.isCancelled());
+ }
+
+ @Test
+ public void requireThatGetThrowsTimeoutUntilCloseCompletionHandlerIsCalled() throws Exception {
+ ReadableContentChannel buf = new ReadableContentChannel();
+ FastContentWriter out = new FastContentWriter(buf);
+
+ out.write(new byte[] { 6, 9 });
+ assertFalse(out.isDone());
+ try {
+ out.get(100, TimeUnit.MILLISECONDS);
+ fail();
+ } catch (TimeoutException e) {
+
+ }
+
+ assertNotNull(buf.read());
+ assertFalse(out.isDone());
+ try {
+ out.get(100, TimeUnit.MILLISECONDS);
+ fail();
+ } catch (TimeoutException e) {
+
+ }
+
+ out.close();
+ assertFalse(out.isDone());
+ try {
+ out.get(100, TimeUnit.MILLISECONDS);
+ fail();
+ } catch (TimeoutException e) {
+
+ }
+
+ assertNull(buf.read());
+ assertTrue(out.isDone());
+ assertTrue(out.get(600, TimeUnit.SECONDS));
+ assertTrue(out.get());
+ }
+
+ @Test
+ public void requireThatSyncWriteExceptionFailsFuture() throws InterruptedException {
+ IllegalStateException expected = new IllegalStateException();
+ ContentChannel content = Mockito.mock(ContentChannel.class);
+ Mockito.doThrow(expected)
+ .when(content).write(Mockito.any(ByteBuffer.class), Mockito.any(CompletionHandler.class));
+ FastContentWriter out = new FastContentWriter(content);
+ try {
+ out.write("foo");
+ fail();
+ } catch (Throwable t) {
+ assertSame(expected, t);
+ }
+ try {
+ out.get();
+ fail();
+ } catch (ExecutionException e) {
+ assertSame(expected, e.getCause());
+ }
+ }
+
+ @Test
+ public void requireThatSyncCloseExceptionFailsFuture() throws InterruptedException {
+ IllegalStateException expected = new IllegalStateException();
+ ContentChannel content = Mockito.mock(ContentChannel.class);
+ Mockito.doThrow(expected)
+ .when(content).close(Mockito.any(CompletionHandler.class));
+ FastContentWriter out = new FastContentWriter(content);
+ try {
+ out.close();
+ fail();
+ } catch (Throwable t) {
+ assertSame(expected, t);
+ }
+ try {
+ out.get();
+ fail();
+ } catch (ExecutionException e) {
+ assertSame(expected, e.getCause());
+ }
+ }
+
+ @Test
+ public void requireThatAsyncExceptionFailsFuture() throws InterruptedException {
+ IllegalStateException expected = new IllegalStateException();
+ ReadableContentChannel content = new ReadableContentChannel();
+ FastContentWriter out = new FastContentWriter(content);
+ out.write("foo");
+ content.failed(expected);
+ try {
+ out.get();
+ fail();
+ } catch (ExecutionException e) {
+ assertSame(expected, e.getCause());
+ }
+ }
+
+ @Test
+ public void requireThatWriterCanBeListenedTo() throws InterruptedException {
+ ReadableContentChannel buf = new ReadableContentChannel();
+ FastContentWriter out = new FastContentWriter(buf);
+ RunnableLatch listener = new RunnableLatch();
+ out.addListener(listener, MoreExecutors.sameThreadExecutor());
+
+ out.write(new byte[] { 6, 9 });
+ assertFalse(listener.await(100, TimeUnit.MILLISECONDS));
+ assertNotNull(buf.read());
+ assertFalse(listener.await(100, TimeUnit.MILLISECONDS));
+ out.close();
+ assertFalse(listener.await(100, TimeUnit.MILLISECONDS));
+ assertNull(buf.read());
+ assertTrue(listener.await(600, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void requireThatWriterIsThreadSafe() throws Exception {
+ final CountDownLatch latch = new CountDownLatch(2);
+ final ReadableContentChannel content = new ReadableContentChannel();
+ Future<Integer> read = Executors.newSingleThreadExecutor().submit(new Callable<Integer>() {
+
+ @Override
+ public Integer call() throws Exception {
+ latch.countDown();
+ latch.await(600, TimeUnit.SECONDS);
+
+ int bufCnt = 0;
+ while (content.read() != null) {
+ ++bufCnt;
+ }
+ return bufCnt;
+ }
+ });
+ Future<Integer> write = Executors.newSingleThreadExecutor().submit(new Callable<Integer>() {
+
+ @Override
+ public Integer call() throws Exception {
+ FastContentWriter out = new FastContentWriter(content);
+ ByteBuffer buf = ByteBuffer.wrap(new byte[69]);
+ int bufCnt = 4096 + new Random().nextInt(4096);
+
+ latch.countDown();
+ latch.await(600, TimeUnit.SECONDS);
+ for (int i = 0; i < bufCnt; ++i) {
+ out.write(buf.slice());
+ }
+ out.close();
+ return bufCnt;
+ }
+ });
+ assertEquals(read.get(600, TimeUnit.SECONDS),
+ write.get(600, TimeUnit.SECONDS));
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureCompletionTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureCompletionTestCase.java
new file mode 100644
index 00000000000..e886d663a72
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureCompletionTestCase.java
@@ -0,0 +1,106 @@
+// 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.MoreExecutors;
+import org.junit.Test;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static org.junit.Assert.fail;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class FutureCompletionTestCase {
+
+ @Test
+ public void requireThatCancelIsUnsupported() {
+ FutureCompletion future = new FutureCompletion();
+ assertFalse(future.isCancelled());
+ try {
+ future.cancel(true);
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ assertFalse(future.isCancelled());
+ try {
+ future.cancel(false);
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ assertFalse(future.isCancelled());
+ }
+
+ @Test
+ public void requireThatCompletedReturnsTrue() throws Exception {
+ FutureCompletion future = new FutureCompletion();
+ try {
+ future.get(0, TimeUnit.MILLISECONDS);
+ fail();
+ } catch (TimeoutException e) {
+
+ }
+ future.completed();
+ assertTrue(future.get(0, TimeUnit.MILLISECONDS));
+ assertTrue(future.get());
+ }
+
+ @Test
+ public void requireThatCompletionIsDoneWhenCompleted() {
+ FutureCompletion future = new FutureCompletion();
+ assertFalse(future.isDone());
+ future.completed();
+ assertTrue(future.isDone());
+ }
+
+ @Test
+ public void requireThatCompletionIsDoneWhenFailed() {
+ FutureCompletion future = new FutureCompletion();
+ assertFalse(future.isDone());
+ future.failed(new Throwable());
+ assertTrue(future.isDone());
+ }
+
+ @Test
+ public void requireThatFailedCauseIsRethrown() throws Exception {
+ FutureCompletion future = new FutureCompletion();
+ Throwable t = new Throwable();
+ future.failed(t);
+ try {
+ future.get(0, TimeUnit.SECONDS);
+ fail();
+ } catch (ExecutionException e) {
+ assertSame(t, e.getCause());
+ }
+ try {
+ future.get();
+ fail();
+ } catch (ExecutionException e) {
+ assertSame(t, e.getCause());
+ }
+ }
+
+ @Test
+ public void requireThatCompletionCanBeListenedTo() throws InterruptedException {
+ FutureCompletion completion = new FutureCompletion();
+ RunnableLatch listener = new RunnableLatch();
+ completion.addListener(listener, MoreExecutors.sameThreadExecutor());
+ assertFalse(listener.await(100, TimeUnit.MILLISECONDS));
+ completion.completed();
+ assertTrue(listener.await(600, TimeUnit.SECONDS));
+
+ completion = new FutureCompletion();
+ listener = new RunnableLatch();
+ completion.addListener(listener, MoreExecutors.sameThreadExecutor());
+ assertFalse(listener.await(100, TimeUnit.MILLISECONDS));
+ completion.failed(new Throwable());
+ assertTrue(listener.await(600, TimeUnit.SECONDS));
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureConjunctionTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureConjunctionTestCase.java
new file mode 100644
index 00000000000..3916eb7ffd6
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureConjunctionTestCase.java
@@ -0,0 +1,255 @@
+// 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.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import org.junit.Test;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static org.junit.Assert.fail;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class FutureConjunctionTestCase {
+
+ private final ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+
+ @Test
+ public void requireThatAllFuturesAreWaitedFor() throws Exception {
+ final CountDownLatch latch = new CountDownLatch(1);
+ FutureConjunction future = new FutureConjunction();
+ future.addOperand(executor.submit(new Callable<Boolean>() {
+
+ @Override
+ public Boolean call() throws Exception {
+ return latch.await(600, TimeUnit.SECONDS);
+ }
+ }));
+ try {
+ future.get(100, TimeUnit.MILLISECONDS);
+ fail();
+ } catch (TimeoutException e) {
+
+ }
+ latch.countDown();
+ assertTrue(future.get(600, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void requireThatGetReturnValueIsAConjunction() throws Exception {
+ assertTrue(tryGet(true));
+ assertTrue(tryGet(true, true));
+ assertTrue(tryGet(true, true, true));
+
+ assertFalse(tryGet(false));
+ assertFalse(tryGet(false, true));
+ assertFalse(tryGet(true, false));
+ assertFalse(tryGet(false, true, true));
+ assertFalse(tryGet(true, false, true));
+ assertFalse(tryGet(true, true, false));
+ assertFalse(tryGet(false, false, true));
+ assertFalse(tryGet(false, true, false));
+ assertFalse(tryGet(true, false, false));
+ }
+
+ @Test
+ public void requireThatIsDoneReturnValueIsAConjunction() {
+ assertTrue(tryIsDone(true));
+ assertTrue(tryIsDone(true, true));
+ assertTrue(tryIsDone(true, true, true));
+
+ assertFalse(tryIsDone(false));
+ assertFalse(tryIsDone(false, true));
+ assertFalse(tryIsDone(true, false));
+ assertFalse(tryIsDone(false, true, true));
+ assertFalse(tryIsDone(true, false, true));
+ assertFalse(tryIsDone(true, true, false));
+ assertFalse(tryIsDone(false, false, true));
+ assertFalse(tryIsDone(false, true, false));
+ assertFalse(tryIsDone(true, false, false));
+ }
+
+ @Test
+ public void requireThatCancelReturnValueIsAConjuction() {
+ assertTrue(tryCancel(true));
+ assertTrue(tryCancel(true, true));
+ assertTrue(tryCancel(true, true, true));
+
+ assertFalse(tryCancel(false));
+ assertFalse(tryCancel(false, true));
+ assertFalse(tryCancel(true, false));
+ assertFalse(tryCancel(false, true, true));
+ assertFalse(tryCancel(true, false, true));
+ assertFalse(tryCancel(true, true, false));
+ assertFalse(tryCancel(false, false, true));
+ assertFalse(tryCancel(false, true, false));
+ assertFalse(tryCancel(true, false, false));
+ }
+
+ @Test
+ public void requireThatIsCancelledReturnValueIsAConjuction() {
+ assertTrue(tryIsCancelled(true));
+ assertTrue(tryIsCancelled(true, true));
+ assertTrue(tryIsCancelled(true, true, true));
+
+ assertFalse(tryIsCancelled(false));
+ assertFalse(tryIsCancelled(false, true));
+ assertFalse(tryIsCancelled(true, false));
+ assertFalse(tryIsCancelled(false, true, true));
+ assertFalse(tryIsCancelled(true, false, true));
+ assertFalse(tryIsCancelled(true, true, false));
+ assertFalse(tryIsCancelled(false, false, true));
+ assertFalse(tryIsCancelled(false, true, false));
+ assertFalse(tryIsCancelled(true, false, false));
+ }
+
+ @Test
+ public void requireThatConjunctionCanBeListenedTo() throws InterruptedException {
+ FutureConjunction conjunction = new FutureConjunction();
+ RunnableLatch listener = new RunnableLatch();
+ conjunction.addListener(listener, MoreExecutors.sameThreadExecutor());
+ assertTrue(listener.await(600, TimeUnit.SECONDS));
+
+ conjunction = new FutureConjunction();
+ FutureBoolean foo = new FutureBoolean();
+ conjunction.addOperand(foo);
+ FutureBoolean bar = new FutureBoolean();
+ conjunction.addOperand(bar);
+ listener = new RunnableLatch();
+ conjunction.addListener(listener, MoreExecutors.sameThreadExecutor());
+ assertFalse(listener.await(100, TimeUnit.MILLISECONDS));
+ foo.set(true);
+ assertFalse(listener.await(100, TimeUnit.MILLISECONDS));
+ bar.set(true);
+ assertTrue(listener.await(600, TimeUnit.SECONDS));
+
+ conjunction = new FutureConjunction();
+ foo = new FutureBoolean();
+ conjunction.addOperand(foo);
+ bar = new FutureBoolean();
+ conjunction.addOperand(bar);
+ listener = new RunnableLatch();
+ conjunction.addListener(listener, MoreExecutors.sameThreadExecutor());
+ assertFalse(listener.await(100, TimeUnit.MILLISECONDS));
+ bar.set(true);
+ assertFalse(listener.await(100, TimeUnit.MILLISECONDS));
+ foo.set(true);
+ assertTrue(listener.await(600, TimeUnit.SECONDS));
+ }
+
+ private static boolean tryGet(boolean... operands) throws Exception {
+ FutureConjunction foo = new FutureConjunction();
+ FutureConjunction bar = new FutureConjunction();
+ for (boolean op : operands) {
+ foo.addOperand(MyFuture.newInstance(op));
+ bar.addOperand(MyFuture.newInstance(op));
+ }
+ boolean fooResult = foo.get();
+ boolean barResult = foo.get(0, TimeUnit.SECONDS);
+ assertEquals(fooResult, barResult);
+ return fooResult;
+ }
+
+ private static boolean tryIsDone(boolean... operands) {
+ FutureConjunction foo = new FutureConjunction();
+ for (boolean op : operands) {
+ foo.addOperand(MyFuture.newIsDone(op));
+ }
+ return foo.isDone();
+ }
+
+ private static boolean tryCancel(boolean... operands) {
+ FutureConjunction foo = new FutureConjunction();
+ FutureConjunction bar = new FutureConjunction();
+ for (boolean op : operands) {
+ foo.addOperand(MyFuture.newCanCancel(op));
+ bar.addOperand(MyFuture.newCanCancel(op));
+ }
+ boolean fooResult = foo.cancel(true);
+ boolean barResult = foo.cancel(false);
+ assertEquals(fooResult, barResult);
+ return fooResult;
+ }
+
+ private static boolean tryIsCancelled(boolean... operands) {
+ FutureConjunction foo = new FutureConjunction();
+ for (boolean op : operands) {
+ foo.addOperand(MyFuture.newIsCancelled(op));
+ }
+ return foo.isCancelled();
+ }
+
+ private static class FutureBoolean extends AbstractFuture<Boolean> {
+
+ public boolean set(Boolean val) {
+ return super.set(val);
+ }
+ }
+
+ private static class MyFuture extends AbstractFuture<Boolean> {
+
+ final boolean value;
+ final boolean isDone;
+ final boolean canCancel;
+ final boolean isCancelled;
+
+ MyFuture(boolean value, boolean isDone, boolean canCancel, boolean isCancelled) {
+ this.value = value;
+ this.isDone = isDone;
+ this.canCancel = canCancel;
+ this.isCancelled = isCancelled;
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ return canCancel;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return isCancelled;
+ }
+
+ @Override
+ public boolean isDone() {
+ return isDone;
+ }
+
+ @Override
+ public Boolean get() {
+ return value;
+ }
+
+ @Override
+ public Boolean get(long timeout, TimeUnit unit) {
+ return value;
+ }
+
+ static ListenableFuture<Boolean> newInstance(boolean value) {
+ return new MyFuture(value, false, false, false);
+ }
+
+ static ListenableFuture<Boolean> newIsDone(boolean isDone) {
+ return new MyFuture(false, isDone, false, false);
+ }
+
+ static ListenableFuture<Boolean> newCanCancel(boolean canCancel) {
+ return new MyFuture(false, false, canCancel, false);
+ }
+
+ static ListenableFuture<Boolean> newIsCancelled(boolean isCancelled) {
+ return new MyFuture(false, false, false, isCancelled);
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureResponseTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureResponseTestCase.java
new file mode 100644
index 00000000000..925c09f763d
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/FutureResponseTestCase.java
@@ -0,0 +1,81 @@
+// 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.MoreExecutors;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.test.NonWorkingContentChannel;
+import org.junit.Test;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static org.junit.Assert.fail;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class FutureResponseTestCase {
+
+ @Test
+ public void requireThatCancelIsUnsupported() {
+ FutureResponse future = new FutureResponse();
+ assertFalse(future.isCancelled());
+ try {
+ future.cancel(true);
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ assertFalse(future.isCancelled());
+ try {
+ future.cancel(false);
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ assertFalse(future.isCancelled());
+ }
+
+ @Test
+ public void requireThatCompletionIsDoneWhenHandlerIsCalled() {
+ FutureResponse future = new FutureResponse();
+ assertFalse(future.isDone());
+ future.handleResponse(new Response(69));
+ assertTrue(future.isDone());
+ }
+
+ @Test
+ public void requireThatResponseBecomesAvailable() throws Exception {
+ FutureResponse future = new FutureResponse();
+ try {
+ future.get(0, TimeUnit.MILLISECONDS);
+ fail();
+ } catch (TimeoutException e) {
+
+ }
+ Response response = new Response(Response.Status.OK);
+ future.handleResponse(response);
+ assertSame(response, future.get(0, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void requireThatResponseContentIsReturnedToCaller() throws Exception {
+ ContentChannel content = new NonWorkingContentChannel();
+ FutureResponse future = new FutureResponse(content);
+ Response response = new Response(Response.Status.OK);
+ assertSame(content, future.handleResponse(response));
+ }
+
+ @Test
+ public void requireThatResponseCanBeListenedTo() throws InterruptedException {
+ FutureResponse response = new FutureResponse();
+ RunnableLatch listener = new RunnableLatch();
+ response.addListener(listener, MoreExecutors.sameThreadExecutor());
+ assertFalse(listener.await(100, TimeUnit.MILLISECONDS));
+ response.handleResponse(new Response(Response.Status.OK));
+ assertTrue(listener.await(600, TimeUnit.SECONDS));
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/NullContentTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/NullContentTestCase.java
new file mode 100644
index 00000000000..40f6cd88216
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/NullContentTestCase.java
@@ -0,0 +1,48 @@
+// 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 org.junit.Test;
+import org.mockito.Mockito;
+
+import java.nio.ByteBuffer;
+
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class NullContentTestCase {
+
+ @Test
+ public void requireThatWriteThrowsException() {
+ CompletionHandler completion = Mockito.mock(CompletionHandler.class);
+ try {
+ NullContent.INSTANCE.write(ByteBuffer.allocate(69), completion);
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ Mockito.verifyZeroInteractions(completion);
+ }
+
+ @Test
+ public void requireThatWriteEmptyDoesNotThrowException() {
+ CompletionHandler completion = Mockito.mock(CompletionHandler.class);
+ NullContent.INSTANCE.write(ByteBuffer.allocate(0), completion);
+ Mockito.verify(completion).completed();
+ Mockito.verifyNoMoreInteractions(completion);
+ }
+
+ @Test
+ public void requireThatCloseCallsCompletion() {
+ CompletionHandler completion = Mockito.mock(CompletionHandler.class);
+ NullContent.INSTANCE.close(completion);
+ Mockito.verify(completion).completed();
+ Mockito.verifyNoMoreInteractions(completion);
+ }
+
+ @Test
+ public void requireThatCloseWithoutCompletionDoesNotThrow() {
+ NullContent.INSTANCE.close(null);
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ReadableContentChannelTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ReadableContentChannelTestCase.java
new file mode 100644
index 00000000000..378da5449c2
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ReadableContentChannelTestCase.java
@@ -0,0 +1,320 @@
+// 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 org.junit.Test;
+
+import java.nio.ByteBuffer;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.concurrent.*;
+
+import static org.junit.Assert.fail;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ReadableContentChannelTestCase {
+
+ @Test
+ public void requireThatWriteNullThrowsException() {
+ ReadableContentChannel content = new ReadableContentChannel();
+ try {
+ content.write(null, new MyCompletion());
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatWriteAfterCloseThrowsException() {
+ ReadableContentChannel content = new ReadableContentChannel();
+ content.close(null);
+ try {
+ content.write(ByteBuffer.allocate(69), new MyCompletion());
+ fail();
+ } catch (IllegalStateException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatWriteAfterFailedThrowsException() {
+ ReadableContentChannel content = new ReadableContentChannel();
+ content.failed(new RuntimeException());
+ try {
+ content.write(ByteBuffer.allocate(69), new MyCompletion());
+ fail();
+ } catch (IllegalStateException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatCloseAfterCloseThrowsException() {
+ ReadableContentChannel content = new ReadableContentChannel();
+ content.close(null);
+ try {
+ content.close(null);
+ fail();
+ } catch (IllegalStateException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatCloseAfterFailedThrowsException() {
+ ReadableContentChannel content = new ReadableContentChannel();
+ content.failed(new RuntimeException());
+ try {
+ content.close(null);
+ fail();
+ } catch (IllegalStateException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatFailedAfterFailedThrowsException() {
+ ReadableContentChannel content = new ReadableContentChannel();
+ content.failed(new RuntimeException());
+ try {
+ content.failed(new RuntimeException());
+ fail();
+ } catch (IllegalStateException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatIteratorDoesNotSupportRemove() {
+ try {
+ new ReadableContentChannel().iterator().remove();
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatWrittenBufferCanBeRead() {
+ ReadableContentChannel content = new ReadableContentChannel();
+ ByteBuffer buf = ByteBuffer.allocate(69);
+ content.write(buf, null);
+ assertSame(buf, content.read());
+ }
+
+ @Test
+ public void requireThatWrittenBuffersAreReadInOrder() {
+ ReadableContentChannel content = new ReadableContentChannel();
+ ByteBuffer foo = ByteBuffer.allocate(69);
+ content.write(foo, null);
+ ByteBuffer bar = ByteBuffer.allocate(69);
+ content.write(bar, null);
+ content.close(null);
+ assertSame(foo, content.read());
+ assertSame(bar, content.read());
+ }
+
+ @Test
+ public void requireThatReadAfterCloseIsNull() {
+ ReadableContentChannel content = new ReadableContentChannel();
+ content.close(null);
+ assertNull(content.read());
+ assertNull(content.read());
+ }
+
+ @Test
+ public void requireThatWrittenBufferCanBeReadByIterator() {
+ ReadableContentChannel content = new ReadableContentChannel();
+ ByteBuffer foo = ByteBuffer.allocate(69);
+ content.write(foo, null);
+ ByteBuffer bar = ByteBuffer.allocate(69);
+ content.write(bar, null);
+ content.close(null);
+
+ Iterator<ByteBuffer> it = content.iterator();
+ assertTrue(it.hasNext());
+ assertSame(foo, it.next());
+ assertTrue(it.hasNext());
+ assertSame(bar, it.next());
+ assertFalse(it.hasNext());
+ try {
+ it.next();
+ fail();
+ } catch (NoSuchElementException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatReadAfterFailedIsNull() {
+ ReadableContentChannel content = new ReadableContentChannel();
+ content.failed(new RuntimeException());
+ assertNull(content.read());
+ assertNull(content.read());
+ }
+
+ @Test
+ public void requireThatReadCallsCompletion() {
+ ReadableContentChannel content = new ReadableContentChannel();
+ ByteBuffer buf = ByteBuffer.allocate(69);
+ MyCompletion completion = new MyCompletion();
+ content.write(buf, completion);
+ assertFalse(completion.completed);
+ assertSame(buf, content.read());
+ assertTrue(completion.completed);
+
+ completion = new MyCompletion();
+ content.close(completion);
+ assertFalse(completion.completed);
+ assertNull(content.read());
+ assertTrue(completion.completed);
+ }
+
+ @Test
+ public void requireThatReadWaitsForWrite() throws Exception {
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ ReadableContentChannel content = new ReadableContentChannel();
+ Future<ByteBuffer> readBuf = executor.submit(new ReadTask(content));
+ try {
+ readBuf.get(100, TimeUnit.MILLISECONDS);
+ fail();
+ } catch (TimeoutException e) {
+
+ }
+ ByteBuffer buf = ByteBuffer.allocate(69);
+ content.write(buf, null);
+ assertSame(buf, readBuf.get(600, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void requireThatCloseNotifiesRead() throws Exception {
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ ReadableContentChannel content = new ReadableContentChannel();
+ Future<ByteBuffer> buf = executor.submit(new ReadTask(content));
+ try {
+ buf.get(100, TimeUnit.MILLISECONDS);
+ fail();
+ } catch (TimeoutException e) {
+
+ }
+ content.close(null);
+ assertNull(buf.get(600, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void requireThatFailedNotifiesRead() throws Exception {
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ ReadableContentChannel content = new ReadableContentChannel();
+ Future<ByteBuffer> buf = executor.submit(new ReadTask(content));
+ try {
+ buf.get(100, TimeUnit.MILLISECONDS);
+ fail();
+ } catch (TimeoutException e) {
+
+ }
+ content.failed(new RuntimeException());
+ assertNull(buf.get(600, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void requireThatFailedCallsPendingCompletions() {
+ MyCompletion foo = new MyCompletion();
+ MyCompletion bar = new MyCompletion();
+ ReadableContentChannel content = new ReadableContentChannel();
+ content.write(ByteBuffer.allocate(69), foo);
+ content.write(ByteBuffer.allocate(69), bar);
+ RuntimeException e = new RuntimeException();
+ content.failed(e);
+ assertSame(e, foo.failed);
+ assertSame(e, bar.failed);
+ }
+
+ @Test
+ public void requireThatAvailableIsNotBlocking() {
+ ReadableContentChannel content = new ReadableContentChannel();
+ assertEquals(0, content.available());
+ ByteBuffer buf = ByteBuffer.wrap(new byte[] { 6, 9 });
+ content.write(buf, null);
+ assertTrue(content.available() > 0);
+ assertSame(buf, content.read());
+ assertEquals(0, content.available());
+ content.close(null);
+ assertNull(content.read());
+ assertEquals(0, content.available());
+ }
+
+ @Test
+ public void requireThatContentIsThreadSafe() {
+ ExecutorService executor = Executors.newFixedThreadPool(100);
+ for (int run = 0; run < 69; ++run) {
+ List<ByteBuffer> bufs = new LinkedList<>();
+ for (int buf = 0; buf < 100; ++buf) {
+ bufs.add(ByteBuffer.allocate(buf));
+ }
+ ReadableContentChannel content = new ReadableContentChannel();
+ for (ByteBuffer buf : bufs) {
+ executor.execute(new WriteTask(content, buf));
+ }
+ for (int buf = 0; buf < 100; ++buf) {
+ assertTrue(bufs.remove(content.read()));
+ }
+ content.close(null);
+ assertNull(content.read());
+ }
+ }
+
+ private static class MyCompletion implements CompletionHandler {
+
+ boolean completed = false;
+ Throwable failed = null;
+
+ @Override
+ public void completed() {
+ completed = true;
+ }
+
+ @Override
+ public void failed(Throwable t) {
+ failed = t;
+ }
+ }
+
+ private static class ReadTask implements Callable<ByteBuffer> {
+
+ final ReadableContentChannel content;
+
+ ReadTask(ReadableContentChannel content) {
+ this.content = content;
+ }
+
+ @Override
+ public ByteBuffer call() throws Exception {
+ return content.read();
+ }
+ }
+
+ private static class WriteTask implements Runnable {
+
+ final ContentChannel content;
+ final ByteBuffer buf;
+
+ WriteTask(ContentChannel content, ByteBuffer buf) {
+ this.content = content;
+ this.buf = buf;
+ }
+
+ @Override
+ public void run() {
+ content.write(buf, null);
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/RequestDeniedTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/RequestDeniedTestCase.java
new file mode 100644
index 00000000000..3cfe794dfed
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/RequestDeniedTestCase.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.handler;
+
+import com.yahoo.jdisc.NoopSharedResource;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+
+import static org.junit.Assert.fail;
+import static org.junit.Assert.assertSame;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class RequestDeniedTestCase {
+
+ @Test
+ public void requireThatAccessorsWork() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ driver.activateContainer(driver.newContainerBuilder());
+ Request request = new Request(driver, URI.create("http://host/path"));
+ RequestDeniedException e = new RequestDeniedException(request);
+ assertSame(request, e.request());
+ request.release();
+ driver.close();
+ }
+
+ @Test
+ public void requireThatRequestDeniedIsThrown() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ RequestHandler requestHandler = new MyRequestHandler();
+ builder.serverBindings().bind("http://host/path", requestHandler);
+ driver.activateContainer(builder);
+ Request request = new Request(driver, URI.create("http://host/path"));
+ try {
+ request.connect(new MyResponseHandler());
+ fail();
+ } catch (RequestDeniedException e) {
+ assertSame(request, e.request());
+ }
+ request.release();
+ driver.close();
+ }
+
+ private static class MyRequestHandler extends NoopSharedResource implements RequestHandler {
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ throw new RequestDeniedException(request);
+ }
+
+ @Override
+ public void handleTimeout(Request request, ResponseHandler handler) {
+
+ }
+ }
+
+ private static class MyResponseHandler implements ResponseHandler {
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ return null;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/RequestDispatchTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/RequestDispatchTestCase.java
new file mode 100644
index 00000000000..f13473a1660
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/RequestDispatchTestCase.java
@@ -0,0 +1,253 @@
+// 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.MoreExecutors;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.fail;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class RequestDispatchTestCase {
+
+ @Test
+ public void requireThatRequestCanBeDispatched() throws Exception {
+ final TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ final List<ByteBuffer> writtenContent = Arrays.asList(ByteBuffer.allocate(6), ByteBuffer.allocate(9));
+ ReadableContentChannel receivedContent = new ReadableContentChannel();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ Response response = new Response(Response.Status.OK);
+ builder.serverBindings().bind("http://localhost/", new MyRequestHandler(receivedContent, response));
+ driver.activateContainer(builder);
+ RequestDispatch dispatch = new RequestDispatch() {
+
+ @Override
+ protected Request newRequest() {
+ return new Request(driver, URI.create("http://localhost/"));
+ }
+
+ @Override
+ protected Iterable<ByteBuffer> requestContent() {
+ return writtenContent;
+ }
+ };
+ dispatch.dispatch();
+ assertFalse(dispatch.isDone());
+ assertSame(writtenContent.get(0), receivedContent.read());
+ assertFalse(dispatch.isDone());
+ assertSame(writtenContent.get(1), receivedContent.read());
+ assertFalse(dispatch.isDone());
+ assertNull(receivedContent.read());
+ assertTrue(dispatch.isDone());
+ assertSame(response, dispatch.get(600, TimeUnit.SECONDS));
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatStreamCanBeConnected() throws IOException {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ ReadableContentChannel content = new ReadableContentChannel();
+ MyRequestHandler requestHandler = new MyRequestHandler(content, new Response(Response.Status.OK));
+ builder.serverBindings().bind("http://localhost/", requestHandler);
+ driver.activateContainer(builder);
+
+ OutputStream out = new FastContentOutputStream(driver.newRequestDispatch("http://localhost/", new FutureResponse()).connect());
+ out.write(6);
+ out.write(9);
+ out.close();
+
+ InputStream in = content.toStream();
+ assertEquals(6, in.read());
+ assertEquals(9, in.read());
+ assertEquals(-1, in.read());
+
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatCancelIsUnsupported() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ RequestDispatch dispatch = driver.newRequestDispatch("http://localhost/", new FutureResponse());
+ assertFalse(dispatch.isCancelled());
+ try {
+ dispatch.cancel(true);
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ assertFalse(dispatch.isCancelled());
+ try {
+ dispatch.cancel(false);
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ assertFalse(dispatch.isCancelled());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatDispatchHandlesConnectException() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind("http://localhost/", new AbstractRequestHandler() {
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ throw new RuntimeException();
+ }
+ });
+ driver.activateContainer(builder);
+ try {
+ driver.newRequestDispatch("http://localhost/", new FutureResponse()).dispatch();
+ fail();
+ } catch (RuntimeException e) {
+
+ }
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatDispatchHandlesWriteException() {
+ final TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ Response response = new Response(Response.Status.OK);
+ builder.serverBindings().bind("http://localhost/", new MyRequestHandler(new ContentChannel() {
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ throw new RuntimeException();
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ handler.completed();
+ }
+ }, response));
+ driver.activateContainer(builder);
+ try {
+ new RequestDispatch() {
+
+ @Override
+ protected Request newRequest() {
+ return new Request(driver, URI.create("http://localhost/"));
+ }
+
+ @Override
+ protected Iterable<ByteBuffer> requestContent() {
+ return Arrays.asList(ByteBuffer.allocate(69));
+ }
+ }.dispatch();
+ fail();
+ } catch (RuntimeException e) {
+
+ }
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatDispatchHandlesCloseException() {
+ final TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ Response response = new Response(Response.Status.OK);
+ builder.serverBindings().bind("http://localhost/", new MyRequestHandler(new ContentChannel() {
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ throw new RuntimeException();
+ }
+ }, response));
+ driver.activateContainer(builder);
+ try {
+ new RequestDispatch() {
+
+ @Override
+ protected Request newRequest() {
+ return new Request(driver, URI.create("http://localhost/"));
+ }
+
+ @Override
+ protected Iterable<ByteBuffer> requestContent() {
+ return Arrays.asList(ByteBuffer.allocate(69));
+ }
+ }.dispatch();
+ fail();
+ } catch (RuntimeException e) {
+
+ }
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatDispatchCanBeListenedTo() throws InterruptedException {
+ final TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ ReadableContentChannel requestContent = new ReadableContentChannel();
+ MyRequestHandler requestHandler = new MyRequestHandler(requestContent, null);
+ builder.serverBindings().bind("http://localhost/", requestHandler);
+ driver.activateContainer(builder);
+ RunnableLatch listener = new RunnableLatch();
+ new RequestDispatch() {
+
+ @Override
+ protected Request newRequest() {
+ return new Request(driver, URI.create("http://localhost/"));
+ }
+ }.dispatch().addListener(listener, MoreExecutors.sameThreadExecutor());
+ assertFalse(listener.await(100, TimeUnit.MILLISECONDS));
+ ContentChannel responseContent = ResponseDispatch.newInstance(Response.Status.OK)
+ .connect(requestHandler.responseHandler);
+ assertFalse(listener.await(100, TimeUnit.MILLISECONDS));
+ assertNull(requestContent.read());
+ assertTrue(listener.await(600, TimeUnit.SECONDS));
+ responseContent.close(null);
+ assertTrue(driver.close());
+ }
+
+ private static class MyRequestHandler extends AbstractRequestHandler {
+
+ final ContentChannel content;
+ final Response response;
+ ResponseHandler responseHandler;
+
+ MyRequestHandler(ContentChannel content, Response response) {
+ this.content = content;
+ this.response = response;
+ }
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ if (response != null) {
+ ResponseDispatch.newInstance(response).dispatch(handler);
+ } else {
+ responseHandler = handler;
+ }
+ return content;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ResponseDispatchTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ResponseDispatchTestCase.java
new file mode 100644
index 00000000000..92fc0f90c07
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ResponseDispatchTestCase.java
@@ -0,0 +1,206 @@
+// 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.MoreExecutors;
+import com.yahoo.jdisc.Response;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.Assert.fail;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class ResponseDispatchTestCase {
+
+ @Test
+ public void requireThatFactoryMethodsWork() throws Exception {
+ {
+ FutureResponse handler = new FutureResponse();
+ ResponseDispatch.newInstance(69).dispatch(handler);
+ Response response = handler.get(600, TimeUnit.SECONDS);
+ assertNotNull(response);
+ assertEquals(69, response.getStatus());
+ }
+ {
+ FutureResponse handler = new FutureResponse();
+ Response sentResponse = new Response(69);
+ ResponseDispatch.newInstance(sentResponse).dispatch(handler);
+ Response receivedResponse = handler.get(600, TimeUnit.SECONDS);
+ assertSame(sentResponse, receivedResponse);
+ }
+ {
+ ReadableContentChannel content = new ReadableContentChannel();
+ FutureResponse handler = new FutureResponse(content);
+ ByteBuffer buf = ByteBuffer.allocate(69);
+ ResponseDispatch.newInstance(69, Arrays.asList(buf)).dispatch(handler);
+ Response response = handler.get(600, TimeUnit.SECONDS);
+ assertNotNull(response);
+ assertEquals(69, response.getStatus());
+ assertSame(buf, content.read());
+ assertNull(content.read());
+ }
+ {
+ ReadableContentChannel content = new ReadableContentChannel();
+ FutureResponse handler = new FutureResponse(content);
+ ByteBuffer buf = ByteBuffer.allocate(69);
+ ResponseDispatch.newInstance(69, Arrays.asList(buf)).dispatch(handler);
+ Response response = handler.get(600, TimeUnit.SECONDS);
+ assertNotNull(response);
+ assertEquals(69, response.getStatus());
+ assertSame(buf, content.read());
+ assertNull(content.read());
+ }
+ {
+ ReadableContentChannel content = new ReadableContentChannel();
+ FutureResponse handler = new FutureResponse(content);
+ ByteBuffer buf = ByteBuffer.allocate(69);
+ Response sentResponse = new Response(69);
+ ResponseDispatch.newInstance(sentResponse, Arrays.asList(buf)).dispatch(handler);
+ Response receivedResponse = handler.get(600, TimeUnit.SECONDS);
+ assertSame(sentResponse, receivedResponse);
+ assertSame(buf, content.read());
+ assertNull(content.read());
+ }
+ }
+
+ @Test
+ public void requireThatResponseCanBeDispatched() throws Exception {
+ final Response response = new Response(Response.Status.OK);
+ final List<ByteBuffer> writtenContent = Arrays.asList(ByteBuffer.allocate(6), ByteBuffer.allocate(9));
+ ResponseDispatch dispatch = new ResponseDispatch() {
+
+ @Override
+ protected Response newResponse() {
+ return response;
+ }
+
+ @Override
+ protected Iterable<ByteBuffer> responseContent() {
+ return writtenContent;
+ }
+ };
+ ReadableContentChannel receivedContent = new ReadableContentChannel();
+ MyResponseHandler responseHandler = new MyResponseHandler(receivedContent);
+ dispatch.dispatch(responseHandler);
+ assertFalse(dispatch.isDone());
+ assertSame(response, responseHandler.response);
+ assertSame(writtenContent.get(0), receivedContent.read());
+ assertFalse(dispatch.isDone());
+ assertSame(writtenContent.get(1), receivedContent.read());
+ assertFalse(dispatch.isDone());
+ assertNull(receivedContent.read());
+ assertTrue(dispatch.isDone());
+ assertTrue(dispatch.get(600, TimeUnit.SECONDS));
+ assertTrue(dispatch.get());
+ }
+
+ @Test
+ public void requireThatStreamCanBeConnected() throws IOException {
+ ReadableContentChannel responseContent = new ReadableContentChannel();
+ OutputStream out = new FastContentOutputStream(new ResponseDispatch() {
+
+ @Override
+ protected Response newResponse() {
+ return new Response(Response.Status.OK);
+ }
+ }.connect(new MyResponseHandler(responseContent)));
+ out.write(6);
+ out.write(9);
+ out.close();
+
+ InputStream in = responseContent.toStream();
+ assertEquals(6, in.read());
+ assertEquals(9, in.read());
+ assertEquals(-1, in.read());
+ }
+
+ @Test
+ public void requireThatCancelIsUnsupported() {
+ ResponseDispatch dispatch = ResponseDispatch.newInstance(69);
+ assertFalse(dispatch.isCancelled());
+ try {
+ dispatch.cancel(true);
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ assertFalse(dispatch.isCancelled());
+ try {
+ dispatch.cancel(false);
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ assertFalse(dispatch.isCancelled());
+ }
+
+ @Test
+ public void requireThatDispatchClosesContentIfWriteThrowsException() {
+ final AtomicBoolean closed = new AtomicBoolean(false);
+ try {
+ ResponseDispatch.newInstance(6, ByteBuffer.allocate(9)).dispatch(
+ new MyResponseHandler(new ContentChannel() {
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ closed.set(true);
+ }
+ }));
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ assertTrue(closed.get());
+ }
+
+ @Test
+ public void requireThatDispatchCanBeListenedTo() throws InterruptedException {
+ RunnableLatch listener = new RunnableLatch();
+ ReadableContentChannel responseContent = new ReadableContentChannel();
+ ResponseDispatch.newInstance(6, ByteBuffer.allocate(9))
+ .dispatch(new MyResponseHandler(responseContent))
+ .addListener(listener, MoreExecutors.sameThreadExecutor());
+ assertFalse(listener.await(100, TimeUnit.MILLISECONDS));
+ assertNotNull(responseContent.read());
+ assertFalse(listener.await(100, TimeUnit.MILLISECONDS));
+ assertNull(responseContent.read());
+ assertTrue(listener.await(600, TimeUnit.SECONDS));
+ }
+
+ private static class MyResponseHandler implements ResponseHandler {
+
+ final ContentChannel content;
+ Response response;
+
+ MyResponseHandler(ContentChannel content) {
+ this.content = content;
+ }
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ this.response = response;
+ return content;
+ }
+ }
+
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/RunnableLatch.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/RunnableLatch.java
new file mode 100644
index 00000000000..e81d7eb16ef
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/RunnableLatch.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 java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+class RunnableLatch implements Runnable {
+
+ private final CountDownLatch latch = new CountDownLatch(1);
+
+ @Override
+ public void run() {
+ latch.countDown();
+ }
+
+ public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
+ return latch.await(timeout, unit);
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ThreadedRequestHandlerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ThreadedRequestHandlerTestCase.java
new file mode 100644
index 00000000000..545be4e03ce
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/ThreadedRequestHandlerTestCase.java
@@ -0,0 +1,228 @@
+// 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.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.service.CurrentContainer;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.fail;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ThreadedRequestHandlerTestCase {
+
+ @Test
+ public void requireThatNullExecutorThrowsException() {
+ try {
+ new ThreadedRequestHandler(null) {
+
+ @Override
+ public void handleRequest(Request request, BufferedContentChannel content, ResponseHandler handler) {
+
+ }
+ };
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatAccessorWork() {
+ MyRequestHandler requestHandler = new MyRequestHandler(newExecutor());
+ requestHandler.setTimeout(1000, TimeUnit.MILLISECONDS);
+ assertEquals(1000, requestHandler.getTimeout(TimeUnit.MILLISECONDS));
+ assertEquals(1, requestHandler.getTimeout(TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void requireThatHandlerSetsRequestTimeout() throws InterruptedException {
+ MyRequestHandler requestHandler = new MyRequestHandler(newExecutor());
+ requestHandler.setTimeout(600, TimeUnit.SECONDS);
+ TestDriver driver = newTestDriver("http://localhost/", requestHandler);
+
+ MyResponseHandler responseHandler = new MyResponseHandler();
+ driver.dispatchRequest("http://localhost/", responseHandler);
+
+ requestHandler.entryLatch.countDown();
+ assertTrue(requestHandler.exitLatch.await(600, TimeUnit.SECONDS));
+ assertNull(requestHandler.content.read());
+ assertNotNull(requestHandler.request.getTimeout(TimeUnit.MILLISECONDS));
+
+ assertTrue(responseHandler.latch.await(600, TimeUnit.SECONDS));
+ assertNull(responseHandler.content.read());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatRequestAndResponseReachHandlers() throws InterruptedException {
+ MyRequestHandler requestHandler = new MyRequestHandler(newExecutor());
+ TestDriver driver = newTestDriver("http://localhost/", requestHandler);
+
+ MyResponseHandler responseHandler = new MyResponseHandler();
+ Request request = new Request(driver, URI.create("http://localhost/"));
+ ContentChannel requestContent = request.connect(responseHandler);
+ ByteBuffer buf = ByteBuffer.allocate(69);
+ requestContent.write(buf, null);
+ requestContent.close(null);
+ request.release();
+
+ requestHandler.entryLatch.countDown();
+ assertTrue(requestHandler.exitLatch.await(600, TimeUnit.SECONDS));
+ assertSame(request, requestHandler.request);
+ assertSame(buf, requestHandler.content.read());
+ assertNull(requestHandler.content.read());
+
+ assertTrue(responseHandler.latch.await(600, TimeUnit.SECONDS));
+ assertSame(requestHandler.response, responseHandler.response);
+ assertNull(responseHandler.content.read());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatNotImplementedHandlerDoesNotPreventShutdown() throws Exception {
+ TestDriver driver = newTestDriver("http://localhost/", new ThreadedRequestHandler(newExecutor()) {
+
+ });
+ assertEquals(Response.Status.NOT_IMPLEMENTED,
+ dispatchRequest(driver, "http://localhost/", ByteBuffer.wrap(new byte[] { 69 }))
+ .get(600, TimeUnit.SECONDS).getStatus());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatThreadedRequestHandlerRetainsTheRequestUntilHandlerIsRun() throws Exception {
+ final TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ final AtomicInteger baseRetainCount = new AtomicInteger();
+ builder.serverBindings().bind("http://localhost/base", new AbstractRequestHandler() {
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ baseRetainCount.set(request.retainCount());
+ handler.handleResponse(new Response(Response.Status.OK)).close(null);
+ return null;
+ }
+ });
+ final CountDownLatch entryLatch = new CountDownLatch(1);
+ final CountDownLatch exitLatch = new CountDownLatch(1);
+ final AtomicInteger testRetainCount = new AtomicInteger();
+ builder.serverBindings().bind("http://localhost/test", new ThreadedRequestHandler(newExecutor()) {
+
+ @Override
+ public void handleRequest(Request request, ReadableContentChannel requestContent,
+ ResponseHandler responseHandler) {
+ try {
+ entryLatch.await(600, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ return;
+ }
+ testRetainCount.set(request.retainCount());
+ responseHandler.handleResponse(new Response(Response.Status.OK)).close(null);
+ requestContent.read(); // drain content to call completion handlers
+ exitLatch.countDown();
+ }
+ });
+ driver.activateContainer(builder);
+ dispatchRequest(driver, "http://localhost/base");
+ dispatchRequest(driver, "http://localhost/test");
+ entryLatch.countDown();
+ exitLatch.await(600, TimeUnit.SECONDS);
+ assertEquals(baseRetainCount.get(), testRetainCount.get());
+ assertTrue(driver.close());
+ }
+
+ private static TestDriver newTestDriver(String uri, RequestHandler requestHandler) {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind(uri, requestHandler);
+ driver.activateContainer(builder);
+ return driver;
+ }
+
+ private static ListenableFuture<Response> dispatchRequest(final CurrentContainer container, final String uri,
+ final ByteBuffer... content) {
+ return new RequestDispatch() {
+
+ @Override
+ protected Request newRequest() {
+ return new Request(container, URI.create(uri));
+ }
+
+ @Override
+ protected Iterable<ByteBuffer> requestContent() {
+ return Arrays.asList(content);
+ }
+ }.dispatch();
+ }
+
+ private static Executor newExecutor() {
+ return Executors.newSingleThreadExecutor();
+ }
+
+ private static class MyRequestHandler extends ThreadedRequestHandler {
+
+ final CountDownLatch entryLatch = new CountDownLatch(1);
+ final CountDownLatch exitLatch = new CountDownLatch(1);
+ final ReadableContentChannel content = new ReadableContentChannel();
+ Response response = null;
+ Request request = null;
+
+ MyRequestHandler(Executor executor) {
+ super(executor);
+ }
+
+ @Override
+ public void handleRequest(Request request, BufferedContentChannel content, ResponseHandler handler) {
+ try {
+ if (!entryLatch.await(600, TimeUnit.SECONDS)) {
+ return;
+ }
+ this.request = request;
+ content.connectTo(this.content);
+ response = new Response(Response.Status.OK);
+ handler.handleResponse(response).close(null);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } finally {
+ exitLatch.countDown();
+ }
+ }
+ }
+
+ private static class MyResponseHandler implements ResponseHandler {
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ReadableContentChannel content = new ReadableContentChannel();
+ Response response = null;
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ this.response = response;
+ latch.countDown();
+
+ BufferedContentChannel content = new BufferedContentChannel();
+ content.connectTo(this.content);
+ return content;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/handler/UnsafeContentInputStreamTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/UnsafeContentInputStreamTestCase.java
new file mode 100644
index 00000000000..9aac2c4ea7f
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/handler/UnsafeContentInputStreamTestCase.java
@@ -0,0 +1,139 @@
+// 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 org.junit.Test;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.ByteBuffer;
+import java.util.concurrent.Future;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class UnsafeContentInputStreamTestCase {
+
+ @Test
+ public void requireThatBytesCanBeRead() throws IOException {
+ BufferedContentChannel channel = new BufferedContentChannel();
+ FastContentWriter writer = new FastContentWriter(channel);
+ writer.write("Hello ");
+ writer.write("World!");
+ writer.close();
+
+ BufferedReader reader = asBufferedReader(channel);
+ assertEquals("Hello World!", reader.readLine());
+ assertNull(reader.readLine());
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void requireThatCompletionsAreCalledWithDeprecatedContentWriter() throws IOException {
+ BufferedContentChannel channel = new BufferedContentChannel();
+ FastContentWriter writer = new FastContentWriter(channel);
+ writer.write("foo");
+ writer.close();
+
+ InputStream stream = asInputStream(channel);
+ assertEquals('f', stream.read());
+ assertEquals('o', stream.read());
+ assertEquals('o', stream.read());
+ assertEquals(-1, stream.read());
+ assertTrue(writer.isDone());
+ }
+
+ @Test
+ public void requireThatCompletionsAreCalled() throws IOException {
+ BufferedContentChannel channel = new BufferedContentChannel();
+ FastContentWriter writer = new FastContentWriter(channel);
+ writer.write("foo");
+ writer.close();
+
+ InputStream stream = asInputStream(channel);
+ assertEquals('f', stream.read());
+ assertEquals('o', stream.read());
+ assertEquals('o', stream.read());
+ assertEquals(-1, stream.read());
+ assertTrue(writer.isDone());
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void requireThatCloseDrainsStreamWithDeprecatedContentWriter() {
+ BufferedContentChannel channel = new BufferedContentChannel();
+ FastContentWriter writer = new FastContentWriter(channel);
+ writer.write("foo");
+ writer.close();
+
+ asInputStream(channel).close();
+ assertTrue(writer.isDone());
+ }
+
+ @Test
+ public void requireThatCloseDrainsStream() {
+ BufferedContentChannel channel = new BufferedContentChannel();
+ FastContentWriter writer = new FastContentWriter(channel);
+ writer.write("foo");
+ writer.close();
+
+ asInputStream(channel).close();
+ assertTrue(writer.isDone());
+ }
+
+ @Test
+ public void requireThatAvailableIsNotBlocking() throws IOException {
+ BufferedContentChannel channel = new BufferedContentChannel();
+ InputStream stream = asInputStream(channel);
+ assertEquals(0, stream.available());
+ channel.write(ByteBuffer.wrap(new byte[] { 6, 9 }), null);
+ assertTrue(stream.available() > 0);
+ assertEquals(6, stream.read());
+ assertTrue(stream.available() > 0);
+ assertEquals(9, stream.read());
+ assertEquals(0, stream.available());
+ channel.close(null);
+ assertEquals(-1, stream.read());
+ assertEquals(0, stream.available());
+ }
+
+ @Test
+ public void requireThatReadLargeArrayIsNotBlocking() throws IOException {
+ BufferedContentChannel channel = new BufferedContentChannel();
+ InputStream stream = asInputStream(channel);
+ assertEquals(0, stream.available());
+ channel.write(ByteBuffer.wrap(new byte[] { 6, 9 }), null);
+ assertTrue(stream.available() > 0);
+ byte[] buf = new byte[69];
+ assertEquals(2, stream.read(buf));
+ assertEquals(6, buf[0]);
+ assertEquals(9, buf[1]);
+ assertEquals(0, stream.available());
+ channel.close(null);
+ assertEquals(-1, stream.read(buf));
+ assertEquals(0, stream.available());
+ }
+
+ @Test
+ public void requireThatAllByteValuesCanBeRead() throws IOException {
+ ReadableContentChannel content = new ReadableContentChannel();
+ InputStream in = new UnsafeContentInputStream(content);
+ for (int i = Byte.MIN_VALUE; i <= Byte.MAX_VALUE; ++i) {
+ content.write(ByteBuffer.wrap(new byte[] { (byte)i }), null);
+ assertEquals(i, (byte)in.read());
+ }
+ }
+
+ private static BufferedReader asBufferedReader(BufferedContentChannel channel) {
+ return new BufferedReader(new InputStreamReader(asInputStream(channel)));
+ }
+
+ private static UnsafeContentInputStream asInputStream(BufferedContentChannel channel) {
+ return new UnsafeContentInputStream(channel.toReadable());
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/service/AbstractClientProviderTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/service/AbstractClientProviderTestCase.java
new file mode 100644
index 00000000000..93e96c29e6f
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/service/AbstractClientProviderTestCase.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.service;
+
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class AbstractClientProviderTestCase {
+
+ @Test
+ public void requireThatAbstractClassIsAClientProvider() {
+ assertTrue(ClientProvider.class.isInstance(new MyClientProvider()));
+ }
+
+ @Test
+ public void requireThatStartDoesNotThrowAnException() {
+ new MyClientProvider().start();
+ }
+
+ private static class MyClientProvider extends AbstractClientProvider {
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ return null;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/service/AbstractServerProviderTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/service/AbstractServerProviderTestCase.java
new file mode 100644
index 00000000000..c6230e928b7
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/service/AbstractServerProviderTestCase.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.service;
+
+import com.google.inject.Inject;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class AbstractServerProviderTestCase {
+
+ @Test
+ public void requireThatAbstractClassIsAServerProvider() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ assertTrue(ServerProvider.class.isInstance(new MyServerProvider(driver)));
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatAccessorsWork() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ MyServerProvider server = builder.getInstance(MyServerProvider.class);
+ assertNotNull(server.container());
+ assertTrue(driver.close());
+ }
+
+ private static class MyServerProvider extends AbstractServerProvider {
+
+ @Inject
+ public MyServerProvider(CurrentContainer container) {
+ super(container);
+ }
+
+ @Override
+ public void start() {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/service/BindingSetNotFoundTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/service/BindingSetNotFoundTestCase.java
new file mode 100644
index 00000000000..6d3ed636972
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/service/BindingSetNotFoundTestCase.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.service;
+
+import com.google.inject.AbstractModule;
+import com.yahoo.jdisc.application.BindingSetSelector;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class BindingSetNotFoundTestCase {
+
+ @Test
+ public void requireThatAccessorsWork() {
+ BindingSetNotFoundException e = new BindingSetNotFoundException("foo");
+ assertEquals("foo", e.bindingSet());
+ }
+
+ @Test
+ public void requireThatBindingSetNotFoundIsThrown() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(BindingSetSelector.class).toInstance(new MySelector("foo"));
+ }
+ });
+ driver.activateContainer(driver.newContainerBuilder());
+ try {
+ driver.newReference(URI.create("http://host"));
+ fail();
+ } catch (BindingSetNotFoundException e) {
+ assertEquals("foo", e.bindingSet());
+ }
+ driver.close();
+ }
+
+ private static class MySelector implements BindingSetSelector {
+
+ final String setName;
+
+ MySelector(String setName) {
+ this.setName = setName;
+ }
+
+ @Override
+ public String select(URI uri) {
+ return setName;
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/service/ConnectToHandlerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/service/ConnectToHandlerTestCase.java
new file mode 100644
index 00000000000..a516e42c11a
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/service/ConnectToHandlerTestCase.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.service;
+
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+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.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ConnectToHandlerTestCase {
+
+ @Test
+ public void requireThatNullResponseHandlerThrowsException() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ driver.activateContainer(driver.newContainerBuilder());
+ Request request = new Request(driver, URI.create("http://host/path"));
+ try {
+ request.connect(null);
+ fail();
+ } catch (NullPointerException e) {
+ // expected
+ }
+ request.release();
+ driver.close();
+ }
+
+ @Test
+ public void requireThatConnectToHandlerWorks() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ MyRequestHandler requestHandler = new MyRequestHandler(new MyContent());
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind("http://host/*", requestHandler);
+ driver.activateContainer(builder);
+ Request request = new Request(driver, URI.create("http://host/path"));
+ MyResponseHandler responseHandler = new MyResponseHandler();
+ ContentChannel content = request.connect(responseHandler);
+ request.release();
+ assertNotNull(content);
+ content.close(null);
+ assertNotNull(requestHandler.handler);
+ assertSame(request, requestHandler.request);
+ requestHandler.handler.handleResponse(new Response(Response.Status.OK)).close(null);
+ driver.close();
+ }
+
+ private class MyRequestHandler extends AbstractRequestHandler {
+
+ final ContentChannel content;
+ Request request;
+ ResponseHandler handler;
+
+ MyRequestHandler(ContentChannel content) {
+ this.content = content;
+ }
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ this.request = request;
+ this.handler = handler;
+ return content;
+ }
+ }
+
+ private class MyResponseHandler implements ResponseHandler {
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ return null;
+ }
+ }
+
+ private static class MyContent implements ContentChannel {
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ handler.completed();
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/service/ContainerNotReadyTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/service/ContainerNotReadyTestCase.java
new file mode 100644
index 00000000000..423e6e4cc91
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/service/ContainerNotReadyTestCase.java
@@ -0,0 +1,28 @@
+// 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.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ContainerNotReadyTestCase {
+
+ @Test
+ public void requireThatExceptionIsThrown() throws BindingSetNotFoundException {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ try {
+ driver.newReference(URI.create("http://host"));
+ fail();
+ } catch (ContainerNotReadyException e) {
+
+ }
+ driver.close();
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/service/CurrentContainerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/service/CurrentContainerTestCase.java
new file mode 100644
index 00000000000..8a5a6aeb913
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/service/CurrentContainerTestCase.java
@@ -0,0 +1,27 @@
+// 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.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+
+import static org.junit.Assert.assertNotNull;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class CurrentContainerTestCase {
+
+ @Test
+ public void requireThatNewRequestsCreateSnapshot() throws Exception {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ driver.activateContainer(driver.newContainerBuilder());
+ Request request = new Request(driver, URI.create("http://host/path"));
+ assertNotNull(request.container());
+ request.release();
+ driver.close();
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/service/NoBindingSetSelectedTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/service/NoBindingSetSelectedTestCase.java
new file mode 100644
index 00000000000..56ea80c2001
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/service/NoBindingSetSelectedTestCase.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.service;
+
+import com.google.inject.AbstractModule;
+import com.yahoo.jdisc.application.BindingSetSelector;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class NoBindingSetSelectedTestCase {
+
+ @Test
+ public void requireThatAccessorsWork() {
+ URI uri = URI.create("http://host/path");
+ NoBindingSetSelectedException e = new NoBindingSetSelectedException(uri);
+ assertEquals(uri, e.uri());
+ }
+
+ @Test
+ public void requireThatNoBindingSetSelectedIsThrown() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(BindingSetSelector.class).toInstance(new MySelector());
+ }
+ });
+ driver.activateContainer(driver.newContainerBuilder());
+ URI uri = URI.create("http://host");
+ try {
+ driver.newReference(uri);
+ fail();
+ } catch (NoBindingSetSelectedException e) {
+ assertEquals(uri, e.uri());
+ }
+ driver.close();
+ }
+
+ private static class MySelector implements BindingSetSelector {
+
+ @Override
+ public String select(URI uri) {
+ return null;
+ }
+ }
+
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingClientTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingClientTestCase.java
new file mode 100644
index 00000000000..ff70ee35049
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingClientTestCase.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.test;
+
+import com.yahoo.jdisc.service.ClientProvider;
+import org.junit.Test;
+
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class NonWorkingClientTestCase {
+
+ @Test
+ public void requireThatHandleRequestThrowsException() {
+ ClientProvider client = new NonWorkingClientProvider();
+ try {
+ client.handleRequest(null, null);
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatHandleTimeoutThrowsException() {
+ ClientProvider client = new NonWorkingClientProvider();
+ try {
+ client.handleTimeout(null, null);
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatStartDoesNotThrow() {
+ ClientProvider client = new NonWorkingClientProvider();
+ client.start();
+ }
+
+ @Test
+ public void requireThatRetainDoesNotThrow() {
+ ClientProvider client = new NonWorkingClientProvider();
+ client.release();
+ }
+
+ @Test
+ public void requireThatReleaseDoesNotThrow() {
+ ClientProvider client = new NonWorkingClientProvider();
+ client.release();
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingCompletionHandlerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingCompletionHandlerTestCase.java
new file mode 100644
index 00000000000..9ee5f5dd265
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingCompletionHandlerTestCase.java
@@ -0,0 +1,43 @@
+// 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 org.junit.Test;
+
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class NonWorkingCompletionHandlerTestCase {
+
+ @Test
+ public void requireThatCompletedThrowsException() {
+ CompletionHandler completion = new NonWorkingCompletionHandler();
+ try {
+ completion.completed();
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatFailedThrowsException() {
+ CompletionHandler completion = new NonWorkingCompletionHandler();
+ try {
+ completion.failed(null);
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ completion = new NonWorkingCompletionHandler();
+ try {
+ completion.failed(new Throwable());
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingContentChannelTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingContentChannelTestCase.java
new file mode 100644
index 00000000000..73adca81bf2
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingContentChannelTestCase.java
@@ -0,0 +1,80 @@
+// 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 org.junit.Test;
+
+import java.nio.ByteBuffer;
+
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class NonWorkingContentChannelTestCase {
+
+ @Test
+ public void requireThatWriteThrowsException() {
+ ContentChannel content = new NonWorkingContentChannel();
+ try {
+ content.write(null, null);
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ content = new NonWorkingContentChannel();
+ try {
+ content.write(ByteBuffer.allocate(69), null);
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ content = new NonWorkingContentChannel();
+ try {
+ content.write(ByteBuffer.allocate(69), new MyCompletion());
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ content = new NonWorkingContentChannel();
+ try {
+ content.write(null, new MyCompletion());
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatCloseThrowsException() {
+ ContentChannel content = new NonWorkingContentChannel();
+ try {
+ content.close(null);
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ content = new NonWorkingContentChannel();
+ try {
+ content.close(new MyCompletion());
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ }
+
+ private static class MyCompletion implements CompletionHandler {
+
+ @Override
+ public void completed() {
+
+ }
+
+ @Override
+ public void failed(Throwable t) {
+
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingOsgiFrameworkTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingOsgiFrameworkTestCase.java
new file mode 100644
index 00000000000..ad1e34aba66
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingOsgiFrameworkTestCase.java
@@ -0,0 +1,72 @@
+// 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.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleException;
+
+import java.util.Collections;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class NonWorkingOsgiFrameworkTestCase {
+
+ @Test
+ public void requireThatFrameworkCanStartAndStop() throws BundleException {
+ OsgiFramework osgi = new NonWorkingOsgiFramework();
+ osgi.start();
+ osgi.stop();
+ }
+
+ @Test
+ public void requireThatFrameworkHasNoBundles() throws BundleException {
+ OsgiFramework osgi = new NonWorkingOsgiFramework();
+ assertTrue(osgi.bundles().isEmpty());
+ }
+
+ @Test
+ public void requireThatFrameworkHasNoBundleContext() {
+ OsgiFramework osgi = new NonWorkingOsgiFramework();
+ assertNull(osgi.bundleContext());
+ }
+
+ @Test
+ public void requireThatFrameworkThrowsOnInstallBundle() throws BundleException {
+ OsgiFramework osgi = new NonWorkingOsgiFramework();
+ try {
+ osgi.installBundle("file:bundle.jar");
+ fail();
+ } catch (UnsupportedOperationException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void requireThatFrameworkThrowsOnStartBundles() throws BundleException {
+ OsgiFramework osgi = new NonWorkingOsgiFramework();
+ try {
+ osgi.startBundles(Collections.<Bundle>emptyList(), false);
+ fail();
+ } catch (UnsupportedOperationException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void requireThatFrameworkThrowsOnRefreshPackages() throws BundleException, InterruptedException {
+ OsgiFramework osgi = new NonWorkingOsgiFramework();
+ try {
+ osgi.refreshPackages();
+ fail();
+ } catch (UnsupportedOperationException e) {
+ // expected
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingRequestHandlerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingRequestHandlerTestCase.java
new file mode 100644
index 00000000000..7893a2d1cf7
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingRequestHandlerTestCase.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.test;
+
+import com.yahoo.jdisc.handler.RequestHandler;
+import org.junit.Test;
+
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class NonWorkingRequestHandlerTestCase {
+
+ @Test
+ public void requireThatHandleRequestThrowsException() {
+ RequestHandler requestHandler = new NonWorkingRequestHandler();
+ try {
+ requestHandler.handleRequest(null, null);
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatHandleTimeoutThrowsException() {
+ RequestHandler requestHandler = new NonWorkingRequestHandler();
+ try {
+ requestHandler.handleTimeout(null, null);
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatDestroyDoesNotThrow() {
+ RequestHandler requestHandler = new NonWorkingRequestHandler();
+ requestHandler.release();
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingRequestTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingRequestTestCase.java
new file mode 100644
index 00000000000..0d1b33fa72f
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingRequestTestCase.java
@@ -0,0 +1,35 @@
+// 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.Key;
+import com.google.inject.name.Names;
+import com.yahoo.jdisc.Request;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class NonWorkingRequestTestCase {
+
+ @Test
+ public void requireThatFactoryMethodWorks() {
+ assertNotNull(NonWorkingRequest.newInstance("scheme://host/path"));
+ }
+
+ @Test
+ public void requireThatGuiceModulesAreInjected() {
+ Request request = NonWorkingRequest.newInstance("scheme://host/path", new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(String.class).annotatedWith(Names.named("foo")).toInstance("bar");
+ }
+ });
+ assertEquals("bar", request.container().getInstance(Key.get(String.class, Names.named("foo"))));
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingResponseHandlerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingResponseHandlerTestCase.java
new file mode 100644
index 00000000000..fb76e66e1e1
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingResponseHandlerTestCase.java
@@ -0,0 +1,25 @@
+// 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 org.junit.Test;
+
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class NonWorkingResponseHandlerTestCase {
+
+ @Test
+ public void requireThatHandleResponseThrowsException() {
+ NonWorkingResponseHandler handler = new NonWorkingResponseHandler();
+ try {
+ handler.handleResponse(new Response(Response.Status.OK));
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingServerTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingServerTestCase.java
new file mode 100644
index 00000000000..eccf4dbc655
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/test/NonWorkingServerTestCase.java
@@ -0,0 +1,35 @@
+// 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.service.ServerProvider;
+import org.junit.Test;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class NonWorkingServerTestCase {
+
+ @Test
+ public void requireThatStartDoesNotThrow() {
+ ServerProvider server = new NonWorkingServerProvider();
+ server.start();
+ }
+
+ @Test
+ public void requireThatCloseDoesNotThrow() {
+ ServerProvider server = new NonWorkingServerProvider();
+ server.close();
+ }
+
+ @Test
+ public void requireThatReferDoesNotThrow() {
+ ServerProvider server = new NonWorkingServerProvider();
+ server.refer();
+ }
+
+ @Test
+ public void requireThatReleaseDoesNotThrow() {
+ ServerProvider server = new NonWorkingServerProvider();
+ server.release();
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/test/ServerProviderConformanceTestTest.java b/jdisc_core/src/test/java/com/yahoo/jdisc/test/ServerProviderConformanceTestTest.java
new file mode 100644
index 00000000000..e59f8c96100
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/test/ServerProviderConformanceTestTest.java
@@ -0,0 +1,657 @@
+// 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.util.concurrent.SettableFuture;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.util.Modules;
+import com.yahoo.jdisc.NoopSharedResource;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.service.CurrentContainer;
+import com.yahoo.jdisc.service.ServerProvider;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class ServerProviderConformanceTestTest extends ServerProviderConformanceTest {
+
+ @Override
+ @Test
+ public void testContainerNotReadyException() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testBindingSetNotFoundException() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testNoBindingSetSelectedException() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testBindingNotFoundException() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestHandlerWithSyncCloseResponse() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestHandlerWithSyncWriteResponse() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestHandlerWithSyncHandleResponse() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestHandlerWithAsyncHandleResponse() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestException() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestExceptionWithSyncCloseResponse() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestExceptionWithSyncWriteResponse() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestNondeterministicExceptionWithSyncHandleResponse() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestExceptionBeforeResponseWriteWithSyncHandleResponse() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestExceptionAfterResponseWriteWithSyncHandleResponse() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestNondeterministicExceptionWithAsyncHandleResponse() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestExceptionBeforeResponseWriteWithAsyncHandleResponse() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestExceptionAfterResponseCloseNoContentWithAsyncHandleResponse() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestExceptionAfterResponseWriteWithAsyncHandleResponse() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteWithSyncCompletion() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteWithAsyncCompletion() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteWithNondeterministicSyncFailure() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteWithSyncFailureBeforeResponseWrite() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteWithSyncFailureAfterResponseWrite() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteWithNondeterministicAsyncFailure() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteWithAsyncFailureBeforeResponseWrite() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteWithAsyncFailureAfterResponseWrite() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteWithAsyncFailureAfterResponseCloseNoContent() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteNondeterministicException() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionBeforeResponseWrite() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionAfterResponseWrite() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionAfterResponseCloseNoContent() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteNondeterministicExceptionWithSyncCompletion() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionBeforeResponseWriteWithSyncCompletion() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionAfterResponseWriteWithSyncCompletion() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionAfterResponseCloseNoContentWithSyncCompletion() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteNondeterministicExceptionWithAsyncCompletion() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionBeforeResponseWriteWithAsyncCompletion() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionAfterResponseWriteWithAsyncCompletion() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionAfterResponseCloseNoContentWithAsyncCompletion() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionWithNondeterministicSyncFailure() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionWithSyncFailureBeforeResponseWrite() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionWithSyncFailureAfterResponseWrite() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionWithSyncFailureAfterResponseCloseNoContent() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionWithNondeterministicAsyncFailure() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionWithAsyncFailureBeforeResponseWrite() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionWithAsyncFailureAfterResponseWrite() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionWithAsyncFailureAfterResponseCloseNoContent() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseWithSyncCompletion() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseWithAsyncCompletion() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseWithNondeterministicSyncFailure() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseWithSyncFailureBeforeResponseWrite() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseWithSyncFailureAfterResponseWrite() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseWithSyncFailureAfterResponseCloseNoContent() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseWithNondeterministicAsyncFailure() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseWithAsyncFailureBeforeResponseWrite() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseWithAsyncFailureAfterResponseWrite() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseWithAsyncFailureAfterResponseCloseNoContent() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseNondeterministicException() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionBeforeResponseWrite() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionAfterResponseWrite() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionAfterResponseCloseNoContent() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseNondeterministicExceptionWithSyncCompletion() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionBeforeResponseWriteWithSyncCompletion() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionAfterResponseWriteWithSyncCompletion() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncCompletion() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseNondeterministicExceptionWithAsyncCompletion() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncCompletion() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionAfterResponseWriteWithAsyncCompletion() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncCompletion() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseNondeterministicExceptionWithSyncFailure() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionBeforeResponseWriteWithSyncFailure() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionAfterResponseWriteWithSyncFailure() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncFailure() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseNondeterministicExceptionWithAsyncFailure() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncFailure() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionAfterResponseWriteWithAsyncFailure() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncFailure() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testResponseWriteCompletionException() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testResponseCloseCompletionException() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ @Override
+ @Test
+ public void testResponseCloseCompletionExceptionNoContent() throws Throwable {
+ runTest(new MyAdapter());
+ }
+
+ private static void tryWrite(final ContentChannel out, final String str) {
+ try {
+ out.write(StandardCharsets.UTF_8.encode(str), null);
+ } catch (Throwable t) {
+ // Simulate handling the failure.
+ t.getMessage();
+ }
+ }
+
+ private static void tryClose(final ContentChannel out) {
+ try {
+ out.close(null);
+ } catch (Throwable t) {
+ // Simulate handling the failure.
+ t.getMessage();
+ }
+ }
+
+ private static void tryComplete(final CompletionHandler handler) {
+ try {
+ handler.completed();
+ } catch (Throwable t) {
+ // Simulate handling the failure.
+ t.getMessage();
+ }
+ }
+
+ private static class MyServer extends NoopSharedResource implements ServerProvider {
+
+ final CurrentContainer container;
+
+ @Inject
+ MyServer(final CurrentContainer container) {
+ this.container = container;
+ }
+
+ @Override
+ public void start() {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+ }
+
+ private static class MyClient {
+
+ final MyServer server;
+
+ MyClient(final MyServer server) {
+ this.server = server;
+ }
+
+ MyResponseHandler executeRequest(final boolean withRequestContent)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ final MyResponseHandler responseHandler = new MyResponseHandler();
+ final Request request;
+ try {
+ request = new Request(server.container, URI.create("http://localhost/"));
+ } catch (Throwable t) {
+ responseHandler.response.set(new Response(Response.Status.INTERNAL_SERVER_ERROR, t));
+ return responseHandler;
+ }
+ try {
+ final ContentChannel out = request.connect(responseHandler);
+ if (withRequestContent) {
+ tryWrite(out, "myRequestContent");
+ }
+ tryClose(out);
+ } catch (Throwable t) {
+ responseHandler.response.set(new Response(Response.Status.INTERNAL_SERVER_ERROR, t));
+ // Simulate handling the failure.
+ t.getMessage();
+ return responseHandler;
+ } finally {
+ request.release();
+ }
+ return responseHandler;
+ }
+ }
+
+ private static class MyResponseHandler implements ResponseHandler {
+
+ final SettableFuture<Response> response = SettableFuture.create();
+ final SettableFuture<String> content = SettableFuture.create();
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ @Override
+ public ContentChannel handleResponse(final Response response) {
+ this.response.set(response);
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ while (buf.hasRemaining()) {
+ out.write(buf.get());
+ }
+ tryComplete(handler);
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ content.set(new String(out.toByteArray(), StandardCharsets.UTF_8));
+ tryComplete(handler);
+ }
+ };
+ }
+ }
+
+ private static class MyAdapter implements Adapter<MyServer, MyClient, MyResponseHandler> {
+
+ @Override
+ public Module newConfigModule() {
+ return Modules.EMPTY_MODULE;
+ }
+
+ @Override
+ public Class<MyServer> getServerProviderClass() {
+ return MyServer.class;
+ }
+
+ @Override
+ public MyClient newClient(final MyServer server) throws Throwable {
+ return new MyClient(server);
+ }
+
+ @Override
+ public MyResponseHandler executeRequest(
+ final MyClient client,
+ final boolean withRequestContent) throws Throwable {
+ return client.executeRequest(withRequestContent);
+ }
+
+ @Override
+ public Iterable<ByteBuffer> newResponseContent() {
+ return Collections.singleton(StandardCharsets.UTF_8.encode("myResponseContent"));
+ }
+
+ @Override
+ public void validateResponse(final MyResponseHandler responseHandler) throws Throwable {
+ responseHandler.response.get(600, TimeUnit.SECONDS);
+ }
+ }
+}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/test/TestDriverTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/test/TestDriverTestCase.java
new file mode 100644
index 00000000000..bea231d19d9
--- /dev/null
+++ b/jdisc_core/src/test/java/com/yahoo/jdisc/test/TestDriverTestCase.java
@@ -0,0 +1,163 @@
+// 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.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.Application;
+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.RequestDeniedException;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.service.ContainerNotReadyException;
+import org.junit.Test;
+
+import java.nio.ByteBuffer;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class TestDriverTestCase {
+
+ @Test
+ public void requireThatFactoryMethodsWork() {
+ TestDriver.newInjectedApplicationInstance(MyApplication.class).close();
+ TestDriver.newInjectedApplicationInstanceWithoutOsgi(MyApplication.class).close();
+ TestDriver.newInjectedApplicationInstance(new MyApplication()).close();
+ TestDriver.newInjectedApplicationInstanceWithoutOsgi(new MyApplication()).close();
+ TestDriver.newSimpleApplicationInstance().close();
+ TestDriver.newSimpleApplicationInstanceWithoutOsgi().close();
+ }
+
+ @Test
+ public void requireThatAccessorsWork() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ assertNotNull(driver.bootstrapLoader());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatConnectRequestWorks() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ MyRequestHandler requestHandler = new MyRequestHandler(new MyContentChannel());
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind("scheme://host/path", requestHandler);
+ driver.activateContainer(builder);
+ ContentChannel content = driver.connectRequest("scheme://host/path", new MyResponseHandler());
+ assertNotNull(content);
+ content.close(null);
+ assertNotNull(requestHandler.handler);
+ requestHandler.handler.handleResponse(new Response(Response.Status.OK)).close(null);
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatDispatchRequestWorks() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ MyRequestHandler requestHandler = new MyRequestHandler(new MyContentChannel());
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind("scheme://host/path", requestHandler);
+ driver.activateContainer(builder);
+ driver.dispatchRequest("scheme://host/path", new MyResponseHandler());
+ assertNotNull(requestHandler.handler);
+ assertTrue(requestHandler.content.closed);
+ requestHandler.handler.handleResponse(new Response(Response.Status.OK)).close(null);
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatFailedRequestCreateDoesNotBlockClose() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ try {
+ driver.connectRequest("scheme://host/path", new MyResponseHandler());
+ fail();
+ } catch (ContainerNotReadyException e) {
+
+ }
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatFailedRequestConnectDoesNotBlockClose() {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind("scheme://host/path", new MyRequestHandler(null));
+ driver.activateContainer(builder);
+ try {
+ driver.connectRequest("scheme://host/path", new MyResponseHandler());
+ fail();
+ } catch (RequestDeniedException e) {
+
+ }
+ assertTrue(driver.close());
+ }
+
+ private static class MyApplication implements Application {
+
+ @Override
+ public void start() {
+
+ }
+
+ @Override
+ public void stop() {
+
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+ }
+
+ private static class MyRequestHandler extends AbstractRequestHandler {
+
+ final MyContentChannel content;
+ ResponseHandler handler;
+
+ MyRequestHandler(MyContentChannel content) {
+ this.content = content;
+ }
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ this.handler = handler;
+ if (content == null) {
+ throw new RequestDeniedException(request);
+ }
+ return content;
+ }
+ }
+
+ private static class MyResponseHandler implements ResponseHandler {
+
+ final MyContentChannel content = new MyContentChannel();
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ return content;
+ }
+ }
+
+ private static class MyContentChannel implements ContentChannel {
+
+ boolean closed = false;
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ closed = true;
+ handler.completed();
+ }
+ }
+}
diff --git a/jdisc_core/src/test/perl/help.Levent.expected b/jdisc_core/src/test/perl/help.Levent.expected
new file mode 100644
index 00000000000..b35e2d9c36b
--- /dev/null
+++ b/jdisc_core/src/test/perl/help.Levent.expected
@@ -0,0 +1,20 @@
+Usage: jdisc_logfmt [options] [inputfile ...]
+Options:
+ -l LEVELLIST --level=LEVELLIST select levels to include
+ -L LEVELLIST --add-level=LEVELLIST define extra levels
+ -s FIELDLIST --show=FIELDLIST select fields to print
+ -p PID --pid=PID select messages from given PID
+ -S SERVICE --service=SERVICE select messages from given SERVICE
+ -H HOST --host=HOST select messages from given HOST
+ -c REGEX --component=REGEX select components matching REGEX
+ -m REGEX --message=REGEX select message text matching REGEX
+ -f --follow invoke tail -F to follow input file
+ -N --nldequote dequote newlines in message text field
+ -t --tc --truncatecomponent chop component to 15 chars
+ --ts --truncateservice chop service to 9 chars
+
+FIELDLIST is comma separated, available fields:
+ time fmttime msecs usecs host level pid service component message
+Available levels for LEVELLIST:
+ debug error event info unknown warning
+for both lists, use 'all' for all possible values, and -xxx to disable xxx.
diff --git a/jdisc_core/src/test/perl/help.expected b/jdisc_core/src/test/perl/help.expected
new file mode 100644
index 00000000000..58da8183696
--- /dev/null
+++ b/jdisc_core/src/test/perl/help.expected
@@ -0,0 +1,20 @@
+Usage: jdisc_logfmt [options] [inputfile ...]
+Options:
+ -l LEVELLIST --level=LEVELLIST select levels to include
+ -L LEVELLIST --add-level=LEVELLIST define extra levels
+ -s FIELDLIST --show=FIELDLIST select fields to print
+ -p PID --pid=PID select messages from given PID
+ -S SERVICE --service=SERVICE select messages from given SERVICE
+ -H HOST --host=HOST select messages from given HOST
+ -c REGEX --component=REGEX select components matching REGEX
+ -m REGEX --message=REGEX select message text matching REGEX
+ -f --follow invoke tail -F to follow input file
+ -N --nldequote dequote newlines in message text field
+ -t --tc --truncatecomponent chop component to 15 chars
+ --ts --truncateservice chop service to 9 chars
+
+FIELDLIST is comma separated, available fields:
+ time fmttime msecs usecs host level pid service component message
+Available levels for LEVELLIST:
+ debug error info unknown warning
+for both lists, use 'all' for all possible values, and -xxx to disable xxx.
diff --git a/jdisc_core/src/test/perl/jdisc.expected b/jdisc_core/src/test/perl/jdisc.expected
new file mode 100644
index 00000000000..6c5d139c0cf
--- /dev/null
+++ b/jdisc_core/src/test/perl/jdisc.expected
@@ -0,0 +1,17 @@
+[2013-01-25 12:46:51.180] INFO : - org.apache.felix.framework ServiceEvent REGISTERED
+[2013-01-25 12:46:51.192] INFO : - jdisc_core.app-a BundleEvent INSTALLED
+[2013-01-25 12:46:51.249] INFO : - jdisc_core.app-a BundleEvent RESOLVED
+[2013-01-25 12:46:51.249] INFO : - jdisc_core.app-a BundleEvent STARTED
+[2013-01-25 12:46:51.334] INFO : - jdisc_core.app-a BundleEvent STOPPED
+[2013-01-25 12:46:51.335] INFO : - jdisc_core.app-a BundleEvent UNRESOLVED
+[2013-01-25 12:46:51.335] INFO : - jdisc_core.app-a BundleEvent UNINSTALLED
+[2013-01-25 12:46:51.376] INFO : - org.apache.felix.framework ServiceEvent REGISTERED
+[2013-01-25 12:46:51.377] ERROR : - jdisc_core.app-a my_error
+[2013-01-25 12:46:51.377] WARNING : - jdisc_core.app-a my_warning
+[2013-01-25 12:46:51.377] INFO : - jdisc_core.app-a my_info
+[2013-01-25 12:46:51.379] INFO : - jdisc_core.app-a BundleEvent INSTALLED
+[2013-01-25 12:46:51.383] INFO : - jdisc_core.app-a BundleEvent RESOLVED
+[2013-01-25 12:46:51.383] INFO : - jdisc_core.app-a BundleEvent STARTED
+[2013-01-25 12:46:51.389] INFO : - jdisc_core.app-a BundleEvent STOPPED
+[2013-01-25 12:46:51.389] INFO : - jdisc_core.app-a BundleEvent UNRESOLVED
+[2013-01-25 12:46:51.390] INFO : - jdisc_core.app-a BundleEvent UNINSTALLED
diff --git a/jdisc_core/src/test/perl/jdisc.lall.expected b/jdisc_core/src/test/perl/jdisc.lall.expected
new file mode 100644
index 00000000000..58875bba8db
--- /dev/null
+++ b/jdisc_core/src/test/perl/jdisc.lall.expected
@@ -0,0 +1,19 @@
+[2013-01-25 12:46:51.180] INFO : - org.apache.felix.framework ServiceEvent REGISTERED
+[2013-01-25 12:46:51.192] INFO : - jdisc_core.app-a BundleEvent INSTALLED
+[2013-01-25 12:46:51.249] INFO : - jdisc_core.app-a BundleEvent RESOLVED
+[2013-01-25 12:46:51.249] INFO : - jdisc_core.app-a BundleEvent STARTED
+[2013-01-25 12:46:51.334] INFO : - jdisc_core.app-a BundleEvent STOPPED
+[2013-01-25 12:46:51.335] INFO : - jdisc_core.app-a BundleEvent UNRESOLVED
+[2013-01-25 12:46:51.335] INFO : - jdisc_core.app-a BundleEvent UNINSTALLED
+[2013-01-25 12:46:51.376] INFO : - org.apache.felix.framework ServiceEvent REGISTERED
+[2013-01-25 12:46:51.377] ERROR : - jdisc_core.app-a my_error
+[2013-01-25 12:46:51.377] WARNING : - jdisc_core.app-a my_warning
+[2013-01-25 12:46:51.377] INFO : - jdisc_core.app-a my_info
+[2013-01-25 12:46:51.377] DEBUG : - jdisc_core.app-a my_debug
+[2013-01-25 12:46:51.377] UNKNOWN : - jdisc_core.app-a my_unknown
+[2013-01-25 12:46:51.379] INFO : - jdisc_core.app-a BundleEvent INSTALLED
+[2013-01-25 12:46:51.383] INFO : - jdisc_core.app-a BundleEvent RESOLVED
+[2013-01-25 12:46:51.383] INFO : - jdisc_core.app-a BundleEvent STARTED
+[2013-01-25 12:46:51.389] INFO : - jdisc_core.app-a BundleEvent STOPPED
+[2013-01-25 12:46:51.389] INFO : - jdisc_core.app-a BundleEvent UNRESOLVED
+[2013-01-25 12:46:51.390] INFO : - jdisc_core.app-a BundleEvent UNINSTALLED
diff --git a/jdisc_core/src/test/perl/jdisc.lall_info.expected b/jdisc_core/src/test/perl/jdisc.lall_info.expected
new file mode 100644
index 00000000000..da834dddc9b
--- /dev/null
+++ b/jdisc_core/src/test/perl/jdisc.lall_info.expected
@@ -0,0 +1,4 @@
+[2013-01-25 12:46:51.377] ERROR : - jdisc_core.app-a my_error
+[2013-01-25 12:46:51.377] WARNING : - jdisc_core.app-a my_warning
+[2013-01-25 12:46:51.377] DEBUG : - jdisc_core.app-a my_debug
+[2013-01-25 12:46:51.377] UNKNOWN : - jdisc_core.app-a my_unknown
diff --git a/jdisc_core/src/test/perl/jdisc.log b/jdisc_core/src/test/perl/jdisc.log
new file mode 100644
index 00000000000..37a74a595ca
--- /dev/null
+++ b/jdisc_core/src/test/perl/jdisc.log
@@ -0,0 +1,19 @@
+1359114411.180 gentleadd-lm 35172 - org.apache.felix.framework info ServiceEvent REGISTERED
+1359114411.192 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent INSTALLED
+1359114411.249 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent RESOLVED
+1359114411.249 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent STARTED
+1359114411.334 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent STOPPED
+1359114411.335 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent UNRESOLVED
+1359114411.335 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent UNINSTALLED
+1359114411.376 gentleadd-lm 35172 - org.apache.felix.framework info ServiceEvent REGISTERED
+1359114411.377 gentleadd-lm 35172 - jdisc_core.app-a error my_error
+1359114411.377 gentleadd-lm 35172 - jdisc_core.app-a warning my_warning
+1359114411.377 gentleadd-lm 35172 - jdisc_core.app-a info my_info
+1359114411.377 gentleadd-lm 35172 - jdisc_core.app-a debug my_debug
+1359114411.377 gentleadd-lm 35172 - jdisc_core.app-a unknown my_unknown
+1359114411.379 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent INSTALLED
+1359114411.383 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent RESOLVED
+1359114411.383 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent STARTED
+1359114411.389 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent STOPPED
+1359114411.389 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent UNRESOLVED
+1359114411.390 gentleadd-lm 35172 - jdisc_core.app-a info BundleEvent UNINSTALLED
diff --git a/jdisc_core/src/test/perl/jdisc.spid.expected b/jdisc_core/src/test/perl/jdisc.spid.expected
new file mode 100644
index 00000000000..44963161dcf
--- /dev/null
+++ b/jdisc_core/src/test/perl/jdisc.spid.expected
@@ -0,0 +1,17 @@
+[2013-01-25 12:46:51.180] INFO : 35172 - org.apache.felix.framework ServiceEvent REGISTERED
+[2013-01-25 12:46:51.192] INFO : 35172 - jdisc_core.app-a BundleEvent INSTALLED
+[2013-01-25 12:46:51.249] INFO : 35172 - jdisc_core.app-a BundleEvent RESOLVED
+[2013-01-25 12:46:51.249] INFO : 35172 - jdisc_core.app-a BundleEvent STARTED
+[2013-01-25 12:46:51.334] INFO : 35172 - jdisc_core.app-a BundleEvent STOPPED
+[2013-01-25 12:46:51.335] INFO : 35172 - jdisc_core.app-a BundleEvent UNRESOLVED
+[2013-01-25 12:46:51.335] INFO : 35172 - jdisc_core.app-a BundleEvent UNINSTALLED
+[2013-01-25 12:46:51.376] INFO : 35172 - org.apache.felix.framework ServiceEvent REGISTERED
+[2013-01-25 12:46:51.377] ERROR : 35172 - jdisc_core.app-a my_error
+[2013-01-25 12:46:51.377] WARNING : 35172 - jdisc_core.app-a my_warning
+[2013-01-25 12:46:51.377] INFO : 35172 - jdisc_core.app-a my_info
+[2013-01-25 12:46:51.379] INFO : 35172 - jdisc_core.app-a BundleEvent INSTALLED
+[2013-01-25 12:46:51.383] INFO : 35172 - jdisc_core.app-a BundleEvent RESOLVED
+[2013-01-25 12:46:51.383] INFO : 35172 - jdisc_core.app-a BundleEvent STARTED
+[2013-01-25 12:46:51.389] INFO : 35172 - jdisc_core.app-a BundleEvent STOPPED
+[2013-01-25 12:46:51.389] INFO : 35172 - jdisc_core.app-a BundleEvent UNRESOLVED
+[2013-01-25 12:46:51.390] INFO : 35172 - jdisc_core.app-a BundleEvent UNINSTALLED
diff --git a/jdisc_core/src/test/perl/jdisc_logfmt_test.sh b/jdisc_core/src/test/perl/jdisc_logfmt_test.sh
new file mode 100755
index 00000000000..bb7e92ed8cf
--- /dev/null
+++ b/jdisc_core/src/test/perl/jdisc_logfmt_test.sh
@@ -0,0 +1,35 @@
+#!/bin/sh
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+MYPATH=`dirname ${0}`
+DIFF=/usr/bin/diff
+LOGFMT=${1}
+
+if [ -e "/usr/local/bin/perl" ]; then
+ echo "Running jdisc_logfmt test suite."
+else
+ echo "Ignoring jdisc_logfmt test suite as there is no /usr/local/bin/perl"
+ exit 0
+fi
+
+set -e
+export TZ=CET
+export VESPA_HOME=$(mktemp -d /tmp/mockup-vespahome-XXXXXX)/
+mkdir -p $VESPA_HOME/libexec/vespa
+touch $VESPA_HOME/libexec/vespa/common-env.sh
+
+echo
+
+${LOGFMT} -h 2>&1 | ${DIFF} - ${MYPATH}/help.expected
+${LOGFMT} -h -L event 2>&1 | ${DIFF} - ${MYPATH}/help.Levent.expected
+
+${LOGFMT} ${MYPATH}/jdisc.log 2>&1 | ${DIFF} - ${MYPATH}/jdisc.expected
+${LOGFMT} -l all ${MYPATH}/jdisc.log 2>&1 | ${DIFF} - ${MYPATH}/jdisc.lall.expected
+${LOGFMT} -l all,-info ${MYPATH}/jdisc.log 2>&1 | ${DIFF} - ${MYPATH}/jdisc.lall_info.expected
+${LOGFMT} -s +pid ${MYPATH}/jdisc.log 2>&1 | ${DIFF} - ${MYPATH}/jdisc.spid.expected
+
+${LOGFMT} ${MYPATH}/vespa.log 2>&1 | ${DIFF} - ${MYPATH}/vespa.expected
+${LOGFMT} -L event ${MYPATH}/vespa.log 2>&1 | ${DIFF} - ${MYPATH}/vespa.Levent.expected
+${LOGFMT} -L event -l all ${MYPATH}/vespa.log 2>&1 | ${DIFF} - ${MYPATH}/vespa.Levent.lall.expected
+
+rm -r ${VESPA_HOME}
+echo All tests passed.
diff --git a/jdisc_core/src/test/perl/vespa.Levent.expected b/jdisc_core/src/test/perl/vespa.Levent.expected
new file mode 100644
index 00000000000..334ba5f5b28
--- /dev/null
+++ b/jdisc_core/src/test/perl/vespa.Levent.expected
@@ -0,0 +1,9 @@
+[2012-11-27 14:22:48.120] INFO : configserver stdout ROOT = /home/vespa
+[2012-11-27 14:22:48.232] INFO : configserver stdout Running without a pid file.
+[2012-11-27 14:22:48.336] INFO : configserver stdout LANG = en_US.UTF-8
+[2012-11-27 14:22:48.393] INFO : configserver stdout env LD_PRELOAD=/home/vespa/libexec64/yjava_daemon_preload.so: /home/vespa/bin64/yjava_daemon -sentinel -rlimit_files 16384 -logdest /home/vespa/logs/vespa/yjava_daemon.out -loglevel error -jvm server -maxrestarts 0 -procs 1 -home /home/vespa/share/yjava_jdk/java -Djava.library.path=/home/vespa/lib64:/home/vespa/lib64 -Dvespa.log.control.dir=/home/vespa/var/db/vespa/logcontrol -XX:ThreadStackSize=512 -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=128 -XX:MaxPermSize=512m -Dconfig.id=dir:/home/vespa/conf/configserver -Dyjava_remote_ip_servlet_filter.logLevel=DEBUG -Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.Jdk14Logger -Dcom.yahoo.protect.Process.forcedExitActive=true -Dzookeeperlogfile=/home/vespa/logs/vespa/zookeeper.log -Xms1536m -Xmx1536m -XX:MaxDirectMemorySize=267m -Djava.awt.headless=true -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/vespa/var/crash -Dsun.net.client.defaultConnectTimeout=5000 -Dsun.net.client.defaultReadTimeout=60000 -Djavax.net.ssl.keyStoreType=JKS -javaagent:/home/vespa/lib/jars/yjava_ysecure_agent.jar -Djdisc_core.config.file=/home/vespa/var/jdisc_core/config.properties -Djdisc.export.packages=yjava.security.ysecure,yjava.security.yca,yjava.security.yck,org.apache.velocity,org.apache.velocity.app,org.apache.velocity.context,org.apache.velocity.runtime,org.apache.velocity.runtime.log, -Djdisc.cache.path=/home/vespa/var/vespa/bundlecache/dir:_home_y_conf_configserver -Djdisc.debug.resources=false -Djdisc.bundle.path=/home/vespa/lib/jars -Djdisc.logger.enabled=false -Djdisc.logger.level=ALL -Djdisc.logger.tag=dir:/home/vespa/conf/configserver -user yahoo -cp /home/vespa/lib/jars/jdisc_core-with-dependencies.jar:lib/jars/yjava_bcookie.jar:lib/jars/yjava_bcookie_jni.jar:lib/jars/yjava_byauth.jar:lib/jars/yjava_cookie_data_servlet_filter.jar:lib/jars/yjava_daemon.jar:lib/jars/yjava_jmx_singleton_server.jar:lib/jars/yjava_remote_ip_servlet_filter.jar:lib/jars/yjava_resource_handler.jar:lib/jars/yjava_servlet.jar:lib/jars/yjava_servlet_filters.jar:lib/jars/yjava_yca.jar:lib/jars/yjava_yck.jar:lib/jars/yjava_yhdrs:lib/jars/yjava_yiv.jar:lib/jars/yjava_yiv_servlet.jar:lib/jars/yjava_ynet.jar:lib/jars/yjava_ysecure.jar:lib/jars/yjava_ysecure_agent.jar:lib/jars/yjava_ysecure_native.jar:share/jports/org_apache_velocity__velocity.jar:share/jports/commons_collections__commons_collections.jar:share/jports/commons_lang__commons_lang.jar: -ynet FILTER_YAHOO_ANY --wait-for-jvm-init 10 -Dzookeeper.jmx.log4j.disable=true -Dconfigsources=tcp/localhost:19070 -XX:OnOutOfMemoryError="kill -9 %p" com.yahoo.jdisc.core.BootstrapDaemon file:/home/vespa/lib/jars/container-disc-with-dependencies.jar
+[2012-11-27 14:22:50.876] INFO : configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi Installing bundles from the latest application
+[2012-11-27 14:22:50.884] INFO : configserver Container.com.yahoo.container.handler.BundleLoader Installing bundle from disk with reference 'file:/home/vespa/lib/jars/config-models/sherpa-config-model-plugin.jar'
+[2012-11-27 14:22:50.889] INFO : configserver Container.com.yahoo.container.handler.BundleLoader Installing bundle from disk with reference 'file:/home/vespa/lib/jars/configserver-container-plugin.jar'
+[2012-11-27 14:22:54.285] INFO : configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi Installing bundles from the latest application
+[2012-11-27 14:22:57.489] INFO : configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi Switched to the latest deployed set of handlers, and dependent components, e.g. search chains, searchers and document processors. Application switch number: 0
diff --git a/jdisc_core/src/test/perl/vespa.Levent.lall.expected b/jdisc_core/src/test/perl/vespa.Levent.lall.expected
new file mode 100644
index 00000000000..ef58eee7bfc
--- /dev/null
+++ b/jdisc_core/src/test/perl/vespa.Levent.lall.expected
@@ -0,0 +1,19 @@
+[2012-11-27 14:22:48.091] EVENT : configserver runserver starting/1 name="/home/vespa/bin/vespa-start-container-daemon -Dzookeeper.jmx.log4j.disable=true -Dconfigsources=tcp/localhost:19070 (pid 18151)"
+[2012-11-27 14:22:48.100] DEBUG : configserver qrs-start exporting: YELL_MA_EURO=INXIGHT
+[2012-11-27 14:22:48.100] DEBUG : configserver qrs-start Not setting ulimit -v, no limit set.
+[2012-11-27 14:22:48.120] INFO : configserver stdout ROOT = /home/vespa
+[2012-11-27 14:22:48.232] INFO : configserver stdout Running without a pid file.
+[2012-11-27 14:22:48.336] INFO : configserver stdout LANG = en_US.UTF-8
+[2012-11-27 14:22:48.393] INFO : configserver stdout env LD_PRELOAD=/home/vespa/libexec64/yjava_daemon_preload.so: /home/vespa/bin64/yjava_daemon -sentinel -rlimit_files 16384 -logdest /home/vespa/logs/vespa/yjava_daemon.out -loglevel error -jvm server -maxrestarts 0 -procs 1 -home /home/vespa/share/yjava_jdk/java -Djava.library.path=/home/vespa/lib64:/home/vespa/lib64 -Dvespa.log.control.dir=/home/vespa/var/db/vespa/logcontrol -XX:ThreadStackSize=512 -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=128 -XX:MaxPermSize=512m -Dconfig.id=dir:/home/vespa/conf/configserver -Dyjava_remote_ip_servlet_filter.logLevel=DEBUG -Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.Jdk14Logger -Dcom.yahoo.protect.Process.forcedExitActive=true -Dzookeeperlogfile=/home/vespa/logs/vespa/zookeeper.log -Xms1536m -Xmx1536m -XX:MaxDirectMemorySize=267m -Djava.awt.headless=true -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/vespa/var/crash -Dsun.net.client.defaultConnectTimeout=5000 -Dsun.net.client.defaultReadTimeout=60000 -Djavax.net.ssl.keyStoreType=JKS -javaagent:/home/vespa/lib/jars/yjava_ysecure_agent.jar -Djdisc_core.config.file=/home/vespa/var/jdisc_core/config.properties -Djdisc.export.packages=yjava.security.ysecure,yjava.security.yca,yjava.security.yck,org.apache.velocity,org.apache.velocity.app,org.apache.velocity.context,org.apache.velocity.runtime,org.apache.velocity.runtime.log, -Djdisc.cache.path=/home/vespa/var/vespa/bundlecache/dir:_home_y_conf_configserver -Djdisc.debug.resources=false -Djdisc.bundle.path=/home/vespa/lib/jars -Djdisc.logger.enabled=false -Djdisc.logger.level=ALL -Djdisc.logger.tag=dir:/home/vespa/conf/configserver -user yahoo -cp /home/vespa/lib/jars/jdisc_core-with-dependencies.jar:lib/jars/yjava_bcookie.jar:lib/jars/yjava_bcookie_jni.jar:lib/jars/yjava_byauth.jar:lib/jars/yjava_cookie_data_servlet_filter.jar:lib/jars/yjava_daemon.jar:lib/jars/yjava_jmx_singleton_server.jar:lib/jars/yjava_remote_ip_servlet_filter.jar:lib/jars/yjava_resource_handler.jar:lib/jars/yjava_servlet.jar:lib/jars/yjava_servlet_filters.jar:lib/jars/yjava_yca.jar:lib/jars/yjava_yck.jar:lib/jars/yjava_yhdrs:lib/jars/yjava_yiv.jar:lib/jars/yjava_yiv_servlet.jar:lib/jars/yjava_ynet.jar:lib/jars/yjava_ysecure.jar:lib/jars/yjava_ysecure_agent.jar:lib/jars/yjava_ysecure_native.jar:share/jports/org_apache_velocity__velocity.jar:share/jports/commons_collections__commons_collections.jar:share/jports/commons_lang__commons_lang.jar: -ynet FILTER_YAHOO_ANY --wait-for-jvm-init 10 -Dzookeeper.jmx.log4j.disable=true -Dconfigsources=tcp/localhost:19070 -XX:OnOutOfMemoryError="kill -9 %p" com.yahoo.jdisc.core.BootstrapDaemon file:/home/vespa/lib/jars/container-disc-with-dependencies.jar
+[2012-11-27 14:22:50.876] INFO : configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi Installing bundles from the latest application
+[2012-11-27 14:22:50.884] INFO : configserver Container.com.yahoo.container.handler.BundleLoader Installing bundle from disk with reference 'file:/home/vespa/lib/jars/config-models/sherpa-config-model-plugin.jar'
+[2012-11-27 14:22:50.889] INFO : configserver Container.com.yahoo.container.handler.BundleLoader Installing bundle from disk with reference 'file:/home/vespa/lib/jars/configserver-container-plugin.jar'
+[2012-11-27 14:22:54.285] INFO : configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi Installing bundles from the latest application
+[2012-11-27 14:22:57.430] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=requests value=0
+[2012-11-27 14:22:57.431] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=requestsCached value=0
+[2012-11-27 14:22:57.432] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=requestsNotCached value=0
+[2012-11-27 14:22:57.432] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=failedRequests value=0
+[2012-11-27 14:22:57.432] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=procTime value=0
+[2012-11-27 14:22:57.432] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=procTimeCached value=0
+[2012-11-27 14:22:57.432] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=procTimeNotCached value=0
+[2012-11-27 14:22:57.489] INFO : configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi Switched to the latest deployed set of handlers, and dependent components, e.g. search chains, searchers and document processors. Application switch number: 0
diff --git a/jdisc_core/src/test/perl/vespa.expected b/jdisc_core/src/test/perl/vespa.expected
new file mode 100644
index 00000000000..897a7084dae
--- /dev/null
+++ b/jdisc_core/src/test/perl/vespa.expected
@@ -0,0 +1,18 @@
+Warning: unknown level 'event' in input
+[2012-11-27 14:22:48.091] EVENT : configserver runserver starting/1 name="/home/vespa/bin/vespa-start-container-daemon -Dzookeeper.jmx.log4j.disable=true -Dconfigsources=tcp/localhost:19070 (pid 18151)"
+[2012-11-27 14:22:48.120] INFO : configserver stdout ROOT = /home/vespa
+[2012-11-27 14:22:48.232] INFO : configserver stdout Running without a pid file.
+[2012-11-27 14:22:48.336] INFO : configserver stdout LANG = en_US.UTF-8
+[2012-11-27 14:22:48.393] INFO : configserver stdout env LD_PRELOAD=/home/vespa/libexec64/yjava_daemon_preload.so: /home/vespa/bin64/yjava_daemon -sentinel -rlimit_files 16384 -logdest /home/vespa/logs/vespa/yjava_daemon.out -loglevel error -jvm server -maxrestarts 0 -procs 1 -home /home/vespa/share/yjava_jdk/java -Djava.library.path=/home/vespa/lib64:/home/vespa/lib64 -Dvespa.log.control.dir=/home/vespa/var/db/vespa/logcontrol -XX:ThreadStackSize=512 -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=128 -XX:MaxPermSize=512m -Dconfig.id=dir:/home/vespa/conf/configserver -Dyjava_remote_ip_servlet_filter.logLevel=DEBUG -Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.Jdk14Logger -Dcom.yahoo.protect.Process.forcedExitActive=true -Dzookeeperlogfile=/home/vespa/logs/vespa/zookeeper.log -Xms1536m -Xmx1536m -XX:MaxDirectMemorySize=267m -Djava.awt.headless=true -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/vespa/var/crash -Dsun.net.client.defaultConnectTimeout=5000 -Dsun.net.client.defaultReadTimeout=60000 -Djavax.net.ssl.keyStoreType=JKS -javaagent:/home/vespa/lib/jars/yjava_ysecure_agent.jar -Djdisc_core.config.file=/home/vespa/var/jdisc_core/config.properties -Djdisc.export.packages=yjava.security.ysecure,yjava.security.yca,yjava.security.yck,org.apache.velocity,org.apache.velocity.app,org.apache.velocity.context,org.apache.velocity.runtime,org.apache.velocity.runtime.log, -Djdisc.cache.path=/home/vespa/var/vespa/bundlecache/dir:_home_y_conf_configserver -Djdisc.debug.resources=false -Djdisc.bundle.path=/home/vespa/lib/jars -Djdisc.logger.enabled=false -Djdisc.logger.level=ALL -Djdisc.logger.tag=dir:/home/vespa/conf/configserver -user yahoo -cp /home/vespa/lib/jars/jdisc_core-with-dependencies.jar:lib/jars/yjava_bcookie.jar:lib/jars/yjava_bcookie_jni.jar:lib/jars/yjava_byauth.jar:lib/jars/yjava_cookie_data_servlet_filter.jar:lib/jars/yjava_daemon.jar:lib/jars/yjava_jmx_singleton_server.jar:lib/jars/yjava_remote_ip_servlet_filter.jar:lib/jars/yjava_resource_handler.jar:lib/jars/yjava_servlet.jar:lib/jars/yjava_servlet_filters.jar:lib/jars/yjava_yca.jar:lib/jars/yjava_yck.jar:lib/jars/yjava_yhdrs:lib/jars/yjava_yiv.jar:lib/jars/yjava_yiv_servlet.jar:lib/jars/yjava_ynet.jar:lib/jars/yjava_ysecure.jar:lib/jars/yjava_ysecure_agent.jar:lib/jars/yjava_ysecure_native.jar:share/jports/org_apache_velocity__velocity.jar:share/jports/commons_collections__commons_collections.jar:share/jports/commons_lang__commons_lang.jar: -ynet FILTER_YAHOO_ANY --wait-for-jvm-init 10 -Dzookeeper.jmx.log4j.disable=true -Dconfigsources=tcp/localhost:19070 -XX:OnOutOfMemoryError="kill -9 %p" com.yahoo.jdisc.core.BootstrapDaemon file:/home/vespa/lib/jars/container-disc-with-dependencies.jar
+[2012-11-27 14:22:50.876] INFO : configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi Installing bundles from the latest application
+[2012-11-27 14:22:50.884] INFO : configserver Container.com.yahoo.container.handler.BundleLoader Installing bundle from disk with reference 'file:/home/vespa/lib/jars/config-models/sherpa-config-model-plugin.jar'
+[2012-11-27 14:22:50.889] INFO : configserver Container.com.yahoo.container.handler.BundleLoader Installing bundle from disk with reference 'file:/home/vespa/lib/jars/configserver-container-plugin.jar'
+[2012-11-27 14:22:54.285] INFO : configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi Installing bundles from the latest application
+[2012-11-27 14:22:57.430] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=requests value=0
+[2012-11-27 14:22:57.431] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=requestsCached value=0
+[2012-11-27 14:22:57.432] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=requestsNotCached value=0
+[2012-11-27 14:22:57.432] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=failedRequests value=0
+[2012-11-27 14:22:57.432] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=procTime value=0
+[2012-11-27 14:22:57.432] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=procTimeCached value=0
+[2012-11-27 14:22:57.432] EVENT : configserver Container.com.yahoo.statistics.Counter count/1 name=procTimeNotCached value=0
+[2012-11-27 14:22:57.489] INFO : configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi Switched to the latest deployed set of handlers, and dependent components, e.g. search chains, searchers and document processors. Application switch number: 0
diff --git a/jdisc_core/src/test/perl/vespa.log b/jdisc_core/src/test/perl/vespa.log
new file mode 100644
index 00000000000..36210bdb798
--- /dev/null
+++ b/jdisc_core/src/test/perl/vespa.log
@@ -0,0 +1,19 @@
+1354022568.091108 example.yahoo.com 18150/38735 configserver runserver event starting/1 name="/home/vespa/bin/vespa-start-container-daemon -Dzookeeper.jmx.log4j.disable=true -Dconfigsources=tcp/localhost:19070 (pid 18151)"
+1354022568.100151 example.yahoo.com 18151 configserver qrs-start debug exporting: YELL_MA_EURO=INXIGHT
+1354022568.100217 example.yahoo.com 18151 configserver qrs-start debug Not setting ulimit -v, no limit set.
+1354022568.120716 example.yahoo.com 18151 configserver stdout info ROOT = /home/vespa
+1354022568.232852 example.yahoo.com 18151 configserver stdout info Running without a pid file.
+1354022568.336341 example.yahoo.com 18151 configserver stdout info LANG = en_US.UTF-8
+1354022568.393706 example.yahoo.com 18151 configserver stdout info env LD_PRELOAD=/home/vespa/libexec64/yjava_daemon_preload.so: /home/vespa/bin64/yjava_daemon -sentinel -rlimit_files 16384 -logdest /home/vespa/logs/vespa/yjava_daemon.out -loglevel error -jvm server -maxrestarts 0 -procs 1 -home /home/vespa/share/yjava_jdk/java -Djava.library.path=/home/vespa/lib64:/home/vespa/lib64 -Dvespa.log.control.dir=/home/vespa/var/db/vespa/logcontrol -XX:ThreadStackSize=512 -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=128 -XX:MaxPermSize=512m -Dconfig.id=dir:/home/vespa/conf/configserver -Dyjava_remote_ip_servlet_filter.logLevel=DEBUG -Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.Jdk14Logger -Dcom.yahoo.protect.Process.forcedExitActive=true -Dzookeeperlogfile=/home/vespa/logs/vespa/zookeeper.log -Xms1536m -Xmx1536m -XX:MaxDirectMemorySize=267m -Djava.awt.headless=true -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/vespa/var/crash -Dsun.net.client.defaultConnectTimeout=5000 -Dsun.net.client.defaultReadTimeout=60000 -Djavax.net.ssl.keyStoreType=JKS -javaagent:/home/vespa/lib/jars/yjava_ysecure_agent.jar -Djdisc_core.config.file=/home/vespa/var/jdisc_core/config.properties -Djdisc.export.packages=yjava.security.ysecure,yjava.security.yca,yjava.security.yck,org.apache.velocity,org.apache.velocity.app,org.apache.velocity.context,org.apache.velocity.runtime,org.apache.velocity.runtime.log, -Djdisc.cache.path=/home/vespa/var/vespa/bundlecache/dir:_home_y_conf_configserver -Djdisc.debug.resources=false -Djdisc.bundle.path=/home/vespa/lib/jars -Djdisc.logger.enabled=false -Djdisc.logger.level=ALL -Djdisc.logger.tag=dir:/home/vespa/conf/configserver -user yahoo -cp /home/vespa/lib/jars/jdisc_core-with-dependencies.jar:lib/jars/yjava_bcookie.jar:lib/jars/yjava_bcookie_jni.jar:lib/jars/yjava_byauth.jar:lib/jars/yjava_cookie_data_servlet_filter.jar:lib/jars/yjava_daemon.jar:lib/jars/yjava_jmx_singleton_server.jar:lib/jars/yjava_remote_ip_servlet_filter.jar:lib/jars/yjava_resource_handler.jar:lib/jars/yjava_servlet.jar:lib/jars/yjava_servlet_filters.jar:lib/jars/yjava_yca.jar:lib/jars/yjava_yck.jar:lib/jars/yjava_yhdrs:lib/jars/yjava_yiv.jar:lib/jars/yjava_yiv_servlet.jar:lib/jars/yjava_ynet.jar:lib/jars/yjava_ysecure.jar:lib/jars/yjava_ysecure_agent.jar:lib/jars/yjava_ysecure_native.jar:share/jports/org_apache_velocity__velocity.jar:share/jports/commons_collections__commons_collections.jar:share/jports/commons_lang__commons_lang.jar: -ynet FILTER_YAHOO_ANY --wait-for-jvm-init 10 -Dzookeeper.jmx.log4j.disable=true -Dconfigsources=tcp/localhost:19070 -XX:OnOutOfMemoryError="kill -9 %p" com.yahoo.jdisc.core.BootstrapDaemon file:/home/vespa/lib/jars/container-disc-with-dependencies.jar
+1354022570.876 example.yahoo.com 18151/1 configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi info Installing bundles from the latest application
+1354022570.884 example.yahoo.com 18151/1 configserver Container.com.yahoo.container.handler.BundleLoader info Installing bundle from disk with reference 'file:/home/vespa/lib/jars/config-models/sherpa-config-model-plugin.jar'
+1354022570.889 example.yahoo.com 18151/1 configserver Container.com.yahoo.container.handler.BundleLoader info Installing bundle from disk with reference 'file:/home/vespa/lib/jars/configserver-container-plugin.jar'
+1354022574.285 example.yahoo.com 18151/1 configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi info Installing bundles from the latest application
+1354022577.430 example.yahoo.com 18151/1 configserver Container.com.yahoo.statistics.Counter event count/1 name=requests value=0
+1354022577.431 example.yahoo.com 18151/1 configserver Container.com.yahoo.statistics.Counter event count/1 name=requestsCached value=0
+1354022577.432 example.yahoo.com 18151/1 configserver Container.com.yahoo.statistics.Counter event count/1 name=requestsNotCached value=0
+1354022577.432 example.yahoo.com 18151/1 configserver Container.com.yahoo.statistics.Counter event count/1 name=failedRequests value=0
+1354022577.432 example.yahoo.com 18151/1 configserver Container.com.yahoo.statistics.Counter event count/1 name=procTime value=0
+1354022577.432 example.yahoo.com 18151/1 configserver Container.com.yahoo.statistics.Counter event count/1 name=procTimeCached value=0
+1354022577.432 example.yahoo.com 18151/1 configserver Container.com.yahoo.statistics.Counter event count/1 name=procTimeNotCached value=0
+1354022577.489 example.yahoo.com 18151/1 configserver Container.com.yahoo.container.handler.config.HandlersConfigurerDi info Switched to the latest deployed set of handlers, and dependent components, e.g. search chains, searchers and document processors. Application switch number: 0