├── gradle.properties ├── .gitignore ├── settings.gradle ├── src ├── test │ ├── resources │ │ └── mockito-extensions │ │ │ └── org.mockito.plugins.MockMaker │ └── kotlin │ │ └── io │ │ └── jkratz │ │ └── mediator │ │ ├── mock │ │ ├── CommandSample.kt │ │ ├── EventSample.kt │ │ └── RequestSample.kt │ │ └── spring │ │ ├── SpringRegistryTest.kt │ │ └── SpringMediatorTest.kt └── main │ └── kotlin │ └── io │ └── jkratz │ └── mediator │ ├── core │ ├── Event.kt │ ├── Command.kt │ ├── Request.kt │ ├── MediatorThreadFactory.kt │ ├── exception │ │ └── Exceptions.kt │ ├── Mediator.kt │ └── Registry.kt │ └── spring │ ├── EventHandlerProvider.kt │ ├── CommandHandlerProvider.kt │ ├── RequestHandlerProvider.kt │ ├── SpringMediator.kt │ └── SpringRegistry.kt ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── CONTRIBUTING.md ├── gradlew.bat ├── gradlew ├── README.md └── LICENSE /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.gradle/ 2 | /.idea/ 3 | /build/ 4 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'spring-mediatr' 2 | 3 | -------------------------------------------------------------------------------- /src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkratz55/spring-mediatR/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/test/kotlin/io/jkratz/mediator/mock/CommandSample.kt: -------------------------------------------------------------------------------- 1 | package io.jkratz.mediator.mock 2 | 3 | import io.jkratz.mediator.core.Command 4 | import io.jkratz.mediator.core.CommandHandler 5 | 6 | class SayHelloCommand(val message: String): Command 7 | 8 | class SayHelloCommandHandler: CommandHandler { 9 | 10 | override fun handle(command: SayHelloCommand) { 11 | println(command.message) 12 | } 13 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/jkratz/mediator/mock/EventSample.kt: -------------------------------------------------------------------------------- 1 | package io.jkratz.mediator.mock 2 | 3 | import io.jkratz.mediator.core.Event 4 | import io.jkratz.mediator.core.EventHandler 5 | 6 | data class OrderCreatedEvent(val orderId: Int): Event 7 | 8 | class OrderCreationListener: EventHandler { 9 | 10 | override fun handle(event: OrderCreatedEvent) { 11 | println("Order created with ID: ${event.orderId}") 12 | } 13 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/jkratz/mediator/mock/RequestSample.kt: -------------------------------------------------------------------------------- 1 | package io.jkratz.mediator.mock 2 | 3 | import io.jkratz.mediator.core.Request 4 | import io.jkratz.mediator.core.RequestHandler 5 | import java.math.BigDecimal 6 | 7 | data class CalculateOrderTotalRequest(val price: BigDecimal, 8 | val qty: BigDecimal): Request 9 | 10 | class CalculateOrderTotalRequestHandler: RequestHandler { 11 | 12 | override fun handle(request: CalculateOrderTotalRequest): BigDecimal { 13 | return request.price.multiply(request.qty) 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/main/kotlin/io/jkratz/mediator/core/Event.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.jkratz.mediator.core 18 | 19 | /** 20 | * Marker interface for an event 21 | * 22 | * @author Joseph Kratz 23 | * @since 1.0 24 | */ 25 | interface Event 26 | 27 | /** 28 | * Handler for a specific event 29 | * 30 | * @author Joseph Kratz 31 | * @since 1.0 32 | */ 33 | interface EventHandler where TEvent: Event { 34 | 35 | fun handle(event: TEvent) 36 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/jkratz/mediator/core/Command.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.jkratz.mediator.core 18 | 19 | /** 20 | * Marker interface for a Command 21 | * 22 | * @author Joseph Kratz 23 | * @since 1.0 24 | */ 25 | interface Command 26 | 27 | /** 28 | * A handler for a given Command 29 | * 30 | * @author Joseph Kratz 31 | * @since 1.0 32 | */ 33 | interface CommandHandler where TCommand: Command { 34 | 35 | fun handle(command: TCommand) 36 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/jkratz/mediator/spring/EventHandlerProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.jkratz.mediator.spring 18 | 19 | import io.jkratz.mediator.core.EventHandler 20 | import org.springframework.context.ApplicationContext 21 | import kotlin.reflect.KClass 22 | 23 | /** 24 | * A wrapper around an EventHandler 25 | * 26 | * @author Joseph Kratz 27 | * @since 1.0 28 | * @property applicationContext ApplicationContext from Spring used to retrieve beans 29 | * @property type Type of EventHandler 30 | */ 31 | internal class EventHandlerProvider( 32 | private val applicationContext: ApplicationContext, 33 | private val type: KClass 34 | ) where T: EventHandler<*> { 35 | 36 | internal val handler: T by lazy { 37 | applicationContext.getBean(type.java) 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/jkratz/mediator/spring/CommandHandlerProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.jkratz.mediator.spring 18 | 19 | import io.jkratz.mediator.core.CommandHandler 20 | import org.springframework.context.ApplicationContext 21 | import kotlin.reflect.KClass 22 | 23 | /** 24 | * A wrapper around a CommandHandler 25 | * 26 | * @author Joseph Kratz 27 | * @since 1.0 28 | * @property applicationContext ApplicationContext from Spring used to retrieve beans 29 | * @property type Type of CommandHandler 30 | */ 31 | internal class CommandHandlerProvider ( 32 | private val applicationContext: ApplicationContext, 33 | private val type: KClass 34 | ) where T: CommandHandler<*> { 35 | 36 | internal val handler: T by lazy { 37 | applicationContext.getBean(type.java) 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/jkratz/mediator/spring/RequestHandlerProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.jkratz.mediator.spring 18 | 19 | import io.jkratz.mediator.core.RequestHandler 20 | import org.springframework.context.ApplicationContext 21 | import kotlin.reflect.KClass 22 | 23 | /** 24 | * A wrapper around a RequestHandler 25 | * 26 | * @author Joseph Kratz 27 | * @since 1.0 28 | * @property applicationContext ApplicationContext from Spring used to retrieve beans 29 | * @property type Type of RequestHandler 30 | */ 31 | internal class RequestHandlerProvider ( 32 | private val applicationContext: ApplicationContext, 33 | private val type: KClass 34 | ) where T: RequestHandler<*, *> { 35 | 36 | internal val handler: T by lazy { 37 | applicationContext.getBean(type.java) 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/jkratz/mediator/core/Request.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.jkratz.mediator.core 18 | 19 | /** 20 | * Marker interface for a request 21 | * 22 | * @author Joseph Kratz 23 | * @since 1.0 24 | * @param type of the return value 25 | */ 26 | interface Request 27 | 28 | /** 29 | * A handler for a request 30 | * 31 | * @author Joseph Kratz 32 | * @since 1.0 33 | * @param the type of TRequest to be handled 34 | * @param the type of the response 35 | */ 36 | interface RequestHandler where TRequest: Request { 37 | 38 | /** 39 | * Handles the request 40 | * 41 | * @param request request to handle 42 | * @return the response of the request 43 | */ 44 | fun handle(request: TRequest): TResponse 45 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/jkratz/mediator/core/MediatorThreadFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.jkratz.mediator.core 18 | 19 | import java.util.concurrent.Executor 20 | import java.util.concurrent.ThreadFactory 21 | 22 | /** 23 | * Custom implementation of [ThreadFactory] to provide custom naming 24 | * of the threads used by the default [Executor] for asynchronous processing. 25 | * 26 | * @author Joseph Kratz 27 | * @since 1.0 28 | */ 29 | class MediatorThreadFactory: ThreadFactory { 30 | 31 | private var counter: Int = 0 32 | 33 | /** 34 | * Creates threads with the naming scheme Mediator-X where X is the 35 | * thread number 36 | * 37 | * Example: Mediator-1 38 | */ 39 | override fun newThread(r: Runnable?): Thread { 40 | counter++ 41 | return Thread(r, "Mediator-$counter") 42 | } 43 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Spring MediatR 2 | 3 | ## How Can I Contribute? 4 | 5 | ### Reporting Bugs 6 | 7 | If you believe you've found a bug with Spring MediatR please feel free to open an issue. Please 8 | use the following guidelines when opening issues. 9 | 10 | * Use a clear and descriptive title 11 | * Describe the exact steps which reproduce the problem in as many details as possible. When listing steps, 12 | don't just say what you did, but explain how you did it. If possible to share the code please do as 13 | that can help significantly in debugging. 14 | * Describe the behavior you observed after following the steps and point out what exactly is the problem with that behavior. 15 | * Explain which behavior you expected to see instead and why. 16 | * Apply the `type:bug` tag to the issue 17 | * List which version of Spring and Java you are using 18 | 19 | ### Suggest Enhancement 20 | 21 | If you have suggestions on enhancements to Spring MediatR feel free to open an issue with the tag 22 | `type:enhancement`. 23 | 24 | * Use a clear and descriptive title for the issue to identify the suggestion. 25 | * Provide a step-by-step description of the suggested enhancement in as many details as possible. 26 | * Describe the current behavior and explain which behavior you expected to see instead and why. 27 | * Explain why this enhancement would be useful to most users of Spring MediatR. 28 | 29 | ### Submit Pull Request 30 | 31 | Before submitting a Pull Request please open an issue first and refer to the issues being addressed 32 | in the Pull Request. 33 | 34 | *While this library is written in Kotlin please keep in mind most Spring code bases are still Java. 35 | Any features added need to be Java friendly and take Java interoperability into consideration.* -------------------------------------------------------------------------------- /src/main/kotlin/io/jkratz/mediator/core/exception/Exceptions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.jkratz.mediator.core.exception 18 | 19 | import io.jkratz.mediator.core.* 20 | 21 | /** 22 | * Exception thrown when there is not a [RequestHandler] available for a [Request] 23 | * 24 | * @author Joseph Kratz 25 | * @since 1.0 26 | */ 27 | class NoRequestHandlerException(message: String?): RuntimeException(message) 28 | 29 | /** 30 | * Exception thrown when there is an attempt to register a [RequestHandler] for a 31 | * [Request] that already has a [RequestHandler] registered. 32 | * 33 | * @author Joseph Kratz 34 | * @since 1.0 35 | */ 36 | class DuplicateRequestHandlerRegistrationException(message: String?): RuntimeException(message) 37 | 38 | /** 39 | * Exception thrown when there are no [EventHandler]s available for an [Event] 40 | * 41 | * @author Joseph Kratz 42 | * @since 1.0 43 | */ 44 | class NoEventHandlersException(message: String?): RuntimeException(message) 45 | 46 | /** 47 | * Exception thrown when there is not a [CommandHandler] available for a [Command] 48 | * 49 | * @author Joseph Kratz 50 | * @since 1.0 51 | */ 52 | class NoCommandHandlerException(message: String?): RuntimeException(message) 53 | 54 | /** 55 | * Exception thrown when there is an attempt to register a [CommandHandler] for a 56 | * [Command] that already has a [CommandHandler] registered. 57 | * 58 | * @author Joseph Kratz 59 | * @since 1.0 60 | */ 61 | class DuplicateCommandHandlerRegistrationException(message: String?): RuntimeException(message) -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/kotlin/io/jkratz/mediator/core/Mediator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.jkratz.mediator.core 18 | 19 | import java.util.concurrent.CompletableFuture 20 | 21 | /** 22 | * Defines a mediator to encapsulate dispatching and publishing interaction patterns. 23 | * 24 | * 25 | * @author Joseph Kratz 26 | * @since 1.0 27 | */ 28 | interface Mediator { 29 | 30 | /** 31 | * Dispatches a [Request] to a single [RequestHandler] synchronously 32 | * 33 | * @param request The request to be executed 34 | * @return 35 | */ 36 | fun , TResponse> dispatch(request: TRequest): TResponse 37 | 38 | /** 39 | * Dispatches a [Request] to a single [RequestHandler] to execute asynchronously on another thread 40 | * 41 | * @param request The request to be executed 42 | * @return 43 | */ 44 | fun , TResponse> dispatchAsync(request: TRequest): CompletableFuture 45 | 46 | 47 | /** 48 | * Sends the event to all registered [EventHandler]s for the particular event synchronously. 49 | * 50 | * @param event The event to send 51 | */ 52 | fun emit(event: Event) 53 | 54 | /** 55 | * Sends the event to all registered [EventHandler]s for the particular event asynchronously on another thread 56 | * 57 | * @param event The event to send 58 | * @return 59 | */ 60 | fun emitAsync(event: Event): CompletableFuture 61 | 62 | /** 63 | * Dispatches a [Command] to a single [CommandHandler] synchronously 64 | * 65 | * @param command Command to dispatch for execution 66 | */ 67 | fun dispatch(command: Command) 68 | 69 | /** 70 | * Dispatches a [Command] to a single [CommandHandler] to execute asynchronously on another thread 71 | */ 72 | fun dispatchAsync(command: Command): CompletableFuture 73 | 74 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/jkratz/mediator/core/Registry.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.jkratz.mediator.core 18 | 19 | import io.jkratz.mediator.core.exception.NoCommandHandlerException 20 | import io.jkratz.mediator.core.exception.NoEventHandlersException 21 | import io.jkratz.mediator.core.exception.NoRequestHandlerException 22 | 23 | /** 24 | * A registry for handlers for messages that can be dispatched in Spring MediatR 25 | * 26 | * @author Joseph Kratz 27 | * @since 1.1 28 | */ 29 | interface Registry { 30 | 31 | /** 32 | * Retrieves the RequestHandler for the provided type. If not RequestHandler is 33 | * registered to handle the type provided [NoRequestHandlerException] will be thrown 34 | * 35 | * @param requestClass The type of the request 36 | * @return The RequestHandler for the request 37 | * @throws NoRequestHandlerException When there is not a RequestHandler available for the request 38 | */ 39 | fun ,R> get(requestClass: Class): RequestHandler 40 | 41 | /** 42 | * Retrieves all the EventHandlers for the provided event type. If no EventHandlers are 43 | * registered to handle the type provided [NoEventHandlersException] will be thrown. 44 | * 45 | * @param eventClass The type of the event 46 | * @return Set of EventHandlers for the eventClass 47 | * @throws NoEventHandlersException When there are no EventHandlers available 48 | */ 49 | fun get(eventClass: Class): Set> 50 | 51 | /** 52 | * Retrieves a CommandHandler for the provided type. If no CommandHandler 53 | * is registered for the Command type [NoCommandHandlerException] will be thrown. 54 | * 55 | * @param commandClass The type of the command 56 | * @return The CommandHandler for the command 57 | * @throws NoCommandHandlerException When there isn't a CommandHandler available 58 | */ 59 | fun get(commandClass: Class): CommandHandler 60 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/jkratz/mediator/spring/SpringRegistryTest.kt: -------------------------------------------------------------------------------- 1 | package io.jkratz.mediator.spring 2 | 3 | import io.jkratz.mediator.core.CommandHandler 4 | import io.jkratz.mediator.core.EventHandler 5 | import io.jkratz.mediator.core.Registry 6 | import io.jkratz.mediator.core.RequestHandler 7 | import io.jkratz.mediator.mock.* 8 | import org.junit.Assert.assertTrue 9 | import org.junit.Before 10 | import org.junit.Test 11 | import org.junit.runner.RunWith 12 | import org.mockito.Mock 13 | import org.mockito.Mockito 14 | import org.mockito.junit.MockitoJUnitRunner 15 | import org.springframework.context.ApplicationContext 16 | import kotlin.test.assertEquals 17 | 18 | @RunWith(MockitoJUnitRunner::class) 19 | class SpringRegistryTest { 20 | 21 | @Mock 22 | private lateinit var applicationContext: ApplicationContext 23 | 24 | @Mock 25 | private lateinit var commandHandler: SayHelloCommandHandler 26 | 27 | @Mock 28 | private lateinit var requestHandler: CalculateOrderTotalRequestHandler 29 | 30 | @Mock 31 | private lateinit var eventHandler: OrderCreationListener 32 | 33 | private lateinit var registry: Registry 34 | 35 | @Before 36 | fun setup() { 37 | 38 | Mockito.`when`(applicationContext.getBeanNamesForType(CommandHandler::class.java)) 39 | .thenReturn(arrayOf("sayHelloCommandHandler")) 40 | 41 | Mockito.`when`(applicationContext.getBeanNamesForType(RequestHandler::class.java)) 42 | .thenReturn(arrayOf("calculateOrderTotalRequestHandler")) 43 | 44 | Mockito.`when`(applicationContext.getBeanNamesForType(EventHandler::class.java)) 45 | .thenReturn(arrayOf("orderCreationListener")) 46 | 47 | Mockito.`when`(applicationContext.getBean("calculateOrderTotalRequestHandler")) 48 | .thenReturn(requestHandler) 49 | 50 | Mockito.`when`(applicationContext.getBean("orderCreationListener")) 51 | .thenReturn(eventHandler) 52 | 53 | Mockito.`when`(applicationContext.getBean("sayHelloCommandHandler")) 54 | .thenReturn(commandHandler) 55 | 56 | Mockito.`when`(applicationContext.getBean(SayHelloCommandHandler::class.java)) 57 | .thenReturn(commandHandler) 58 | 59 | Mockito.`when`(applicationContext.getBean(CalculateOrderTotalRequestHandler::class.java)) 60 | .thenReturn(requestHandler) 61 | 62 | Mockito.`when`(applicationContext.getBean(OrderCreationListener::class.java)) 63 | .thenReturn(eventHandler) 64 | 65 | this.registry = SpringRegistry(applicationContext) 66 | } 67 | 68 | @Test 69 | fun testGetCommandHandler() { 70 | val handlers = this.registry.get(OrderCreatedEvent::class.java) 71 | assertTrue(handlers.isNotEmpty()) 72 | assertTrue(handlers.contains(this.eventHandler)) 73 | } 74 | 75 | @Test 76 | fun testGetEventHandler() { 77 | val handler = this.registry.get(SayHelloCommand::class.java) 78 | assertEquals(this.commandHandler, handler) 79 | } 80 | 81 | @Test 82 | fun testGetRequestHandler() { 83 | val handler = this.registry.get(CalculateOrderTotalRequest::class.java) 84 | assertEquals(this.requestHandler, handler) 85 | } 86 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/jkratz/mediator/spring/SpringMediatorTest.kt: -------------------------------------------------------------------------------- 1 | package io.jkratz.mediator.spring 2 | 3 | import io.jkratz.mediator.core.CommandHandler 4 | import io.jkratz.mediator.core.EventHandler 5 | import io.jkratz.mediator.core.Registry 6 | import io.jkratz.mediator.core.RequestHandler 7 | import io.jkratz.mediator.mock.* 8 | import org.junit.Before 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | import org.mockito.Mock 12 | import org.mockito.Mockito 13 | import org.mockito.Mockito.spy 14 | import org.mockito.junit.MockitoJUnitRunner 15 | import org.springframework.context.ApplicationContext 16 | import java.math.BigDecimal 17 | 18 | //@RunWith(MockitoJUnitRunner::class) 19 | //class SpringMediatorTest { 20 | // 21 | // @Mock 22 | // private lateinit var registry: Registry 23 | // 24 | // @Mock 25 | // private lateinit var commandHandler: SayHelloCommandHandler 26 | // 27 | // @Mock 28 | // private lateinit var requestHandler: CalculateOrderTotalRequestHandler 29 | // 30 | // @Mock 31 | // private lateinit var eventHandler: OrderCreationListener 32 | // 33 | // private lateinit var mediator: SpringMediator 34 | // 35 | // @Before 36 | // fun setup() { 37 | // 38 | // Mockito.`when`(registry.get(SayHelloCommand::class.java)) 39 | // .thenReturn(commandHandler) 40 | // 41 | // Mockito.`when`(registry.get(OrderCreatedEvent::class.java)) 42 | // .thenReturn(setOf(eventHandler)) 43 | // 44 | // Mockito.`when`(registry.get(CalculateOrderTotalRequest::class.java)) 45 | // .thenReturn(requestHandler) 46 | // 47 | // mediator = spy(SpringMediator(registry)) 48 | // } 49 | // 50 | // @Test 51 | // fun testCommandDispatch() { 52 | // val command = SayHelloCommand("Hello Friends!") 53 | // mediator.dispatch(command) 54 | // Mockito.verify(commandHandler).handle(command) 55 | // } 56 | // 57 | // @Test 58 | // fun testCommandDispatchAsync() { 59 | // val command = SayHelloCommand("Hello Friends!") 60 | // mediator.dispatchAsync(command) 61 | // .exceptionally { throw RuntimeException() } 62 | // .thenAccept { Mockito.verify(commandHandler).handle(command) } 63 | // } 64 | // 65 | // @Test 66 | // fun testRequestDispatch() { 67 | // val request = CalculateOrderTotalRequest(BigDecimal(15.99), BigDecimal.TEN) 68 | // mediator.dispatch(request) 69 | // Mockito.verify(requestHandler).handle(request) 70 | // } 71 | // 72 | // @Test 73 | // fun testRequestDispatchAsync() { 74 | // val request = CalculateOrderTotalRequest(BigDecimal(15.99), BigDecimal.TEN) 75 | // mediator.dispatchAsync(request) 76 | // .exceptionally { throw RuntimeException() } 77 | // .thenAccept { Mockito.verify(requestHandler).handle(request) } 78 | // } 79 | // 80 | // @Test 81 | // fun testEventEmit() { 82 | // val event = OrderCreatedEvent(55) 83 | // mediator.emit(event) 84 | // Mockito.verify(eventHandler).handle(event) 85 | // } 86 | // 87 | // @Test 88 | // fun testEventEmitAysnc() { 89 | // val event = OrderCreatedEvent(55) 90 | // mediator.emitAsync(event) 91 | // .thenAccept { Mockito.verify(eventHandler).handle(event) } 92 | // } 93 | //} -------------------------------------------------------------------------------- /src/main/kotlin/io/jkratz/mediator/spring/SpringMediator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.jkratz.mediator.spring 18 | 19 | import io.jkratz.mediator.core.* 20 | import org.slf4j.Logger 21 | import org.slf4j.LoggerFactory 22 | import org.springframework.context.ApplicationContext 23 | import java.util.concurrent.CompletableFuture 24 | import java.util.concurrent.Executor 25 | import java.util.concurrent.Executors 26 | import javax.validation.Valid 27 | 28 | /** 29 | * Implementation of Mediator that is specific for the Spring Framework. This class requires it be 30 | * instantiated with the [ApplicationContext] containing the beans for all the handlers. The 31 | * [ApplicationContext] is used to retrieve all the beans that implement [CommandHandler], 32 | * [EventHandler], and [RequestHandler]. Optionally this class can be instantiated with a 33 | * [Executor]. If one if not provided a FixedThreadPool will be used with a thread count 34 | * equal to the count of processor available. The [Executor] is only used on the async variants 35 | * of the dispatch and emit events. 36 | * 37 | * @author Joseph Kratz 38 | * @since 1.0 39 | * @constructor Creates the Spring specific implementation of MediatR using the default [Executor] 40 | * which is a fixed thread pool with the amount of threads equal to the number of 41 | * processors available. 42 | * @param applicationContext Spring application context containing the beans for MediatR 43 | */ 44 | class SpringMediator constructor(private val registry: Registry): Mediator { 45 | 46 | private var executor: Executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), MediatorThreadFactory()) 47 | 48 | /** 49 | * Creates the Spring specific implementation of MediatR with a custom [Executor] for 50 | * performing async operations. 51 | * 52 | * @param applicationContext Spring application context containing the beans for MediatR 53 | * @param executor The executor to execute asynchronous operations 54 | */ 55 | constructor(registry: Registry, executor: Executor) : this(registry) { 56 | this.executor = executor 57 | logger.info("${executor::class.java.simpleName} will be used for asynchronous operations instead of the default Executor") 58 | } 59 | 60 | override fun , TResponse> dispatch(@Valid request: TRequest): TResponse { 61 | val handler = registry.get(request::class.java) 62 | logger.debug("Dispatching ${request::class.simpleName} to handler ${handler::class.simpleName}") 63 | return handler.handle(request) 64 | } 65 | 66 | override fun , TResponse> dispatchAsync(@Valid request: TRequest): CompletableFuture { 67 | return CompletableFuture.supplyAsync({ 68 | val handler = registry.get(request::class.java) 69 | logger.debug("Dispatching ${request::class.simpleName} to handler ${handler::class.simpleName}") 70 | handler.handle(request) 71 | }, executor::execute) 72 | } 73 | 74 | override fun emit(@Valid event: Event) { 75 | val eventHandlers = registry.get(event::class.java) 76 | eventHandlers.forEach { handler -> 77 | logger.debug("Dispatching ${event::class.simpleName} to handler ${handler::class.simpleName}") 78 | handler.handle(event) 79 | } 80 | } 81 | 82 | override fun emitAsync(@Valid event: Event): CompletableFuture { 83 | return CompletableFuture.runAsync({ 84 | val eventHandlers = registry.get(event::class.java) 85 | eventHandlers.forEach { handler -> 86 | logger.debug("Dispatching ${event::class.simpleName} to handler ${handler::class.simpleName}") 87 | handler.handle(event) } 88 | }, executor::execute) 89 | } 90 | 91 | override fun dispatch(@Valid command: Command) { 92 | val handler = registry.get(command::class.java) 93 | logger.debug("Dispatching ${command::class.simpleName} to handler ${handler::class.simpleName}") 94 | handler.handle(command) 95 | } 96 | 97 | override fun dispatchAsync(@Valid command: Command): CompletableFuture { 98 | return CompletableFuture.runAsync({ 99 | val handler = registry.get(command::class.java) 100 | logger.debug("Dispatching ${command::class.simpleName} to handler ${handler::class.simpleName}") 101 | handler.handle(command) 102 | }, executor::execute) 103 | } 104 | 105 | companion object { 106 | val logger: Logger = LoggerFactory.getLogger(SpringMediator::class.java) 107 | } 108 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/main/kotlin/io/jkratz/mediator/spring/SpringRegistry.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.jkratz.mediator.spring 18 | 19 | import io.jkratz.mediator.core.* 20 | import io.jkratz.mediator.core.exception.* 21 | import org.slf4j.Logger 22 | import org.slf4j.LoggerFactory 23 | import org.springframework.context.ApplicationContext 24 | import org.springframework.core.GenericTypeResolver 25 | import kotlin.collections.HashMap 26 | 27 | /** 28 | * This class is responsible for registering beans from the Spring ApplicationContext 29 | * that implements the [RequestHandler] interface or [EventHandler] interface to the specific 30 | * [Event] or [Request] they are to handle. This class is also used to retrieve the handler 31 | * based on the type of the event or request. 32 | * 33 | * In order to address potential issues with circular dependencies the handlers are lazy registered. 34 | * The registration takes place once on the first lookup of a handler. 35 | * 36 | * @author Joseph Kratz 37 | * @since 1.0 38 | * @property applicationContext Context from the Spring container 39 | */ 40 | class SpringRegistry(private val applicationContext: ApplicationContext): Registry { 41 | 42 | private val requestRegistry: MutableMap>, RequestHandlerProvider<*>> = HashMap() 43 | private val eventRegistry: MutableMap, MutableSet>> = HashMap() 44 | private val commandRegistry: MutableMap, CommandHandlerProvider<*>> = HashMap() 45 | private var initialized: Boolean = false 46 | 47 | override fun ,R> get(requestClass: Class): RequestHandler { 48 | if (!initialized) { 49 | initializeHandlers() 50 | } 51 | requestRegistry[requestClass]?.let { provider -> 52 | return provider.handler as RequestHandler 53 | } ?: throw NoRequestHandlerException("No RequestHandler is registered to handle request of type ${requestClass.canonicalName}") 54 | } 55 | 56 | override fun get(eventClass: Class): Set> { 57 | if (!initialized) { 58 | initializeHandlers() 59 | } 60 | val handlers = mutableSetOf>() 61 | eventRegistry[eventClass]?.let { providers -> 62 | for (provider in providers) { 63 | val handler = provider.handler as EventHandler 64 | handlers.add(handler) 65 | } 66 | } ?: throw NoEventHandlersException("No EventHandlers are registered to handle event of type ${eventClass.canonicalName}") 67 | return handlers 68 | } 69 | 70 | override fun get(commandClass: Class): CommandHandler { 71 | if (!initialized) { 72 | initializeHandlers() 73 | } 74 | commandRegistry[commandClass]?.let { provider -> 75 | return provider.handler as CommandHandler 76 | } ?: throw NoCommandHandlerException("No CommandHandler is registered to handle request of type ${commandClass.canonicalName}") 77 | } 78 | 79 | /** 80 | * Initializes all the handlers from the ApplicationContext 81 | */ 82 | private fun initializeHandlers() { 83 | synchronized(this) { 84 | if (!initialized) { 85 | applicationContext.getBeanNamesForType(RequestHandler::class.java) 86 | .forEach { registerRequestHandler(it) } 87 | applicationContext.getBeanNamesForType(EventHandler::class.java) 88 | .forEach { registerEventHandler(it) } 89 | applicationContext.getBeanNamesForType(CommandHandler::class.java) 90 | .forEach { registerCommandHandler(it) } 91 | initialized = true 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * Registers a RequestHandler from the Spring context by name 98 | * 99 | * @param name Name of the bean to register as a CommandHandler 100 | */ 101 | private fun registerRequestHandler(name: String) { 102 | logger.debug("Registering RequestHandler with name $name") 103 | val handler: RequestHandler<*,*> = applicationContext.getBean(name) as RequestHandler<*,*> 104 | val generics = GenericTypeResolver.resolveTypeArguments(handler::class.java, RequestHandler::class.java) 105 | generics?.let { 106 | val requestType = it[0] as Class> 107 | if (requestRegistry.contains(requestType)) { 108 | throw DuplicateRequestHandlerRegistrationException("${requestType.canonicalName} already has a registered handler. Each request must have a single request handler") 109 | } 110 | val requestProvider = RequestHandlerProvider(applicationContext, handler::class) 111 | requestRegistry[requestType] = requestProvider 112 | logger.info("Registered RequestHandler ${handler::class.simpleName} to handle Request ${requestType.simpleName}") 113 | } 114 | } 115 | 116 | /** 117 | * Registers an EventHandler from the Spring context by name 118 | * 119 | * @param name Name of the bean to register as an EventHandler 120 | */ 121 | private fun registerEventHandler(name: String) { 122 | logger.debug("Registering EventHandler with name $name") 123 | val eventHandler: EventHandler<*> = applicationContext.getBean(name) as EventHandler<*> 124 | val generics = GenericTypeResolver.resolveTypeArguments(eventHandler::class.java, EventHandler::class.java) 125 | generics?.let { 126 | val eventType = it[0] as Class 127 | val eventProvider = EventHandlerProvider(applicationContext, eventHandler::class) 128 | eventRegistry[eventType]?.add(eventProvider) ?: kotlin.run { 129 | eventRegistry[eventType] = mutableSetOf(eventProvider) 130 | } 131 | logger.info("Register EventHandler ${eventHandler::class.simpleName} to handle Event ${eventType.simpleName}") 132 | } 133 | } 134 | 135 | /** 136 | * Registers a CommandHandler from the Spring context by name 137 | * 138 | * @param name Name of the bean to register as a CommandHandler 139 | */ 140 | private fun registerCommandHandler(name: String) { 141 | logger.debug("Registering CommandHandler with name $name") 142 | val commandHandler: CommandHandler<*> = applicationContext.getBean(name) as CommandHandler<*> 143 | val generics = GenericTypeResolver.resolveTypeArguments(commandHandler::class.java, CommandHandler::class.java) 144 | generics?.let { 145 | val commandType = it[0] as Class 146 | if (commandRegistry.containsKey(commandType)) { 147 | throw DuplicateCommandHandlerRegistrationException("${commandType.canonicalName} already has a registered handler. Each command must have a single command handler") 148 | } 149 | val commandHandlerProvider = CommandHandlerProvider(applicationContext, commandHandler::class) 150 | commandRegistry[commandType] = commandHandlerProvider 151 | logger.info("Registered CommandHandler ${commandHandler::class.simpleName} to handle Command ${commandType.simpleName}") 152 | } 153 | } 154 | 155 | companion object { 156 | val logger: Logger = LoggerFactory.getLogger(SpringRegistry::class.java) 157 | } 158 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring MediatR 2 | 3 | An implementation of [MediatR](https://github.com/jbogard/MediatR) on the JVM for the Spring Framework. This project is heavily inspired by the [MediatR](https://github.com/jbogard/MediatR) project for .NET by Jimmy Bogard. Many of the concepts from [MediatR](https://github.com/jbogard/MediatR) have been adapted in this project for the JVM and Spring Framework. 4 | 5 | Spring MediatR is a simple library intended to help developers write cleaner more focused code and decouple components. This library can also be used to implement some of the ideas and concepts from CQRS. 6 | 7 | ## Requirements 8 | 9 | * Java 8 + 10 | * Spring Framework 5 / Spring Boot 2* 11 | 12 | *While Spring MediatR may work on older versions of Spring I have not verified nor tested it with previous versions.* 13 | 14 | ## Getting Started 15 | 16 | The published artifacts for Spring MediatR are hosted on Bintray. In order to use Spring MediatR you will need to add jcenter repository. 17 | 18 | ### Gradle 19 | 20 | Add jcenter to the repositories 21 | 22 | 23 | ```groovy 24 | repositories { 25 | jcenter() 26 | mavenCentral() 27 | } 28 | ``` 29 | 30 | Add Spring MediatR dependency 31 | 32 | ```groovy 33 | dependencies { 34 | 35 | // Other dependencies go here 36 | 37 | implementation 'io.jkratz.springmediatr:spring-mediatr:1.1-RELEASE' 38 | } 39 | 40 | ``` 41 | 42 | ### Maven 43 | 44 | ```xml 45 | 46 | 47 | jcenter 48 | https://jcenter.bintray.com/ 49 | 50 | 51 | ``` 52 | 53 | ```xml 54 | 55 | io.jkratz.springmediatr 56 | spring-mediatr 57 | 1.1-RELEASE 58 | pom 59 | 60 | ``` 61 | 62 | ### Setup 63 | 64 | Declare the Mediator and Registry Beans 65 | 66 | ```java 67 | @SpringBootApplication 68 | public class SpringMediatrJavaSampleApplication { 69 | 70 | private final ApplicationContext applicationContext; 71 | 72 | @Autowired 73 | public SpringMediatrJavaSampleApplication(ApplicationContext applicationContext) { 74 | this.applicationContext = applicationContext; 75 | } 76 | 77 | @Bean 78 | public Registry registry() { 79 | return new SpringRegistry(applicationContext); 80 | } 81 | 82 | @Bean 83 | public Mediator mediator(Registry registry) { 84 | return new SpringMediator(registry); 85 | } 86 | 87 | public static void main(String[] args) { 88 | SpringApplication.run(SpringMediatrJavaSampleApplication.class, args); 89 | } 90 | } 91 | 92 | ``` 93 | 94 | ### Samples 95 | 96 | The below link is a sample project that demonstrates the basic concepts and use of Spring MediatR. 97 | 98 | [Java Sample](https://github.com/jkratz55/spring-mediatr-java-sample) 99 | 100 | 101 | ## Basics 102 | 103 | Spring MediatR has three kinds of messages it dispatches: 104 | 105 | * Request - Dispatches to a single handler, has return value 106 | * Command - Dispatches to a single handler, does not provide a mechanism to return value 107 | * Event - Dispatches to one or many handlers, does not provide a mechanism to return value 108 | 109 | MediatR supports both synchronous and asynchronous dispatching. Asynchronous dispatching methods returns a CompletableFuture. The asynchronous dispatching is ideal for long running operations where you don't want to hold the container thread hostage until the operation is completed. If the operation is fast it may be more ideal to use the synchronous variants unless you have a particular reason not to. 110 | 111 | #### A Word About Spring @Transactional and Async Dispatching/Emitting 112 | 113 | Spring does a fantastic job of providing APIs and annotations to make handling transactions easy. However, out of the box @Tranasctional does not work across multiple threads. If we start a transaction from one thread and try to commit or rollback the transaction on another thread, a runtime error will be generated complaining that the Spring transaction is not active on the current thread. Though we start and end the transaction from the same thread, we cannot perform database operations belong to transaction from another thread either. 114 | 115 | There are ways to work around this limitation, but the following is not exhaustive. 116 | 117 | 1. Manage the transaction manually. This can be done by injecting the TransationManager 118 | 2. Mark the handle method in the [X]Hander class as @Transactional and ensure all transactional work is done within that method on the same thread. 119 | 120 | ### Request and RequestHandler 121 | 122 | Requests and RequestHandlers can be used for both queries and commands. There are also Commands and CommandHandlers because of the limitations of the type system on the JVM due to type erasure. Generally if you need to return a value, use a Request, otherwise use a Command. 123 | 124 | As an example a consumer might call your rest API requesting a user be created. If the created user needs to be immediately available to the consumer you may need to return the ID of the user that was just created. This can be common when the API is expected to return a HTTP 201 status with the new resource link in the location header. In that case you would need to use a Request. 125 | 126 | ```java 127 | public class CreateUserRequest implements Request { 128 | 129 | private final String email; 130 | private final String password; 131 | 132 | public CreateUserRequest(String email, String password) { 133 | this.email = email; 134 | this.password = password; 135 | } 136 | 137 | // getter methods omitted 138 | } 139 | ``` 140 | 141 | ```java 142 | public class CreateUserRequestHandler implements RequestHandler { 143 | 144 | public Integer handle(CreateUserRequest request) { 145 | // do logic to create user and then return the ID 146 | return 42; 147 | } 148 | } 149 | ``` 150 | 151 | On the other hand if you don't need to return a value you can use a command. See below. 152 | 153 | ### Command and CommandHandler 154 | 155 | Commands and CommandHandlers are very similar to Request and RequestHandlers. The difference being that Commands do not return a value. This makes the API a little more convenient for cases where you don't want a return value. With the Java/Kotlin 156 | type system and type erasure it is not possible to have the same interface with and without generics. Commands and CommandHandlers also resonate closer to CQRS and Event Sourcing concepts. 157 | 158 | ### Event and EventHandler 159 | 160 | Events work similar to Commands except an Event can be handled by multiple event handlers. Events provide a mechanism to notify components in the application that something has happen. As an example a command may come into the system to create a User which upon successfully creating the user it emits a UserCreatedEvent, and any component interested in that event can be notified. 161 | 162 | ## Using MediatR 163 | 164 | ### Building Messages and Handlers 165 | 166 | Messages and Handlers are the core of Spring MediatR. Messages carry the intent and data, while handlers as you might have guessed, handle the messages. Handlers can be thought of as a replacement for service classes, and because they are managed by Spring you can inject your dependencies like any other Spring managed class. 167 | 168 | *Important: Always remember to annotation your handler classes with @Component. If the class is not managed by Spring it will fail to be registered and likely result in an exception at runtime.* 169 | 170 | The following is an example of a command message and command handler. Requests and events are quite similar, just a use of different interfaces. If you need more details please refer to the sample applications. 171 | 172 | [Java Sample](https://github.com/jkratz55/spring-mediatr-java-sample) 173 | 174 | #### Command & CommandHandler 175 | 176 | ```java 177 | public class CreateUserCommand implements Command { 178 | 179 | @NotBlank 180 | private final String userName; 181 | 182 | @NotBlank 183 | private final String email; 184 | 185 | @NotBlank 186 | private final String password; 187 | 188 | public CreateUserCommand(String userName, String email, String password) { 189 | this.userName = userName; 190 | this.email = email; 191 | this.password = password; 192 | } 193 | 194 | public String getUserName() { 195 | return userName; 196 | } 197 | 198 | public String getEmail() { 199 | return email; 200 | } 201 | 202 | public String getPassword() { 203 | return password; 204 | } 205 | } 206 | ``` 207 | 208 | ```java 209 | @Component 210 | public class CreateUserCommandHandler implements CommandHandler { 211 | 212 | private final UserRepository userRepository; 213 | 214 | @Autowired 215 | private Mediator mediator; 216 | 217 | public CreateUserCommandHandler(UserRepository userRepository) { 218 | this.userRepository = userRepository; 219 | } 220 | 221 | @Override 222 | public void handle(@NotNull CreateUserCommand createUserCommand) { 223 | User user = new User(createUserCommand.getUserName(), 224 | createUserCommand.getEmail(), 225 | createUserCommand.getPassword()); 226 | this.userRepository.save(user); 227 | this.mediator.emit(new UserCreatedEvent(user.getId())); 228 | } 229 | } 230 | ``` 231 | 232 | ### Dispatching and Emitting with the Mediator 233 | 234 | Anywhere you want to dispatch or emit events you will need to inject an the Mediator bean. With the Mediator messages (commands, requests, or events) can be dispatched/emitted to the proper handler(s). The follow snippet below shows the Mediator being injected into a RESTful controller and then being used to dispatch messages. 235 | 236 | ```java 237 | @RestController 238 | public class SampleController { 239 | 240 | private final Mediator mediator; 241 | 242 | @Autowired 243 | public SampleController(Mediator mediator) { 244 | this.mediator = mediator; 245 | } 246 | 247 | @PostMapping("/command") 248 | public ResponseEntity runCommand(@RequestBody CreateUserCommand command) { 249 | this.mediator.dispatch(command); 250 | return ResponseEntity.ok().build(); 251 | } 252 | 253 | @PostMapping("/request") 254 | public ResponseEntity runRequest(@RequestBody CreateUserRequest request) { 255 | UUID id = this.mediator.dispatch(request); 256 | UriComponents uri = UriComponentsBuilder.fromPath("/user/{id}").buildAndExpand(id); 257 | return ResponseEntity.created(uri.toUri()).build(); 258 | } 259 | 260 | @PostMapping("/requestAsync") 261 | public CompletableFuture runRequestAsync(@RequestBody SlowCreateUserRequest request) { 262 | return this.mediator.dispatchAsync(request) 263 | .thenApply(id -> { 264 | UriComponents uri = UriComponentsBuilder.fromPath("/user/{id}").buildAndExpand(id); 265 | return ResponseEntity.created(uri.toUri()).build(); 266 | }); 267 | } 268 | } 269 | ``` 270 | 271 | 272 | ## Troubleshooting 273 | 274 | ### No Handler Exceptions 275 | 276 | NoRequestHandlerException, NoCommandHandlerException, and NoEventHandlersException all have a common root; there was not a handler regiserted to handle that particular type of message. The two most likely causes are: 277 | 278 | 1. The Handler class is not being managed by Spring, IE forgot to add @Component to the class. 279 | 2. There is no class to handle that type of message 280 | 281 | ### Duplicate Handler Registration Exceptions 282 | 283 | Both Commands and Request message types must map to a single handler. If a particular command or request is mapped to more than a single CommandHandler/RequestHandler the application will fail to start with an exception. Solution: Don't map a command or request to multiple handlers. 284 | 285 | 286 | 287 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Joseph A Kratz 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------