├── .gitignore ├── build.gradle ├── docs ├── Framework.plantuml ├── Framework.png ├── Web.plantuml └── web.png ├── framework ├── build.gradle └── src │ ├── integrationTest │ └── java │ │ ├── io │ │ └── jd │ │ │ └── framework │ │ │ └── tests │ │ │ ├── ExampleController.java │ │ │ ├── NotWiseTransactionalManager.java │ │ │ ├── ProviderTest.java │ │ │ ├── RepositoryA.java │ │ │ ├── Service.java │ │ │ ├── ServiceA.java │ │ │ ├── ServiceB.java │ │ │ ├── ServiceC.java │ │ │ ├── Services.java │ │ │ ├── TransactionallyInterceptedTest.java │ │ │ └── WebHandlerTest.java │ │ └── notio │ │ └── notjd │ │ └── ExternalService.java │ ├── main │ ├── java │ │ └── io │ │ │ └── jd │ │ │ └── framework │ │ │ ├── BaseBeanProvider.java │ │ │ ├── BeanDefinition.java │ │ │ ├── BeanProvider.java │ │ │ ├── BeanProviderFactory.java │ │ │ ├── FailedToInstantiateBeanDefinitionException.java │ │ │ ├── Intercepted.java │ │ │ ├── ProcessingEnvUtils.java │ │ │ ├── ScopeProvider.java │ │ │ ├── processor │ │ │ ├── BeanProcessor.java │ │ │ ├── DefinitionWriter.java │ │ │ ├── Dependency.java │ │ │ ├── ProcessorPlugin.java │ │ │ └── TypeDependencyResolver.java │ │ │ ├── transactional │ │ │ ├── TransactionalInterceptedWriter.java │ │ │ ├── TransactionalMessenger.java │ │ │ └── TransactionalPlugin.java │ │ │ └── webapp │ │ │ ├── HandlerWriter.java │ │ │ ├── HttpMethod.java │ │ │ ├── IndexedValue.java │ │ │ ├── MediaType.java │ │ │ ├── Request.java │ │ │ ├── RequestHandle.java │ │ │ ├── RequestHandler.java │ │ │ ├── Response.java │ │ │ └── WebPlugin.java │ └── resources │ │ └── META-INF │ │ └── services │ │ └── javax.annotation.processing.Processor │ └── test │ ├── java │ └── io │ │ └── jd │ │ └── framework │ │ ├── AbstractAnnotationProcessorTest.java │ │ ├── DefinitionCreationTest.java │ │ ├── RequestHandlerCreationTest.java │ │ ├── TestUtil.java │ │ └── TransactionalCreationTest.java │ └── resources │ └── definitions │ ├── invalidDefs │ ├── AbstractB.java │ ├── InterfaceC.java │ └── TwoConstructors.java │ ├── simpleDefs │ ├── A.java │ ├── B.java │ └── C.java │ ├── transactional │ ├── A.java │ ├── FinalA.java │ ├── FinalMethodA.java │ ├── PrivateA.java │ └── StaticA.java │ └── web │ └── ExampleController.java ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── netty-web ├── build.gradle └── src │ ├── main │ └── java │ │ └── io │ │ └── jd │ │ └── framework │ │ └── tests │ │ ├── FrameworkHandler.java │ │ ├── ServerContainer.java │ │ └── SimpleRequest.java │ └── test │ └── java │ └── io │ └── jd │ └── framework │ └── tests │ ├── ExampleController.java │ ├── OtherController.java │ ├── ServerTest.java │ └── SimpleRequestTest.java ├── readme.md ├── settings.gradle ├── testapp-transactional ├── build.gradle └── src │ └── main │ └── java │ └── io │ └── jd │ └── testapp │ ├── DeclarativeTransactionsParticipationService.java │ ├── Event.java │ ├── EventId.java │ ├── EventRepository.java │ ├── EventRepositoryImpl.java │ ├── FrameworkApp.java │ ├── Participant.java │ ├── ParticipantId.java │ ├── ParticipantRepository.java │ ├── ParticipantRepositoryImpl.java │ ├── ParticipationService.java │ └── TransactionalManagerStub.java ├── testapp-web ├── build.gradle └── src │ └── main │ └── java │ └── io │ └── jd │ └── testapp │ ├── DeclarativeTransactionsParticipationService.java │ ├── Event.java │ ├── EventId.java │ ├── EventRepository.java │ ├── EventRepositoryImpl.java │ ├── FrameworkApp.java │ ├── Participant.java │ ├── ParticipantId.java │ ├── ParticipantRepository.java │ ├── ParticipantRepositoryImpl.java │ ├── ParticipationController.java │ ├── ParticipationDTO.java │ ├── ParticipationService.java │ └── TransactionalManagerStub.java └── testapp ├── build.gradle └── src └── main └── java └── io └── jd └── testapp ├── Event.java ├── EventId.java ├── EventRepository.java ├── EventRepositoryImpl.java ├── FrameworkApp.java ├── ManualTransactionParticipationService.java ├── NoFrameworkApp.java ├── Participant.java ├── ParticipantId.java ├── ParticipantRepository.java ├── ParticipantRepositoryImpl.java ├── ParticipationService.java └── TransactionalManagerStub.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Project exclude paths 2 | /.gradle/ 3 | /.idea/ 4 | **/.classpath 5 | **/.project 6 | **/.settings/ 7 | **/build/ 8 | **/bin/ 9 | .vscode/ 10 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'io.jd' 6 | version '0.0.1' 7 | 8 | sourceCompatibility = 17 9 | targetCompatibility = 17 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' 17 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' 18 | } 19 | 20 | test { 21 | useJUnitPlatform() 22 | } 23 | -------------------------------------------------------------------------------- /docs/Framework.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | interface BeanDefinition { 3 | T create(BeanProvider beanProvider) 4 | Class type() 5 | } 6 | 7 | class BeanA 8 | class BeanB 9 | class BeanC 10 | 11 | class $BeanA$Definition { 12 | provider : ScopeProvider 13 | } 14 | class $BeanB$Definition { 15 | provider : ScopeProvider 16 | } 17 | class $BeanC$Definition { 18 | provider : ScopeProvider 19 | } 20 | 21 | $BeanA$Definition --> BeanA : publishes definition 22 | $BeanB$Definition --> BeanB : publishes definition 23 | $BeanC$Definition --> BeanC : publishes definition 24 | 25 | BeanDefinition <|-- $BeanA$Definition 26 | BeanDefinition <|-- $BeanB$Definition 27 | BeanDefinition <|-- $BeanC$Definition 28 | 29 | interface ScopeProvider { 30 | T apply(BeanProvider beanProvider) 31 | } 32 | 33 | class SingletionScopeProvider 34 | 35 | SingletionScopeProvider --|> ScopeProvider 36 | 37 | class BeanProviderFactory { 38 | {static} BeanProvider getInstance(...) 39 | } 40 | 41 | class BaseBeanProvider { 42 | T provide(Class beanType) 43 | 44 | Iterable provideAll(Class beanType) 45 | } 46 | 47 | BeanProviderFactory --> BaseBeanProvider : create 48 | BaseBeanProvider o-- BeanDefinition 49 | 50 | class BeanProcessor 51 | 52 | note "BeanProcessor writes the source for $BeanA$Definition, $BeanB$Definition, $BeanC$Definition" as N1 53 | N1 -- BeanProcessor 54 | N1 -- $BeanA$Definition 55 | N1 -- $BeanB$Definition 56 | N1 -- $BeanC$Definition 57 | @enduml 58 | 59 | -------------------------------------------------------------------------------- /docs/Framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacekDubikowski/build-your-own-framework/6b80e090fd8fc70acebff1753ef6b4577e5ffa1a/docs/Framework.png -------------------------------------------------------------------------------- /docs/Web.plantuml: -------------------------------------------------------------------------------- 1 | @startuml web 2 | 3 | namespace AnnotationProcessing { 4 | annotation RequestHandle 5 | interface RequestHandler 6 | 7 | class BeanProcessor { 8 | webPlugin: WebPlugin 9 | } 10 | class WebPlugin 11 | 12 | class Controller$get$1$handler 13 | class Controller$post$1$handler 14 | 15 | BeanProcessor --> WebPlugin : uses 16 | 17 | RequestHandler <|-- Controller$get$1$handler 18 | RequestHandler <|-- Controller$post$1$handler 19 | 20 | note "BeanProcessor using WebPlugin writes \nthe source for implementations of RequestHandler" as N1 21 | N1 -- WebPlugin 22 | N1 -- RequestHandle 23 | N1 -- RequestHandler 24 | } 25 | 26 | class org.eclipse.jetty.server.handler.AbstractHandler 27 | 28 | namespace ServerSide { 29 | 30 | org.eclipse.jetty.server.handler.AbstractHandler <|-u- FrameworkHandler 31 | 32 | class FrameworkHandler { 33 | new(RequestHandler handler) 34 | } 35 | 36 | class ServerContainer { 37 | handlers: FrameworkHandler 38 | ServerContainer new(List handlers) 39 | void start() 40 | void stop() 41 | } 42 | ServerContainer -- AnnotationProcessing.RequestHandler : uses 43 | ServerContainer *-- FrameworkHandler 44 | } 45 | 46 | @enduml 47 | 48 | -------------------------------------------------------------------------------- /docs/web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacekDubikowski/build-your-own-framework/6b80e090fd8fc70acebff1753ef6b4577e5ffa1a/docs/web.png -------------------------------------------------------------------------------- /framework/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'java-library' 4 | id 'jvm-test-suite' 5 | } 6 | 7 | group 'io.jd.framework' 8 | version '0.0.1' 9 | 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | testing { 15 | suites { 16 | integrationTest(JvmTestSuite) { 17 | dependencies { 18 | implementation project 19 | } 20 | } 21 | } 22 | } 23 | 24 | tasks.named('check') { 25 | dependsOn testing.suites.integrationTest 26 | } 27 | 28 | dependencies { 29 | api('jakarta.inject:jakarta.inject-api:2.0.1') 30 | api('jakarta.transaction:jakarta.transaction-api:2.0.1') 31 | implementation('com.squareup:javapoet:1.13.0') 32 | implementation('org.reflections:reflections:0.10.2') 33 | testImplementation('org.junit.jupiter:junit-jupiter-api:5.8.2') 34 | testImplementation('com.google.testing.compile:compile-testing:0.19') 35 | testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.8.2') 36 | integrationTestImplementation('jakarta.transaction:jakarta.transaction-api:2.0.1') 37 | integrationTestImplementation('jakarta.inject:jakarta.inject-api:2.0.1') 38 | integrationTestImplementation('org.junit.jupiter:junit-jupiter-api:5.8.2') 39 | integrationTestRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.8.2') 40 | integrationTestAnnotationProcessor(project(":framework")) 41 | } 42 | 43 | test { 44 | useJUnitPlatform() 45 | } 46 | 47 | tasks.withType(JavaCompile) { 48 | configure(options) { 49 | options.compilerArgs << '-XprintProcessorInfo' << '-XprintRounds' 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /framework/src/integrationTest/java/io/jd/framework/tests/ExampleController.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.tests; 2 | 3 | import io.jd.framework.webapp.HttpMethod; 4 | import io.jd.framework.webapp.MediaType; 5 | import io.jd.framework.webapp.Request; 6 | import io.jd.framework.webapp.RequestHandle; 7 | import jakarta.inject.Singleton; 8 | 9 | import java.util.Random; 10 | 11 | @Singleton 12 | public class ExampleController { 13 | private final static Random RANDOM = new Random(); 14 | 15 | @RequestHandle(value = "/int", method = HttpMethod.GET, produce = "application/json+framework") 16 | public int getInt() { 17 | return RANDOM.nextInt(); 18 | } 19 | 20 | @RequestHandle(value = "/int2", method = HttpMethod.GET, produce = MediaType.TEXT_PLAIN) 21 | public int getInt(Request request) { 22 | return RANDOM.nextInt(); 23 | } 24 | 25 | @RequestHandle(value = "/int3", method = HttpMethod.POST) 26 | public int getIntFromString(Request request) { 27 | return Integer.parseInt(request.body()); 28 | } 29 | 30 | @RequestHandle(value = "/void", method = HttpMethod.POST) 31 | public void doSomething() { 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /framework/src/integrationTest/java/io/jd/framework/tests/NotWiseTransactionalManager.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.tests; 2 | 3 | import jakarta.inject.Singleton; 4 | import jakarta.transaction.Transaction; 5 | import jakarta.transaction.TransactionManager; 6 | 7 | import java.util.concurrent.atomic.AtomicInteger; 8 | 9 | @Singleton 10 | public class NotWiseTransactionalManager implements TransactionManager { 11 | 12 | private final AtomicInteger beginCounter = new AtomicInteger(); 13 | private final AtomicInteger commitCounter = new AtomicInteger(); 14 | private final AtomicInteger rollbackCounter = new AtomicInteger(); 15 | 16 | @Override 17 | public void begin() { 18 | beginCounter.incrementAndGet(); 19 | } 20 | 21 | @Override 22 | public void commit() throws SecurityException, IllegalStateException { 23 | commitCounter.incrementAndGet(); 24 | } 25 | 26 | @Override 27 | public int getStatus() { 28 | return 0; 29 | } 30 | 31 | @Override 32 | public Transaction getTransaction() { 33 | return null; 34 | } 35 | 36 | @Override 37 | public void resume(Transaction tobj) throws IllegalStateException { 38 | 39 | } 40 | 41 | @Override 42 | public void rollback() throws IllegalStateException, SecurityException { 43 | rollbackCounter.incrementAndGet(); 44 | } 45 | 46 | @Override 47 | public void setRollbackOnly() throws IllegalStateException { 48 | 49 | } 50 | 51 | @Override 52 | public void setTransactionTimeout(int seconds) { 53 | 54 | } 55 | 56 | @Override 57 | public Transaction suspend() { 58 | return null; 59 | } 60 | 61 | public AtomicInteger beginCounter() { 62 | return beginCounter; 63 | } 64 | 65 | public AtomicInteger commitCounter() { 66 | return commitCounter; 67 | } 68 | 69 | public AtomicInteger rollbackCounter() { 70 | return rollbackCounter; 71 | } 72 | 73 | void reset() { 74 | commitCounter.set(0); 75 | rollbackCounter.set(0); 76 | beginCounter.set(0); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /framework/src/integrationTest/java/io/jd/framework/tests/ProviderTest.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.tests; 2 | 3 | import io.jd.framework.BeanProvider; 4 | import io.jd.framework.BeanProviderFactory; 5 | import notio.notjd.ExternalService; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.junit.jupiter.api.Assertions.*; 9 | 10 | class ProviderTest { 11 | 12 | @Test 13 | void shouldCreateDefinitionForServices() { 14 | assertEquals(new $ServiceA$Definition().type(), ServiceA.class); 15 | assertEquals(new $ServiceB$Definition().type(), ServiceB.class); 16 | assertEquals(new $ServiceC$Definition().type(), ServiceC.class); 17 | assertEquals(new $RepositoryA$Intercepted$Definition().type(), RepositoryA$Intercepted.class); 18 | assertEquals(new $RepositoryA$Definition().type(), RepositoryA.class); 19 | } 20 | 21 | @Test 22 | void shouldProvideInstanceForServiceWithoutDependencies() { 23 | BeanProvider beanProvider = BeanProviderFactory.getInstance(); 24 | 25 | ServiceA serviceA = beanProvider.provide(ServiceA.class); 26 | assertNotNull(serviceA); 27 | assertInstanceOf(ServiceA.class, serviceA); 28 | } 29 | 30 | @Test 31 | void shouldProvideInstanceForServiceWithOneDependency() { 32 | BeanProvider beanProvider = BeanProviderFactory.getInstance(); 33 | 34 | ServiceB serviceB = beanProvider.provide(ServiceB.class); 35 | assertNotNull(serviceB); 36 | assertInstanceOf(ServiceB.class, serviceB); 37 | } 38 | 39 | @Test 40 | void shouldProvideInstanceForServiceWithTwoDependency() { 41 | BeanProvider beanProvider = BeanProviderFactory.getInstance(); 42 | 43 | ServiceC serviceC = beanProvider.provide(ServiceC.class); 44 | assertNotNull(serviceC); 45 | assertInstanceOf(ServiceC.class, serviceC); 46 | } 47 | 48 | @Test 49 | void shouldProvideInterceptedInstance() { 50 | BeanProvider beanProvider = BeanProviderFactory.getInstance(); 51 | 52 | RepositoryA repositoryA = beanProvider.provide(RepositoryA$Intercepted.class); 53 | assertNotNull(repositoryA); 54 | assertInstanceOf(RepositoryA$Intercepted.class, repositoryA); 55 | } 56 | 57 | @Test 58 | void shouldProvideTransactionallyInterceptedRepositoryWhenProvidingBaseOne() { 59 | BeanProvider beanProvider = BeanProviderFactory.getInstance(); 60 | 61 | RepositoryA repositoryA = beanProvider.provide(RepositoryA.class); 62 | assertNotNull(repositoryA); 63 | assertInstanceOf(RepositoryA$Intercepted.class, repositoryA); 64 | } 65 | 66 | @Test 67 | void shouldNotProvideBeanWhichPackageIsNotScanned() { 68 | BeanProvider beanProvider = BeanProviderFactory.getInstance(); 69 | 70 | var result = beanProvider.provideAll(ExternalService.class); 71 | assertTrue(result.isEmpty(), "Should not provide the bean which package is not scanned for BeanDefinitions"); 72 | } 73 | 74 | @Test 75 | void shouldProvideBeanWhichPackageIsNotScanned() { 76 | BeanProvider beanProvider = BeanProviderFactory.getInstance("notio.notjd"); 77 | 78 | var result = beanProvider.provideAll(ExternalService.class); 79 | assertFalse(result.isEmpty(), "Should provide the bean which package is was explicitly provided to be scanned for BeanDefinitions"); 80 | } 81 | 82 | @Test 83 | void shouldProvideClassDeclaredWithCollectionOfBeans() { 84 | BeanProvider beanProvider = BeanProviderFactory.getInstance(); 85 | 86 | Services services = beanProvider.provide(Services.class); 87 | assertNotNull(services); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /framework/src/integrationTest/java/io/jd/framework/tests/RepositoryA.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.tests; 2 | 3 | import jakarta.inject.Singleton; 4 | import jakarta.transaction.Transactional; 5 | 6 | @Singleton 7 | public class RepositoryA { 8 | 9 | @Transactional 10 | void voidMethod() { 11 | } 12 | 13 | @Transactional 14 | int intMethod() { 15 | return 1; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /framework/src/integrationTest/java/io/jd/framework/tests/Service.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.tests; 2 | 3 | public interface Service { 4 | } 5 | -------------------------------------------------------------------------------- /framework/src/integrationTest/java/io/jd/framework/tests/ServiceA.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.tests; 2 | 3 | import jakarta.inject.Singleton; 4 | 5 | @Singleton 6 | public class ServiceA implements Service { 7 | } 8 | -------------------------------------------------------------------------------- /framework/src/integrationTest/java/io/jd/framework/tests/ServiceB.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.tests; 2 | 3 | import jakarta.inject.Singleton; 4 | 5 | @Singleton 6 | public class ServiceB implements Service { 7 | private final ServiceA serviceA; 8 | 9 | public ServiceB(ServiceA serviceA) { 10 | this.serviceA = serviceA; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /framework/src/integrationTest/java/io/jd/framework/tests/ServiceC.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.tests; 2 | 3 | import jakarta.inject.Singleton; 4 | 5 | @Singleton 6 | public class ServiceC implements Service { 7 | private final ServiceA serviceA; 8 | private final ServiceB serviceB; 9 | 10 | public ServiceC(ServiceA serviceA, ServiceB serviceB) { 11 | this.serviceA = serviceA; 12 | this.serviceB = serviceB; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /framework/src/integrationTest/java/io/jd/framework/tests/Services.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.tests; 2 | 3 | import jakarta.inject.Singleton; 4 | 5 | import java.util.Collection; 6 | 7 | @Singleton 8 | public class Services { 9 | 10 | private final Collection services; 11 | 12 | public Services(Collection services) { 13 | this.services = services; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /framework/src/integrationTest/java/io/jd/framework/tests/TransactionallyInterceptedTest.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.tests; 2 | 3 | import jakarta.inject.Singleton; 4 | import jakarta.transaction.Transactional; 5 | import org.junit.jupiter.api.AfterEach; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertThrows; 10 | 11 | class TransactionallyInterceptedTest { 12 | 13 | NotWiseTransactionalManager manager = new NotWiseTransactionalManager(); 14 | TestRepository repository = new TestRepository$Intercepted(manager, new ServiceA()); 15 | 16 | @AfterEach 17 | void tearDown() { 18 | manager.reset(); 19 | repository.shouldThrow = false; 20 | } 21 | 22 | @Test 23 | void shouldCreateTransactionalVersionThatWouldBeginAndCommitTransaction() { 24 | repository.transactionalMethod(2); 25 | 26 | assertEquals(1, manager.beginCounter().get()); 27 | assertEquals(1, manager.commitCounter().get()); 28 | assertEquals(0, manager.rollbackCounter().get()); 29 | } 30 | 31 | @Test 32 | void shouldCreateTransactionalVersionThatWouldBeginAndRollbackTransactionIfThereWasError() { 33 | repository.shouldThrow = true; 34 | 35 | assertThrows(RuntimeException.class, () -> repository.transactionalMethod(3)); 36 | 37 | assertEquals(1, manager.beginCounter().get()); 38 | assertEquals(0, manager.commitCounter().get()); 39 | assertEquals(1, manager.rollbackCounter().get()); 40 | } 41 | 42 | @Test 43 | void shouldNotUseTransactionMechanismWhenCalledMethodIsNotAnnotatedAsTransactional() { 44 | repository.nonTransactionalMethod(); 45 | 46 | assertEquals(0, manager.beginCounter().get()); 47 | assertEquals(0, manager.commitCounter().get()); 48 | assertEquals(0, manager.rollbackCounter().get()); 49 | } 50 | } 51 | 52 | @Singleton 53 | class TestRepository { 54 | private final ServiceA serviceA; 55 | boolean shouldThrow = false; 56 | 57 | TestRepository(ServiceA serviceA) { 58 | this.serviceA = serviceA; 59 | } 60 | 61 | @Transactional 62 | void transactionalMethod(int a) { 63 | if (shouldThrow) { 64 | throw new RuntimeException(); 65 | } 66 | } 67 | 68 | void nonTransactionalMethod() { 69 | 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /framework/src/integrationTest/java/io/jd/framework/tests/WebHandlerTest.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.tests; 2 | 3 | import io.jd.framework.BeanProvider; 4 | import io.jd.framework.BeanProviderFactory; 5 | import io.jd.framework.webapp.HttpMethod; 6 | import io.jd.framework.webapp.MediaType; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | 11 | public class WebHandlerTest { 12 | 13 | @Test 14 | void shouldProvideHandlerInt2WithExpectedBehaviour() { 15 | BeanProvider beanProvider = BeanProviderFactory.getInstance(); 16 | var handler = beanProvider.provide(ExampleController$getInt$2$handler.class); 17 | 18 | assertEquals(HttpMethod.GET, handler.method()); 19 | assertEquals("/int2", handler.path()); 20 | assertEquals(MediaType.TEXT_PLAIN, handler.produce()); 21 | } 22 | 23 | @Test 24 | void shouldProvideHandlerInt1WithExpectedBehaviour() { 25 | BeanProvider beanProvider = BeanProviderFactory.getInstance(); 26 | var handler = beanProvider.provide(ExampleController$getInt$1$handler.class); 27 | 28 | assertEquals(HttpMethod.GET, handler.method()); 29 | assertEquals("/int", handler.path()); 30 | assertEquals("application/json+framework", handler.produce()); 31 | } 32 | 33 | @Test 34 | void shouldProvideHandlerInt3WithExpectedBehaviour() { 35 | BeanProvider beanProvider = BeanProviderFactory.getInstance(); 36 | var handler = beanProvider.provide(ExampleController$getIntFromString$1$handler.class); 37 | 38 | assertEquals(HttpMethod.POST, handler.method()); 39 | assertEquals("/int3", handler.path()); 40 | assertEquals(MediaType.APPLICATION_JSON, handler.produce()); 41 | } 42 | 43 | @Test 44 | void shouldProvideHandlerVoidWithExpectedBehaviour() { 45 | BeanProvider beanProvider = BeanProviderFactory.getInstance(); 46 | var handler = beanProvider.provide(ExampleController$doSomething$1$handler.class); 47 | 48 | assertEquals(HttpMethod.POST, handler.method()); 49 | assertEquals("/void", handler.path()); 50 | assertEquals(MediaType.APPLICATION_JSON, handler.produce()); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /framework/src/integrationTest/java/notio/notjd/ExternalService.java: -------------------------------------------------------------------------------- 1 | package notio.notjd; 2 | 3 | import jakarta.inject.Singleton; 4 | 5 | @Singleton 6 | public class ExternalService { 7 | } 8 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/BaseBeanProvider.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework; 2 | 3 | import java.util.List; 4 | 5 | import static java.util.function.Predicate.not; 6 | 7 | class BaseBeanProvider implements BeanProvider { 8 | private final List> definitions; 9 | 10 | public BaseBeanProvider(List> definitions) { 11 | this.definitions = definitions; 12 | } 13 | 14 | @Override 15 | public T provide(Class beanType) { 16 | var beans = provideAll(beanType); 17 | if (beans.isEmpty()) { 18 | throw new IllegalStateException("No bean of given type: '%s'".formatted(beanType.getCanonicalName())); 19 | } else if (beans.size() > 1) { 20 | throw new IllegalStateException("More than one bean of given type: '%s'".formatted(beanType.getCanonicalName())); 21 | } else { 22 | return beans.get(0); 23 | } 24 | } 25 | 26 | @Override 27 | public List provideAll(Class beanType) { 28 | var allBeans = definitions.stream().filter(def -> beanType.isAssignableFrom(def.type())) 29 | .map(def -> beanType.cast(def.create(this))) 30 | .toList(); 31 | var interceptedTypes = allBeans.stream().filter(bean -> Intercepted.class.isAssignableFrom(bean.getClass())) 32 | .map(bean -> ((Intercepted) bean).interceptedType()) 33 | .toList(); 34 | return allBeans.stream().filter(not(bean -> interceptedTypes.contains(bean.getClass()))).toList(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/BeanDefinition.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework; 2 | 3 | public interface BeanDefinition { 4 | T create(BeanProvider beanProvider); 5 | 6 | Class type(); 7 | } 8 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/BeanProvider.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework; 2 | 3 | import java.util.Collection; 4 | 5 | public interface BeanProvider { 6 | T provide(Class beanType); 7 | 8 | Collection provideAll(Class beanType); 9 | } 10 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/BeanProviderFactory.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework; 2 | 3 | import org.reflections.Reflections; 4 | import org.reflections.Store; 5 | import org.reflections.util.ConfigurationBuilder; 6 | import org.reflections.util.FilterBuilder; 7 | import org.reflections.util.QueryFunction; 8 | 9 | import java.lang.reflect.InvocationTargetException; 10 | import java.util.Arrays; 11 | import java.util.List; 12 | 13 | import static org.reflections.scanners.Scanners.SubTypes; 14 | 15 | public class BeanProviderFactory { 16 | 17 | private static final QueryFunction> TYPE_QUERY = SubTypes.of(BeanDefinition.class).asClass(); 18 | 19 | public static BeanProvider getInstance(String... packages) { 20 | ConfigurationBuilder reflectionsConfig = new ConfigurationBuilder() 21 | .forPackage("io.jd") 22 | .forPackages(packages) 23 | .filterInputsBy(createPackageFilter(packages)); 24 | var reflections = new Reflections(reflectionsConfig); 25 | var definitions = definitions(reflections); 26 | return new BaseBeanProvider(definitions); 27 | } 28 | 29 | private static FilterBuilder createPackageFilter(String[] packages) { 30 | var filter = new FilterBuilder().includePackage("io.jd"); 31 | Arrays.asList(packages).forEach(filter::includePackage); 32 | return filter; 33 | } 34 | 35 | private static List> definitions(Reflections reflections) { 36 | return reflections 37 | .get(TYPE_QUERY) 38 | .stream() 39 | .map(BeanProviderFactory::getInstance) 40 | .toList(); 41 | } 42 | 43 | private static BeanDefinition getInstance(Class e) { 44 | try { 45 | return (BeanDefinition) e.getDeclaredConstructors()[0].newInstance(); 46 | } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) { 47 | throw new FailedToInstantiateBeanDefinitionException(e, ex); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/FailedToInstantiateBeanDefinitionException.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework; 2 | 3 | class FailedToInstantiateBeanDefinitionException extends RuntimeException { 4 | 5 | public FailedToInstantiateBeanDefinitionException(Class definitionClass, Throwable cause) { 6 | super("Failed to instantiate '%s'".formatted(definitionClass.getCanonicalName()), cause); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/Intercepted.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework; 2 | 3 | public interface Intercepted { 4 | 5 | Class interceptedType(); 6 | } 7 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/ProcessingEnvUtils.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework; 2 | 3 | import javax.annotation.processing.ProcessingEnvironment; 4 | import javax.lang.model.element.Element; 5 | import javax.lang.model.element.PackageElement; 6 | 7 | public class ProcessingEnvUtils { 8 | private ProcessingEnvUtils() { 9 | } 10 | 11 | public static String getPackageName(ProcessingEnvironment processingEnvironment, Element element) { 12 | return getPackageElement(processingEnvironment, element).getQualifiedName().toString(); 13 | } 14 | 15 | public static PackageElement getPackageElement(ProcessingEnvironment processingEnvironment, Element element) { 16 | return processingEnvironment.getElementUtils().getPackageOf(element); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/ScopeProvider.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework; 2 | 3 | import java.util.function.Function; 4 | 5 | public sealed interface ScopeProvider extends Function { 6 | 7 | static ScopeProvider singletonScope(Function delegate) { 8 | return new SingletonProvider<>(delegate); 9 | } 10 | } 11 | 12 | final class SingletonProvider implements ScopeProvider { 13 | private final Function delegate; 14 | private volatile T value; 15 | 16 | SingletonProvider(Function delegate) { 17 | this.delegate = delegate; 18 | } 19 | 20 | public synchronized T apply(BeanProvider beanProvider) { 21 | if (value == null) { 22 | value = delegate.apply(beanProvider); 23 | } 24 | return value; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/processor/BeanProcessor.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.processor; 2 | 3 | import com.squareup.javapoet.JavaFile; 4 | import io.jd.framework.transactional.TransactionalPlugin; 5 | import io.jd.framework.webapp.WebPlugin; 6 | import jakarta.inject.Singleton; 7 | 8 | import javax.annotation.processing.*; 9 | import javax.lang.model.SourceVersion; 10 | import javax.lang.model.element.TypeElement; 11 | import javax.lang.model.util.ElementFilter; 12 | import java.io.IOException; 13 | import java.util.Collection; 14 | import java.util.List; 15 | import java.util.Set; 16 | 17 | import static javax.tools.Diagnostic.Kind.ERROR; 18 | 19 | @SupportedAnnotationTypes({"jakarta.inject.Singleton"}) 20 | @SupportedSourceVersion(SourceVersion.RELEASE_17) 21 | public class BeanProcessor extends AbstractProcessor { 22 | private List plugins = List.of(); 23 | private TypeElement collectionElement; 24 | 25 | @Override 26 | public synchronized void init(ProcessingEnvironment processingEnv) { 27 | super.init(processingEnv); 28 | plugins = List.of(new TransactionalPlugin(), new WebPlugin()); 29 | plugins.forEach(processorPlugin -> processorPlugin.init(processingEnv)); 30 | this.collectionElement = processingEnv.getElementUtils().getTypeElement("java.util.Collection"); 31 | } 32 | 33 | @Override 34 | public boolean process(Set annotations, RoundEnvironment roundEnv) { 35 | try { 36 | runPluginsProcessing(roundEnv); 37 | processBeans(roundEnv); 38 | } catch (Exception e) { 39 | processingEnv.getMessager().printMessage(ERROR, "Exception occurred %s".formatted(e)); 40 | } 41 | return false; 42 | } 43 | 44 | private void runPluginsProcessing(RoundEnvironment roundEnv) { 45 | plugins.stream().map(processorPlugin -> processorPlugin.process(roundEnv.getElementsAnnotatedWith(processorPlugin.reactsTo()))) 46 | .flatMap(Collection::stream) 47 | .forEach(this::writeFile); 48 | } 49 | 50 | private void processBeans(RoundEnvironment roundEnv) { 51 | var annotated = roundEnv.getElementsAnnotatedWith(Singleton.class); 52 | var types = ElementFilter.typesIn(annotated); 53 | var typeDependencyResolver = new TypeDependencyResolver(); 54 | types.stream().map(t -> typeDependencyResolver.resolve(t, processingEnv.getMessager())) 55 | .forEach(this::writeDefinition); 56 | } 57 | 58 | private void writeDefinition(Dependency dependency) { 59 | JavaFile javaFile = new DefinitionWriter( 60 | dependency.type(), 61 | dependency.dependencies(), 62 | processingEnv.getTypeUtils(), 63 | this.collectionElement 64 | ).createDefinition(); 65 | writeFile(javaFile); 66 | } 67 | 68 | private void writeFile(JavaFile javaFile) { 69 | try { 70 | javaFile.writeTo(processingEnv.getFiler()); 71 | } catch (IOException e) { 72 | processingEnv.getMessager().printMessage(ERROR, "Failed to write definition %s".formatted(javaFile)); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/processor/DefinitionWriter.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.processor; 2 | 3 | import com.squareup.javapoet.*; 4 | import io.jd.framework.BeanDefinition; 5 | import io.jd.framework.BeanProvider; 6 | import io.jd.framework.ScopeProvider; 7 | 8 | import javax.lang.model.element.Modifier; 9 | import javax.lang.model.element.TypeElement; 10 | import javax.lang.model.type.DeclaredType; 11 | import javax.lang.model.type.TypeMirror; 12 | import javax.lang.model.util.Types; 13 | import java.util.List; 14 | 15 | import static java.util.stream.Collectors.joining; 16 | import static javax.lang.model.element.Modifier.PUBLIC; 17 | 18 | class DefinitionWriter { 19 | private final TypeElement definedClass; 20 | private final List constructorParameterTypes; 21 | private final ClassName definedClassName; 22 | private final Types types; 23 | private final TypeElement collectionElement; 24 | 25 | DefinitionWriter(TypeElement definedClass, List constructorParameterTypes, Types types, TypeElement collectionElement) { 26 | this.definedClass = definedClass; 27 | this.constructorParameterTypes = constructorParameterTypes; 28 | this.definedClassName = ClassName.get(definedClass); 29 | this.types = types; 30 | this.collectionElement = collectionElement; 31 | } 32 | 33 | public JavaFile createDefinition() { 34 | ParameterizedTypeName parameterizedBeanDefinition = ParameterizedTypeName.get(ClassName.get(BeanDefinition.class), definedClassName); 35 | var definitionSpec = TypeSpec.classBuilder("$%s$Definition".formatted(definedClassName.simpleName())) 36 | .addModifiers(PUBLIC) 37 | .addSuperinterface(parameterizedBeanDefinition) 38 | .addMethod(createMethodSpec()) 39 | .addMethod(typeMethodSpec()) 40 | .addField(scopeProvider()) 41 | .build(); 42 | return JavaFile.builder(definedClassName.packageName(), definitionSpec).build(); 43 | } 44 | 45 | private MethodSpec typeMethodSpec() { 46 | var classTypeForDefinedTyped = ParameterizedTypeName.get(ClassName.get(Class.class), definedClassName); 47 | return MethodSpec.methodBuilder("type") 48 | .addAnnotation(Override.class) 49 | .addModifiers(PUBLIC) 50 | .addStatement("return $T.class", definedClass) 51 | .returns(classTypeForDefinedTyped) 52 | .build(); 53 | } 54 | 55 | private MethodSpec createMethodSpec() { 56 | return MethodSpec.methodBuilder("create") 57 | .addAnnotation(Override.class) 58 | .addModifiers(PUBLIC) 59 | .addParameter(ParameterSpec.builder(BeanProvider.class, "beanProvider").build()) 60 | .addStatement("return provider.apply(beanProvider)") 61 | .returns(definedClassName) 62 | .build(); 63 | } 64 | 65 | private FieldSpec scopeProvider() { 66 | ParameterizedTypeName scopeProviderType = ParameterizedTypeName.get(ClassName.get(ScopeProvider.class), definedClassName); 67 | return FieldSpec.builder(scopeProviderType, "provider", Modifier.FINAL, Modifier.PRIVATE) 68 | .initializer(singletonScopeInitializer()) 69 | .build(); 70 | } 71 | 72 | private CodeBlock singletonScopeInitializer() { 73 | var providerCallAndItsTypes = constructorParameterTypes.stream() 74 | .map(this::processConstructorType) 75 | .toList(); 76 | var constructorParameters = providerCallAndItsTypes.stream() 77 | .map(ProviderCallAndItsType::callTemplate) 78 | .collect(joining(",")); 79 | var types = providerCallAndItsTypes.stream() 80 | .map(ProviderCallAndItsType::typeName) 81 | .toArray(); 82 | 83 | return CodeBlock.builder() 84 | .add("ScopeProvider.singletonScope(") 85 | .add("beanProvider -> ") 86 | .add("new ") 87 | .add("$T", definedClassName) 88 | .add("(" + constructorParameters + ")", types) 89 | .add(")") 90 | .build(); 91 | } 92 | 93 | private ProviderCallAndItsType processConstructorType(TypeMirror type) { 94 | DeclaredType declaredType = (DeclaredType) type; 95 | 96 | List typeArguments = declaredType.getTypeArguments(); 97 | if (typeArguments.size() == 0) { 98 | return new ProviderCallAndItsType("beanProvider.provide($T.class)", TypeName.get(type)); 99 | } else if (typeArguments.size() == 1 && types.isAssignable(types.erasure(type), collectionElement.asType())) { 100 | return new ProviderCallAndItsType("beanProvider.provideAll($T.class)", TypeName.get(typeArguments.get(0))); 101 | } else { 102 | throw new RuntimeException("Cannot provide %s".formatted(type)); 103 | } 104 | } 105 | 106 | private record ProviderCallAndItsType( 107 | String callTemplate, 108 | TypeName typeName 109 | ) { 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/processor/Dependency.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.processor; 2 | 3 | import javax.lang.model.element.TypeElement; 4 | import javax.lang.model.type.TypeMirror; 5 | import java.util.List; 6 | import java.util.Objects; 7 | 8 | public final class Dependency { 9 | private final TypeElement type; 10 | private final List dependencies; 11 | 12 | public Dependency(TypeElement type, List dependencies) { 13 | this.type = type; 14 | this.dependencies = dependencies; 15 | } 16 | 17 | public TypeElement type() { 18 | return type; 19 | } 20 | 21 | public List dependencies() { 22 | return dependencies; 23 | } 24 | 25 | @Override 26 | public boolean equals(Object obj) { 27 | if (obj == this) return true; 28 | if (obj == null || obj.getClass() != this.getClass()) return false; 29 | var that = (Dependency) obj; 30 | return Objects.equals(this.type, that.type) && 31 | Objects.equals(this.dependencies, that.dependencies); 32 | } 33 | 34 | @Override 35 | public int hashCode() { 36 | return Objects.hash(type, dependencies); 37 | } 38 | 39 | @Override 40 | public String toString() { 41 | return "Dependency[" + 42 | "type=" + type + ", " + 43 | "dependencies=" + dependencies + ']'; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/processor/ProcessorPlugin.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.processor; 2 | 3 | import com.squareup.javapoet.JavaFile; 4 | 5 | import javax.annotation.processing.ProcessingEnvironment; 6 | import javax.lang.model.element.Element; 7 | import java.lang.annotation.Annotation; 8 | import java.util.Collection; 9 | import java.util.Set; 10 | 11 | public interface ProcessorPlugin { 12 | void init(ProcessingEnvironment processingEnv); 13 | 14 | Collection process(Set annotated); 15 | 16 | Class reactsTo(); 17 | } 18 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/processor/TypeDependencyResolver.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.processor; 2 | 3 | import javax.annotation.processing.Messager; 4 | import javax.lang.model.element.ExecutableElement; 5 | import javax.lang.model.element.Modifier; 6 | import javax.lang.model.element.TypeElement; 7 | import javax.lang.model.element.VariableElement; 8 | import javax.lang.model.util.ElementFilter; 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | 12 | import static javax.tools.Diagnostic.Kind.ERROR; 13 | 14 | public class TypeDependencyResolver { 15 | 16 | public Dependency resolve(TypeElement element, Messager messager) { 17 | return isConcreteClass(element) 18 | ? resolveConcreteClass(element, messager) 19 | : failOnInvalidElement(element, messager); 20 | } 21 | 22 | private Dependency resolveConcreteClass(TypeElement element, Messager messager) { 23 | var constructors = ElementFilter.constructorsIn(element.getEnclosedElements()); 24 | return constructors.size() == 1 25 | ? resolveDependency(element, constructors) 26 | : failOnTooManyConstructors(element, messager, constructors); 27 | } 28 | 29 | private boolean isConcreteClass(TypeElement element) { 30 | return element.getKind().isClass() && !element.getModifiers().contains(Modifier.ABSTRACT); 31 | } 32 | 33 | private Dependency resolveDependency(TypeElement element, List constructors) { 34 | ExecutableElement constructor = constructors.get(0); 35 | return new Dependency(element, constructor.getParameters().stream().map(VariableElement::asType).toList()); 36 | } 37 | 38 | private Dependency failOnTooManyConstructors(TypeElement element, Messager messager, List constructors) { 39 | String constructorsRep = constructors.stream().map(ExecutableElement::toString).collect(Collectors.joining(", ")); 40 | messager.printMessage(ERROR, "Too many constructors of the class %s (%s)".formatted(constructorsRep, element), element); 41 | throw new IllegalStateException("Compilation faced error."); 42 | } 43 | 44 | private Dependency failOnInvalidElement(TypeElement element, Messager messager) { 45 | messager.printMessage(ERROR, "Invalid type element: %s".formatted(element), element); 46 | throw new IllegalStateException("Compilation faced error."); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/transactional/TransactionalInterceptedWriter.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.transactional; 2 | 3 | import com.squareup.javapoet.*; 4 | import io.jd.framework.Intercepted; 5 | import io.jd.framework.processor.Dependency; 6 | import io.jd.framework.processor.TypeDependencyResolver; 7 | import jakarta.inject.Singleton; 8 | import jakarta.transaction.TransactionManager; 9 | 10 | import javax.annotation.processing.Messager; 11 | import javax.lang.model.element.ExecutableElement; 12 | import javax.lang.model.element.Modifier; 13 | import javax.lang.model.element.PackageElement; 14 | import javax.lang.model.element.TypeElement; 15 | import javax.lang.model.type.TypeKind; 16 | import java.util.List; 17 | import java.util.Optional; 18 | import java.util.stream.IntStream; 19 | 20 | import static java.util.stream.Collectors.joining; 21 | import static javax.lang.model.element.Modifier.PUBLIC; 22 | 23 | class TransactionalInterceptedWriter { 24 | private static final String TRANSACTION_MANAGER = "transactionManager"; 25 | private static final Modifier[] PRIVATE_FINAL_MODIFIERS = {Modifier.PRIVATE, Modifier.FINAL}; 26 | 27 | private final TypeElement transactionalElement; 28 | private final List transactionalMethods; 29 | private final PackageElement packageElement; 30 | 31 | TransactionalInterceptedWriter(TypeElement transactionalElement, List transactionalMethods, PackageElement packageElement) { 32 | this.transactionalElement = transactionalElement; 33 | this.transactionalMethods = transactionalMethods; 34 | this.packageElement = packageElement; 35 | } 36 | 37 | private static CodeBlock tryClause(CodeBlock transactionalMethodCall, CodeBlock catchClause) { 38 | return CodeBlock.builder() 39 | .beginControlFlow("try") 40 | .add(transactionalMethodCall) 41 | .endControlFlow() 42 | .add(catchClause) 43 | .build(); 44 | } 45 | 46 | public JavaFile createDefinition(Messager messager) { 47 | TypeSpec typeSpec = TypeSpec.classBuilder("%s$Intercepted".formatted(transactionalElement.getSimpleName().toString())) 48 | .addAnnotation(Singleton.class) 49 | .superclass(transactionalElement.asType()) 50 | .addSuperinterface(TypeName.get(Intercepted.class)) 51 | .addField(TransactionManager.class, TRANSACTION_MANAGER, PRIVATE_FINAL_MODIFIERS) 52 | .addMethod(constructor(messager)) 53 | .addMethod(interceptedTypeMethod()) 54 | .addMethods(transactionalMethodDefinitions()) 55 | .build(); 56 | return JavaFile.builder(packageElement.getQualifiedName().toString(), typeSpec).build(); 57 | } 58 | 59 | private MethodSpec constructor(Messager messager) { 60 | Dependency dependency = new TypeDependencyResolver().resolve(transactionalElement, messager); 61 | var typeNames = dependency.dependencies().stream().map(TypeName::get).toList(); 62 | var constructorParameters = typeNames.stream() 63 | .map(typeName -> ParameterSpec.builder(typeName, "$" + typeNames.indexOf(typeName)).build()) 64 | .toList(); 65 | var superCallParams = IntStream.range(0, typeNames.size()) 66 | .mapToObj(integer -> "$" + integer) 67 | .collect(joining(", ")); 68 | 69 | return MethodSpec.constructorBuilder() 70 | .addParameter(ParameterSpec.builder(TransactionManager.class, TRANSACTION_MANAGER).build()) 71 | .addParameters(constructorParameters) 72 | .addCode(CodeBlock.builder() 73 | .addStatement("super($L)", superCallParams) 74 | .addStatement("this.$L = $L", TRANSACTION_MANAGER, TRANSACTION_MANAGER) 75 | .build()) 76 | .build(); 77 | } 78 | 79 | private MethodSpec interceptedTypeMethod() { 80 | return MethodSpec.methodBuilder("interceptedType") 81 | .addAnnotation(Override.class) 82 | .addModifiers(PUBLIC) 83 | .addStatement("return $T.class", TypeName.get(transactionalElement.asType())) 84 | .returns(ClassName.get(Class.class)) 85 | .build(); 86 | } 87 | 88 | private List transactionalMethodDefinitions() { 89 | return transactionalMethods.stream().map(this::generateTransactionalMethod).toList(); 90 | } 91 | 92 | private MethodSpec generateTransactionalMethod(ExecutableElement executableElement) { 93 | var methodName = executableElement.getSimpleName().toString(); 94 | var transactionalMethodCall = transactionalMethodCall(executableElement); 95 | var methodCode = tryClause(transactionalMethodCall, catchClause()); 96 | return MethodSpec.methodBuilder(methodName) 97 | .addModifiers(executableElement.getModifiers()) 98 | .addParameters(executableElement.getParameters().stream().map(ParameterSpec::get).toList()) 99 | .addAnnotation(Override.class) 100 | .addCode(methodCode) 101 | .returns(TypeName.get(executableElement.getReturnType())) 102 | .addTypeVariables(getTypeVariableIfNeeded(executableElement).stream().toList()) 103 | .build(); 104 | } 105 | 106 | private CodeBlock transactionalMethodCall(ExecutableElement executableElement) { 107 | return executableElement.getReturnType().getKind() == TypeKind.VOID 108 | ? transactionalVoidCall(executableElement) 109 | : returningTransactionalMethodCall(executableElement); 110 | } 111 | 112 | private CodeBlock transactionalVoidCall(ExecutableElement method) { 113 | var params = translateMethodToSuperCallParams(method); 114 | return CodeBlock.builder() 115 | .addStatement(TRANSACTION_MANAGER + ".begin()") 116 | .addStatement("super.$L(%s)".formatted(params), method.getSimpleName()) 117 | .addStatement(TRANSACTION_MANAGER + ".commit()") 118 | .build(); 119 | } 120 | 121 | private CodeBlock returningTransactionalMethodCall(ExecutableElement method) { 122 | var methodName = method.getSimpleName(); 123 | var params = translateMethodToSuperCallParams(method); 124 | return CodeBlock.builder() 125 | .addStatement(TRANSACTION_MANAGER + ".begin()") 126 | .addStatement("var $LReturnValue = ($L) super.$L(%s)".formatted(params), methodName, method.getReturnType(), methodName) 127 | .addStatement(TRANSACTION_MANAGER + ".commit()") 128 | .addStatement("return $LReturnValue", methodName) 129 | .build(); 130 | } 131 | 132 | private String translateMethodToSuperCallParams(ExecutableElement method) { 133 | return method.getParameters().stream().map(variableElement -> variableElement.getSimpleName().toString()).collect(joining(", ")); 134 | } 135 | 136 | private CodeBlock catchClause() { 137 | return CodeBlock.builder() 138 | .beginControlFlow("catch ($T e)", Exception.class) 139 | .beginControlFlow("try") 140 | .addStatement(TRANSACTION_MANAGER + ".rollback()") 141 | .endControlFlow() 142 | .beginControlFlow("catch ($T innerException)", Exception.class) 143 | .addStatement("throw new $T(innerException)", RuntimeException.class) 144 | .endControlFlow() 145 | .addStatement("throw new $T(e)", RuntimeException.class) 146 | .endControlFlow() 147 | .build(); 148 | } 149 | 150 | private Optional getTypeVariableIfNeeded(ExecutableElement executableElement) { 151 | if (executableElement.getReturnType().getKind() == TypeKind.TYPEVAR) { 152 | return Optional.of(TypeVariableName.get(executableElement.getReturnType().toString())); 153 | } 154 | return Optional.empty(); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/transactional/TransactionalMessenger.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.transactional; 2 | 3 | import javax.annotation.processing.Messager; 4 | import javax.lang.model.element.Element; 5 | import javax.lang.model.element.ExecutableElement; 6 | import javax.lang.model.element.Modifier; 7 | import java.util.Set; 8 | import java.util.function.Function; 9 | 10 | import static javax.tools.Diagnostic.Kind.ERROR; 11 | 12 | class TransactionalMessenger { 13 | private final Messager messager; 14 | 15 | public TransactionalMessenger(Messager messager) { 16 | this.messager = messager; 17 | } 18 | 19 | void raiseFor(Set executableElements, Modifier modifier, String errorMessage) { 20 | raiseFor(executableElements, modifier, errorMessage, Function.identity()); 21 | } 22 | 23 | void raiseFor(Set executableElements, Modifier modifier, String errorMessage, Function mapper) { 24 | executableElements.stream().map(mapper) 25 | .filter(method -> method.getModifiers().contains(modifier)) 26 | .findFirst() 27 | .ifPresent(method -> messager.printMessage(ERROR, errorMessage, method)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/transactional/TransactionalPlugin.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.transactional; 2 | 3 | import com.squareup.javapoet.JavaFile; 4 | import io.jd.framework.ProcessingEnvUtils; 5 | import io.jd.framework.processor.ProcessorPlugin; 6 | import jakarta.transaction.Transactional; 7 | 8 | import javax.annotation.processing.ProcessingEnvironment; 9 | import javax.lang.model.element.Element; 10 | import javax.lang.model.element.ExecutableElement; 11 | import javax.lang.model.element.Modifier; 12 | import javax.lang.model.element.TypeElement; 13 | import javax.lang.model.util.ElementFilter; 14 | import java.lang.annotation.Annotation; 15 | import java.util.Collection; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Set; 19 | 20 | import static java.util.stream.Collectors.groupingBy; 21 | 22 | public class TransactionalPlugin implements ProcessorPlugin { 23 | private TransactionalMessenger transactionalMessenger; 24 | private ProcessingEnvironment processingEnv; 25 | 26 | @Override 27 | public void init(ProcessingEnvironment processingEnv) { 28 | this.processingEnv = processingEnv; 29 | transactionalMessenger = new TransactionalMessenger(processingEnv.getMessager()); 30 | } 31 | 32 | @Override 33 | public Class reactsTo() { 34 | return Transactional.class; 35 | } 36 | 37 | @Override 38 | public Collection process(Set annotated) { 39 | Set transactionalMethods = ElementFilter.methodsIn(annotated); 40 | validateMethods(transactionalMethods); 41 | Map> typeToTransactionalMethods = transactionalMethods.stream().collect(groupingBy(element -> (TypeElement) element.getEnclosingElement())); 42 | return typeToTransactionalMethods.entrySet() 43 | .stream() 44 | .map(this::writeTransactional) 45 | .toList(); 46 | } 47 | 48 | private void validateMethods(Set transactionalMethods) { 49 | raiseForPrivate(transactionalMethods); 50 | raiseForStatic(transactionalMethods); 51 | raiseForFinalMethods(transactionalMethods); 52 | raiseForFinalClass(transactionalMethods); 53 | } 54 | 55 | private void raiseForFinalClass(Set transactionalMethods) { 56 | transactionalMessenger.raiseFor(transactionalMethods, Modifier.FINAL, "final class annotated as transactional", javax.lang.model.element.Element::getEnclosingElement); 57 | } 58 | 59 | private void raiseForFinalMethods(Set transactionalMethods) { 60 | transactionalMessenger.raiseFor(transactionalMethods, Modifier.FINAL, "final method annotated as transactional"); 61 | } 62 | 63 | private void raiseForStatic(Set transactionalMethods) { 64 | transactionalMessenger.raiseFor(transactionalMethods, Modifier.STATIC, "static method annotated as transactional"); 65 | } 66 | 67 | private void raiseForPrivate(Set transactionalMethods) { 68 | transactionalMessenger.raiseFor(transactionalMethods, Modifier.PRIVATE, "private method annotated as transactional"); 69 | } 70 | 71 | private JavaFile writeTransactional(Map.Entry> typeElementListEntry) { 72 | var transactionalType = typeElementListEntry.getKey(); 73 | var transactionalMethods = typeElementListEntry.getValue(); 74 | var packageElement = ProcessingEnvUtils.getPackageElement(processingEnv, transactionalType); 75 | 76 | return new TransactionalInterceptedWriter(transactionalType, transactionalMethods, packageElement) 77 | .createDefinition(processingEnv.getMessager()); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/webapp/HandlerWriter.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.webapp; 2 | 3 | import com.squareup.javapoet.*; 4 | import jakarta.inject.Singleton; 5 | 6 | import javax.annotation.processing.ProcessingEnvironment; 7 | import javax.lang.model.element.*; 8 | import javax.lang.model.type.TypeKind; 9 | import javax.lang.model.type.TypeMirror; 10 | import javax.lang.model.util.ElementFilter; 11 | import javax.lang.model.util.Types; 12 | import java.util.List; 13 | 14 | class HandlerWriter { 15 | 16 | private final ExecutableElement httpMethodElement; 17 | private final ExecutableElement produceElement; 18 | private final ExecutableElement processElement; 19 | private final ExecutableElement pathElement; 20 | private final Types typeUtils; 21 | private final TypeMirror requestType; 22 | 23 | HandlerWriter(ProcessingEnvironment processingEnv) { 24 | this.typeUtils = processingEnv.getTypeUtils(); 25 | var handlerInterfaceElement = processingEnv.getElementUtils().getTypeElement(RequestHandler.class.getCanonicalName()); 26 | this.httpMethodElement = getMethodElement(handlerInterfaceElement, "method"); 27 | this.produceElement = getMethodElement(handlerInterfaceElement, "produce"); 28 | this.processElement = getMethodElement(handlerInterfaceElement, "process"); 29 | this.pathElement = getMethodElement(handlerInterfaceElement, "path"); 30 | this.requestType = processingEnv.getElementUtils() 31 | .getTypeElement(Request.class.getCanonicalName()) 32 | .asType(); 33 | } 34 | 35 | private static MethodSpec constructor(TypeName typeName) { 36 | return MethodSpec.constructorBuilder() 37 | .addParameter(ParameterSpec.builder(typeName, "controller").build()) 38 | .addCode("this.controller = controller;") 39 | .build(); 40 | } 41 | 42 | private static ExecutableElement getMethodElement(TypeElement typeElement, String elementName) { 43 | return ElementFilter.methodsIn(typeElement.getEnclosedElements()).stream() 44 | .filter(it -> it.getSimpleName().toString().equals(elementName)) 45 | .findFirst() 46 | .get(); 47 | } 48 | 49 | TypeSpec buildHandler(String handlerMethodName, ExecutableElement handler, TypeName typeName, RequestHandle annotation) { 50 | return TypeSpec.classBuilder(handlerMethodName) 51 | .addField(FieldSpec.builder(typeName, "controller", Modifier.FINAL, Modifier.PRIVATE).build()) 52 | .addMethod(constructor(typeName)) 53 | .addAnnotation(Singleton.class) 54 | .addSuperinterface(TypeName.get(RequestHandler.class)) 55 | .addModifiers(Modifier.FINAL) 56 | .addMethods(List.of( 57 | produce(annotation.produce()), 58 | path(annotation.value()), 59 | method(annotation.method()), 60 | process(handler) 61 | )) 62 | .build(); 63 | } 64 | 65 | private MethodSpec method(HttpMethod httpMethod) { 66 | return MethodSpec.overriding(httpMethodElement) 67 | .addCode("return $T.$L;", httpMethod.getClass(), httpMethod.toString()) 68 | .build(); 69 | } 70 | 71 | private MethodSpec path(String value) { 72 | return MethodSpec.overriding(pathElement) 73 | .addCode("return $S;", value) 74 | .build(); 75 | } 76 | 77 | private MethodSpec produce(String produce) { 78 | return MethodSpec.overriding(produceElement) 79 | .addCode("return $S;", produce) 80 | .build(); 81 | } 82 | 83 | private MethodSpec process(ExecutableElement handlerMethod) { 84 | var controllerCall = controllerCall(handlerMethod, handlerMethod.getParameters()); 85 | TypeKind handlerMethodType = handlerMethod.getReturnType().getKind(); 86 | return handlerMethodType == TypeKind.VOID 87 | ? voidMethod(controllerCall) 88 | : valueReturningMethod(controllerCall); 89 | } 90 | 91 | private MethodSpec valueReturningMethod(CodeBlock controllerCall) { 92 | return MethodSpec.overriding(processElement) 93 | .addStatement("return $L", controllerCall) 94 | .build(); 95 | } 96 | 97 | private MethodSpec voidMethod(CodeBlock controllerCall) { 98 | return MethodSpec.overriding(processElement) 99 | .addStatement(controllerCall) 100 | .addStatement("return $T.noContent()", Response.class) 101 | .build(); 102 | } 103 | 104 | private CodeBlock controllerCall(ExecutableElement handlerMethod, List parameters) { 105 | if (parameters.size() > 1 || !doesParamTypesMatchRequest(parameters)) { 106 | throw new RuntimeException("Too many parameters or type of param is not Request"); 107 | } 108 | var methodCallParams = parameters.stream().map(VariableElement::getSimpleName).map(Name::toString) 109 | .findFirst() 110 | .map(__ -> "(arg0)") 111 | .orElse("()"); 112 | return CodeBlock.builder() 113 | .add("controller.$L$L", handlerMethod.getSimpleName().toString(), methodCallParams) 114 | .build(); 115 | } 116 | 117 | private boolean doesParamTypesMatchRequest(List parameters) { 118 | var requestType = this.requestType; 119 | return parameters.stream().map(VariableElement::asType) 120 | .allMatch(paramType -> typeUtils.isSameType(paramType, requestType)); 121 | } 122 | } -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/webapp/HttpMethod.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.webapp; 2 | 3 | public enum HttpMethod { 4 | DELETE, 5 | GET, 6 | PATCH, 7 | POST, 8 | PUT 9 | } 10 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/webapp/IndexedValue.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.webapp; 2 | 3 | import java.util.function.Function; 4 | 5 | record IndexedValue(int index, T value) { 6 | static Function> indexed() { 7 | return new Function<>() { 8 | int index = 1; 9 | 10 | @Override 11 | public IndexedValue apply(T t) { 12 | return new IndexedValue(index++, t); 13 | } 14 | }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/webapp/MediaType.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.webapp; 2 | 3 | public class MediaType { 4 | 5 | public static final String APPLICATION_JSON = "application/json"; 6 | public static final String TEXT_PLAIN = "text/plain"; 7 | 8 | private MediaType() { 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/webapp/Request.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.webapp; 2 | 3 | public interface Request { 4 | String body(); 5 | } 6 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/webapp/RequestHandle.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.webapp; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.SOURCE) 9 | @Target(ElementType.METHOD) 10 | public @interface RequestHandle { 11 | 12 | HttpMethod method(); 13 | 14 | String value() default "/"; 15 | 16 | String produce() default MediaType.APPLICATION_JSON; 17 | } 18 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/webapp/RequestHandler.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.webapp; 2 | 3 | public interface RequestHandler { 4 | 5 | HttpMethod method(); 6 | 7 | String produce(); 8 | 9 | String path(); 10 | 11 | Object process(Request request) throws Exception; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/webapp/Response.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.webapp; 2 | 3 | public interface Response { 4 | static Response noContent() { 5 | return new Response() { 6 | @Override 7 | public int statusCode() { 8 | return 204; 9 | } 10 | 11 | @Override 12 | public String body() { 13 | return null; 14 | } 15 | }; 16 | } 17 | 18 | int statusCode(); 19 | 20 | String body(); 21 | } 22 | -------------------------------------------------------------------------------- /framework/src/main/java/io/jd/framework/webapp/WebPlugin.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.webapp; 2 | 3 | import com.squareup.javapoet.JavaFile; 4 | import com.squareup.javapoet.TypeName; 5 | import com.squareup.javapoet.TypeSpec; 6 | import io.jd.framework.ProcessingEnvUtils; 7 | import io.jd.framework.processor.ProcessorPlugin; 8 | 9 | import javax.annotation.processing.ProcessingEnvironment; 10 | import javax.lang.model.element.Element; 11 | import javax.lang.model.element.ExecutableElement; 12 | import javax.lang.model.element.Name; 13 | import javax.lang.model.util.ElementFilter; 14 | import javax.tools.Diagnostic; 15 | import java.lang.annotation.Annotation; 16 | import java.util.Collection; 17 | import java.util.List; 18 | import java.util.Set; 19 | import java.util.stream.Stream; 20 | 21 | import static java.util.stream.Collectors.groupingBy; 22 | 23 | public class WebPlugin implements ProcessorPlugin { 24 | private HandlerWriter handlerWriter; 25 | private ProcessingEnvironment processingEnv; 26 | 27 | @Override 28 | public void init(ProcessingEnvironment processingEnv) { 29 | this.processingEnv = processingEnv; 30 | this.handlerWriter = new HandlerWriter(processingEnv); 31 | } 32 | 33 | @Override 34 | public Class reactsTo() { 35 | return RequestHandle.class; 36 | } 37 | 38 | @Override 39 | public Collection process(Set annotated) { 40 | return ElementFilter.methodsIn(annotated).stream() 41 | .collect(groupingBy(NameData::new)) 42 | .entrySet() 43 | .stream() 44 | .flatMap(entry -> handle(entry.getKey(), entry.getValue())) 45 | .toList(); 46 | } 47 | 48 | private Stream handle(NameData nameData, List handlers) { 49 | return handlers.stream() 50 | .map(IndexedValue.indexed()) 51 | .map(element -> handle(nameData, element)); 52 | } 53 | 54 | private JavaFile handle(NameData nameData, IndexedValue indexedValue) { 55 | try { 56 | return getJavaFile(nameData, indexedValue); 57 | } catch (Exception e) { 58 | processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage(), indexedValue.value()); 59 | throw e; 60 | } 61 | } 62 | 63 | private JavaFile getJavaFile(NameData nameData, IndexedValue indexedValue) { 64 | var handlerMethod = indexedValue.value(); 65 | var typeSpec = createType(nameData, indexedValue, handlerMethod); 66 | String packageName = ProcessingEnvUtils.getPackageName(processingEnv, handlerMethod); 67 | return JavaFile.builder(packageName, typeSpec).build(); 68 | } 69 | 70 | private TypeSpec createType(NameData nameData, IndexedValue indexedValue, ExecutableElement handlerMethod) { 71 | return handlerWriter.buildHandler( 72 | nameData.toHandlerMethodName(indexedValue.index()), 73 | handlerMethod, 74 | TypeName.get(handlerMethod.getEnclosingElement().asType()), 75 | handlerMethod.getAnnotation(RequestHandle.class) 76 | ); 77 | } 78 | 79 | private record NameData(Name controllerName, Name handleName) { 80 | NameData(ExecutableElement element) { 81 | this(element.getEnclosingElement().getSimpleName(), element.getSimpleName()); 82 | } 83 | 84 | String toHandlerMethodName(int index) { 85 | return "%s$%s$%s$handler".formatted(controllerName.toString(), handleName.toString(), index); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /framework/src/main/resources/META-INF/services/javax.annotation.processing.Processor: -------------------------------------------------------------------------------- 1 | io.jd.framework.processor.BeanProcessor 2 | -------------------------------------------------------------------------------- /framework/src/test/java/io/jd/framework/AbstractAnnotationProcessorTest.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework; 2 | 3 | import com.google.testing.compile.Compiler; 4 | import io.jd.framework.processor.BeanProcessor; 5 | 6 | public class AbstractAnnotationProcessorTest { 7 | protected final Compiler javac = Compiler.javac() 8 | .withProcessors(new BeanProcessor()); 9 | } 10 | -------------------------------------------------------------------------------- /framework/src/test/java/io/jd/framework/DefinitionCreationTest.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework; 2 | 3 | import com.google.testing.compile.Compilation; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import javax.tools.JavaFileObject; 7 | import java.util.List; 8 | import java.util.stream.Stream; 9 | 10 | import static com.google.testing.compile.CompilationSubject.assertThat; 11 | import static io.jd.framework.TestUtil.getJavaFileObject; 12 | import static io.jd.framework.TestUtil.getJavaFileObjects; 13 | 14 | public class DefinitionCreationTest extends AbstractAnnotationProcessorTest { 15 | private static final List CLASS_NAMES = Stream.of("A", "B", "C").toList(); 16 | private static final List FILES_TO_CREATE_DEFS = getJavaFileObjects(CLASS_NAMES.stream(), "definitions/simpleDefs/%s.java"); 17 | private static final JavaFileObject TWO_CONSTRUCTORS = getJavaFileObject("definitions/invalidDefs/TwoConstructors.java"); 18 | private static final JavaFileObject ABSTRACT_B = getJavaFileObject("definitions/invalidDefs/AbstractB.java"); 19 | private static final JavaFileObject INTERFACE_C = getJavaFileObject("definitions/invalidDefs/InterfaceC.java"); 20 | 21 | private static final String PACKAGE_NAME = "io.jd.framework.definitions"; 22 | 23 | @Test 24 | void shouldCreateExpectedDefinitions() { 25 | Compilation compilation = javac.compile(FILES_TO_CREATE_DEFS); 26 | 27 | assertThat(compilation).succeededWithoutWarnings(); 28 | CLASS_NAMES.forEach(className -> 29 | assertThat(compilation).generatedSourceFile("%s.$%s$Definition".formatted(PACKAGE_NAME, className)) 30 | ); 31 | } 32 | 33 | @Test 34 | void shouldFailForInterfaces() { 35 | Compilation compilation = javac.compile(INTERFACE_C); 36 | 37 | assertThat(compilation).failed(); 38 | assertThat(compilation).hadErrorContaining("Invalid type element: "); 39 | } 40 | 41 | @Test 42 | void shouldFailForAbstractClasses() { 43 | Compilation compilation = javac.compile(ABSTRACT_B); 44 | 45 | assertThat(compilation).hadErrorContaining("Invalid type element: "); 46 | } 47 | 48 | @Test 49 | void shouldFailForClassesWithTwoConstructors() { 50 | Compilation compilation = javac.compile(TWO_CONSTRUCTORS); 51 | 52 | assertThat(compilation).hadErrorContaining("Too many constructors of the class"); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /framework/src/test/java/io/jd/framework/RequestHandlerCreationTest.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework; 2 | 3 | import com.google.testing.compile.Compilation; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import javax.tools.JavaFileObject; 7 | 8 | import static com.google.testing.compile.CompilationSubject.assertThat; 9 | import static io.jd.framework.TestUtil.getJavaFileObject; 10 | 11 | public class RequestHandlerCreationTest extends AbstractAnnotationProcessorTest { 12 | private static final JavaFileObject CONTROLLER = getJavaFileObject("definitions/web/ExampleController.java"); 13 | private static final String PACKAGE_NAME = "io.jd.framework.web"; 14 | 15 | @Test 16 | void shouldCreateTwoRequestHandlers() { 17 | Compilation compilation = javac.compile(CONTROLLER); 18 | 19 | assertThat(compilation).succeededWithoutWarnings(); 20 | assertThat(compilation).generatedSourceFile("%s.$ExampleController$Definition".formatted(PACKAGE_NAME)); 21 | assertThat(compilation).generatedSourceFile("%s.ExampleController$getInt$1$handler".formatted(PACKAGE_NAME)); 22 | assertThat(compilation).generatedSourceFile("%s.ExampleController$getInt$2$handler".formatted(PACKAGE_NAME)); 23 | assertThat(compilation).generatedSourceFile("%s.ExampleController$getIntFromString$1$handler".formatted(PACKAGE_NAME)); 24 | assertThat(compilation).generatedSourceFile("%s.ExampleController$doSomething$1$handler".formatted(PACKAGE_NAME)); 25 | assertThat(compilation).generatedSourceFile("%s.$ExampleController$getInt$1$handler$Definition".formatted(PACKAGE_NAME)); 26 | assertThat(compilation).generatedSourceFile("%s.$ExampleController$getInt$2$handler$Definition".formatted(PACKAGE_NAME)); 27 | assertThat(compilation).generatedSourceFile("%s.$ExampleController$getIntFromString$1$handler$Definition".formatted(PACKAGE_NAME)); 28 | assertThat(compilation).generatedSourceFile("%s.$ExampleController$doSomething$1$handler$Definition".formatted(PACKAGE_NAME)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /framework/src/test/java/io/jd/framework/TestUtil.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework; 2 | 3 | import com.google.testing.compile.JavaFileObjects; 4 | 5 | import javax.tools.JavaFileObject; 6 | import java.util.List; 7 | import java.util.stream.Stream; 8 | 9 | public class TestUtil { 10 | private TestUtil() { 11 | } 12 | 13 | public static JavaFileObject getJavaFileObject(String fileName) { 14 | return JavaFileObjects.forResource(fileName); 15 | } 16 | 17 | public static List getJavaFileObjects(Stream fileNames, String pathTemplate) { 18 | return fileNames 19 | .map(pathTemplate::formatted) 20 | .map(JavaFileObjects::forResource) 21 | .toList(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /framework/src/test/java/io/jd/framework/TransactionalCreationTest.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework; 2 | 3 | import com.google.testing.compile.Compilation; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import javax.tools.JavaFileObject; 7 | 8 | import static com.google.testing.compile.CompilationSubject.assertThat; 9 | import static io.jd.framework.TestUtil.getJavaFileObject; 10 | 11 | public class TransactionalCreationTest extends AbstractAnnotationProcessorTest { 12 | private static final JavaFileObject VALID_TRANSACTIONAL_CASE = getJavaFileObject("definitions/transactional/A.java"); 13 | private static final JavaFileObject PRIVATE_TRANSACTIONAL_METHOD_CASE = getJavaFileObject("definitions/transactional/PrivateA.java"); 14 | private static final JavaFileObject FINAL_CLASS_TRANSACTIONAL_TEST = getJavaFileObject("definitions/transactional/FinalA.java"); 15 | private static final JavaFileObject FINAL_METHOD_TRANSACTIONAL_TEST = getJavaFileObject("definitions/transactional/FinalMethodA.java"); 16 | private static final JavaFileObject STATIC_TRANSACTIONAL_TEST = getJavaFileObject("definitions/transactional/StaticA.java"); 17 | private static final String PACKAGE_NAME = "io.jd.framework.definitions"; 18 | 19 | @Test 20 | void shouldGenerateExpectedFilesForTransactional() { 21 | Compilation compilation = javac.compile(VALID_TRANSACTIONAL_CASE); 22 | 23 | assertThat(compilation).succeededWithoutWarnings(); 24 | assertThat(compilation).generatedSourceFile("%s.$A$Definition".formatted(PACKAGE_NAME)); 25 | assertThat(compilation).generatedSourceFile("%s.A$Intercepted".formatted(PACKAGE_NAME)); 26 | assertThat(compilation).generatedSourceFile("%s.$A$Intercepted$Definition".formatted(PACKAGE_NAME)); 27 | } 28 | 29 | @Test 30 | void shouldFailOnPrivateMethodAnnotatedAsTransactional() { 31 | Compilation compilation = javac.compile(PRIVATE_TRANSACTIONAL_METHOD_CASE); 32 | 33 | assertThat(compilation).failed(); 34 | assertThat(compilation).hadErrorContaining("private method annotated as transactional"); 35 | } 36 | 37 | @Test 38 | void shouldFailOnFinalClassWithMethodAnnotatedAsTransactional() { 39 | Compilation compilation = javac.compile(FINAL_CLASS_TRANSACTIONAL_TEST); 40 | 41 | assertThat(compilation).failed(); 42 | assertThat(compilation).hadErrorContaining("final class annotated as transactional"); 43 | } 44 | 45 | @Test 46 | void shouldFailOnFinalMethodAnnotatedAsTransactional() { 47 | Compilation compilation = javac.compile(FINAL_METHOD_TRANSACTIONAL_TEST); 48 | 49 | assertThat(compilation).failed(); 50 | assertThat(compilation).hadErrorContaining("final method annotated as transactional"); 51 | } 52 | 53 | @Test 54 | void shouldFailOnStaticMethodAnnotatedAsTransactional() { 55 | Compilation compilation = javac.compile(STATIC_TRANSACTIONAL_TEST); 56 | 57 | assertThat(compilation).failed(); 58 | assertThat(compilation).hadErrorContaining("static method annotated as transactional"); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /framework/src/test/resources/definitions/invalidDefs/AbstractB.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.definitions; 2 | 3 | import jakarta.inject.Singleton; 4 | 5 | @Singleton 6 | public abstract class AbstractB { 7 | } 8 | -------------------------------------------------------------------------------- /framework/src/test/resources/definitions/invalidDefs/InterfaceC.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.definitions; 2 | 3 | import jakarta.inject.Singleton; 4 | 5 | @Singleton 6 | public interface InterfaceC { 7 | } 8 | -------------------------------------------------------------------------------- /framework/src/test/resources/definitions/invalidDefs/TwoConstructors.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.definitions; 2 | 3 | import jakarta.inject.Singleton; 4 | 5 | @Singleton 6 | public class TwoConstructors { 7 | private final int b; 8 | private final String c; 9 | 10 | public TwoConstructors(int b, String c) { 11 | this.b = b; 12 | this.c = c; 13 | } 14 | 15 | public TwoConstructors(int b) { 16 | super(b, "second constructor oh no!"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /framework/src/test/resources/definitions/simpleDefs/A.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.definitions; 2 | 3 | import jakarta.inject.Singleton; 4 | 5 | @Singleton 6 | public class A { 7 | private final B b; 8 | private final C c; 9 | 10 | public A(B b, C c) { 11 | this.b = b; 12 | this.c = c; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /framework/src/test/resources/definitions/simpleDefs/B.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.definitions; 2 | 3 | import jakarta.inject.Singleton; 4 | 5 | @Singleton 6 | public class B { 7 | } 8 | -------------------------------------------------------------------------------- /framework/src/test/resources/definitions/simpleDefs/C.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.definitions; 2 | 3 | import jakarta.inject.Singleton; 4 | 5 | @Singleton 6 | public class C { 7 | } 8 | -------------------------------------------------------------------------------- /framework/src/test/resources/definitions/transactional/A.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.definitions; 2 | 3 | import jakarta.inject.Singleton; 4 | import jakarta.transaction.Transactional; 5 | 6 | @Singleton 7 | public class A { 8 | 9 | @Transactional 10 | void save() { 11 | } 12 | 13 | @Transactional 14 | int read() { 15 | return 1; 16 | } 17 | 18 | @Transactional 19 | T readGeneric() { 20 | return null; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /framework/src/test/resources/definitions/transactional/FinalA.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.definitions; 2 | 3 | import jakarta.inject.Singleton; 4 | import jakarta.transaction.Transactional; 5 | 6 | @Singleton 7 | public final class FinalA { 8 | 9 | @Transactional 10 | void sava() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /framework/src/test/resources/definitions/transactional/FinalMethodA.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.definitions; 2 | 3 | import jakarta.inject.Singleton; 4 | import jakarta.transaction.Transactional; 5 | 6 | @Singleton 7 | public class FinalMethodA { 8 | 9 | @Transactional 10 | public final void save() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /framework/src/test/resources/definitions/transactional/PrivateA.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.definitions; 2 | 3 | import jakarta.inject.Singleton; 4 | import jakarta.transaction.Transactional; 5 | 6 | @Singleton 7 | public class PrivateA { 8 | 9 | @Transactional 10 | private void save() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /framework/src/test/resources/definitions/transactional/StaticA.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.definitions; 2 | 3 | import jakarta.inject.Singleton; 4 | import jakarta.transaction.Transactional; 5 | 6 | @Singleton 7 | public class StaticA { 8 | 9 | @Transactional 10 | static void save() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /framework/src/test/resources/definitions/web/ExampleController.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.web; 2 | 3 | import io.jd.framework.webapp.HttpMethod; 4 | import io.jd.framework.webapp.Request; 5 | import io.jd.framework.webapp.RequestHandle; 6 | import jakarta.inject.Singleton; 7 | 8 | import java.util.Random; 9 | 10 | @Singleton 11 | public class ExampleController { 12 | private final static Random RANDOM = new Random(); 13 | 14 | @RequestHandle(value = "/int", method = HttpMethod.GET) 15 | int getInt() { 16 | return RANDOM.nextInt(); 17 | } 18 | 19 | @RequestHandle(value = "/int2", method = HttpMethod.GET) 20 | int getInt(Request request) { 21 | return 2; 22 | } 23 | 24 | @RequestHandle(value = "/int3", method = HttpMethod.GET) 25 | int getIntFromString(Request request) { 26 | return Integer.parseInt("1"); 27 | } 28 | 29 | @RequestHandle(value = "/void", method = HttpMethod.POST) 30 | void doSomething() { 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacekDubikowski/build-your-own-framework/6b80e090fd8fc70acebff1753ef6b4577e5ffa1a/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-7.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Stop when "xargs" is not available. 209 | if ! command -v xargs >/dev/null 2>&1 210 | then 211 | die "xargs is not available" 212 | fi 213 | 214 | # Use "xargs" to parse quoted args. 215 | # 216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 217 | # 218 | # In Bash we could simply go: 219 | # 220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 221 | # set -- "${ARGS[@]}" "$@" 222 | # 223 | # but POSIX shell has neither arrays nor command substitution, so instead we 224 | # post-process each arg (as a line of input to sed) to backslash-escape any 225 | # character that might be a shell metacharacter, then use eval to reverse 226 | # that process (while maintaining the separation between arguments), and wrap 227 | # the whole thing up as a single "set" statement. 228 | # 229 | # This will of course break if any of these variables contains a newline or 230 | # an unmatched quote. 231 | # 232 | 233 | eval "set -- $( 234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 235 | xargs -n1 | 236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 237 | tr '\n' ' ' 238 | )" '"$@"' 239 | 240 | exec "$JAVACMD" "$@" 241 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /netty-web/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'io.jd.testapp' 6 | version '0.0.1' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | implementation(project(":framework")) 14 | implementation('org.eclipse.jetty:jetty-server:11.0.13') 15 | annotationProcessor(project(":framework")) 16 | 17 | testAnnotationProcessor(project(":framework")) 18 | testImplementation('org.junit.jupiter:junit-jupiter:5.8.1') 19 | testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.8.1') 20 | } 21 | 22 | test { 23 | useJUnitPlatform() 24 | } 25 | -------------------------------------------------------------------------------- /netty-web/src/main/java/io/jd/framework/tests/FrameworkHandler.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.tests; 2 | 3 | import io.jd.framework.webapp.HttpMethod; 4 | import io.jd.framework.webapp.RequestHandler; 5 | import io.jd.framework.webapp.Response; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import org.eclipse.jetty.server.Request; 9 | import org.eclipse.jetty.server.handler.AbstractHandler; 10 | 11 | import java.io.IOException; 12 | import java.util.Objects; 13 | import java.util.stream.Collectors; 14 | 15 | class FrameworkHandler extends AbstractHandler { 16 | 17 | private final RequestHandler handler; 18 | 19 | FrameworkHandler(RequestHandler handler) { 20 | this.handler = handler; 21 | } 22 | 23 | private static void handleWorkResult(HttpServletResponse response, Object result) throws IOException { 24 | if (result instanceof Response r) { 25 | processResponse(response, r); 26 | } else { 27 | process(response, result); 28 | } 29 | } 30 | 31 | private static void processResponse(HttpServletResponse response, Response r) throws IOException { 32 | response.setStatus(r.statusCode()); 33 | if (r.body() != null) { 34 | response.getWriter().print(r.body()); 35 | } 36 | } 37 | 38 | private static void process(HttpServletResponse response, Object result) throws IOException { 39 | response.setStatus(200); 40 | response.getWriter().print(result.toString()); 41 | } 42 | 43 | private static String readWholeBody(HttpServletRequest request) throws IOException { 44 | return request.getReader().lines().collect(Collectors.joining("\n")); 45 | } 46 | 47 | @Override 48 | public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { 49 | if (methodAndPathMatches(baseRequest)) { 50 | response.setCharacterEncoding("utf-8"); 51 | response.setContentType(handler.produce()); 52 | handle(response, request); 53 | baseRequest.setHandled(true); 54 | } 55 | } 56 | 57 | private void handle(HttpServletResponse response, HttpServletRequest request) throws IOException { 58 | var frameworkRequest = SimpleRequest.of(HttpMethod.valueOf(request.getMethod()), readWholeBody(request)); 59 | try { 60 | var result = handler.process(frameworkRequest); 61 | handleWorkResult(response, result); 62 | } catch (Exception e) { 63 | response.getWriter().print("{\"errorMessage\": \"%s\"}".formatted(e.getMessage())); 64 | response.setStatus(500); 65 | } 66 | } 67 | 68 | private boolean methodAndPathMatches(Request baseRequest) { 69 | return Objects.equals(baseRequest.getHttpURI().getPath(), handler.path()) 70 | && Objects.equals(baseRequest.getMethod(), handler.method().toString()); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /netty-web/src/main/java/io/jd/framework/tests/ServerContainer.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.tests; 2 | 3 | import io.jd.framework.webapp.RequestHandler; 4 | import jakarta.inject.Singleton; 5 | import org.eclipse.jetty.server.Server; 6 | import org.eclipse.jetty.server.ServerConnector; 7 | import org.eclipse.jetty.server.handler.HandlerCollection; 8 | 9 | import java.util.Collection; 10 | 11 | @Singleton 12 | public class ServerContainer { 13 | private final HandlerCollection handlers = new HandlerCollection(); 14 | private volatile Server server = null; 15 | 16 | ServerContainer(Collection handlers) { 17 | handlers.stream() 18 | .map(FrameworkHandler::new) 19 | .forEach(this.handlers::addHandler); 20 | } 21 | 22 | public synchronized void start() throws Exception { 23 | if (server == null) { 24 | createServerInstance(); 25 | } 26 | server.start(); 27 | } 28 | 29 | private void createServerInstance() { 30 | server = new Server(); 31 | var connector = new ServerConnector(server); 32 | server.addConnector(connector); 33 | server.setHandler(this.handlers); 34 | } 35 | 36 | public int port() { 37 | return server.getURI().getPort(); 38 | } 39 | 40 | public void stop() throws Exception { 41 | server.stop(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /netty-web/src/main/java/io/jd/framework/tests/SimpleRequest.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.tests; 2 | 3 | import io.jd.framework.webapp.HttpMethod; 4 | import io.jd.framework.webapp.Request; 5 | 6 | import java.util.List; 7 | 8 | public record SimpleRequest(String body) implements Request { 9 | private static final List HTTP_METHOD_WITH_BODY = 10 | List.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH); 11 | 12 | static SimpleRequest of(HttpMethod method, String body) { 13 | return HTTP_METHOD_WITH_BODY.contains(method) ? new SimpleRequest(body) : new SimpleRequest(null); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /netty-web/src/test/java/io/jd/framework/tests/ExampleController.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.tests; 2 | 3 | import io.jd.framework.webapp.*; 4 | import jakarta.inject.Singleton; 5 | 6 | import java.util.Random; 7 | 8 | @Singleton 9 | public class ExampleController { 10 | private final static Random RANDOM = new Random(); 11 | 12 | @RequestHandle(value = "/int", method = HttpMethod.GET, produce = "application/json+framework") 13 | public int getInt() { 14 | return RANDOM.nextInt(); 15 | } 16 | 17 | @RequestHandle(value = "/int2", method = HttpMethod.GET, produce = MediaType.TEXT_PLAIN) 18 | public int getInt(Request request) { 19 | return RANDOM.nextInt(); 20 | } 21 | 22 | @RequestHandle(value = "/int3", method = HttpMethod.POST) 23 | public int getIntFromString(Request request) { 24 | return Integer.parseInt(request.body()); 25 | } 26 | 27 | @RequestHandle(value = "/void", method = HttpMethod.POST) 28 | public void doSomething() { 29 | } 30 | 31 | @RequestHandle(value = "/return-error-response", method = HttpMethod.PUT) 32 | public Response returnErrorResponse() { 33 | return new Response() { 34 | @Override 35 | public int statusCode() { 36 | return 500; 37 | } 38 | 39 | @Override 40 | public String body() { 41 | return null; 42 | } 43 | }; 44 | } 45 | 46 | @RequestHandle(value = "/error", method = HttpMethod.GET) 47 | void throwError() throws Exception { 48 | throw new Exception("Expected error message"); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /netty-web/src/test/java/io/jd/framework/tests/OtherController.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.tests; 2 | 3 | import io.jd.framework.webapp.HttpMethod; 4 | import io.jd.framework.webapp.RequestHandle; 5 | import jakarta.inject.Singleton; 6 | 7 | @Singleton 8 | public class OtherController { 9 | 10 | @RequestHandle(value = "/resource", method = HttpMethod.GET) 11 | public String getResource() { 12 | return """ 13 | {"key":"value"}"""; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /netty-web/src/test/java/io/jd/framework/tests/ServerTest.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.tests; 2 | 3 | import io.jd.framework.BeanProvider; 4 | import io.jd.framework.BeanProviderFactory; 5 | import org.junit.jupiter.api.AfterAll; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.params.ParameterizedTest; 9 | import org.junit.jupiter.params.provider.ValueSource; 10 | 11 | import java.io.IOException; 12 | import java.net.URI; 13 | import java.net.http.HttpClient; 14 | import java.net.http.HttpRequest; 15 | import java.net.http.HttpRequest.BodyPublisher; 16 | import java.net.http.HttpRequest.BodyPublishers; 17 | import java.net.http.HttpResponse; 18 | 19 | import static io.jd.framework.webapp.MediaType.APPLICATION_JSON; 20 | import static java.net.http.HttpRequest.BodyPublishers.noBody; 21 | import static java.net.http.HttpResponse.BodyHandlers.ofString; 22 | import static org.eclipse.jetty.http.HttpHeader.ACCEPT; 23 | import static org.eclipse.jetty.http.HttpHeader.CONTENT_TYPE; 24 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 25 | import static org.junit.jupiter.api.Assertions.assertEquals; 26 | 27 | class ServerTest { 28 | private static ServerContainer serverContainer = null; 29 | private final HttpClient client = HttpClient.newHttpClient(); 30 | 31 | @BeforeAll 32 | static void setupTest() throws Exception { 33 | BeanProvider beanProvider = BeanProviderFactory.getInstance(); 34 | serverContainer = beanProvider.provide(ServerContainer.class); 35 | serverContainer.start(); 36 | } 37 | 38 | @AfterAll 39 | static void cleanupTest() throws Exception { 40 | serverContainer.stop(); 41 | } 42 | 43 | private static URI path(String endpoint) { 44 | return URI.create("http://localhost:%s".formatted(serverContainer.port()) + endpoint); 45 | } 46 | 47 | private static HttpRequest getRequest(String endpointPath) { 48 | return buildWithAcceptHeader(endpointPath) 49 | .GET() 50 | .build(); 51 | } 52 | 53 | private static HttpRequest postRequest(BodyPublisher publisher, String endpoint) { 54 | return buildWithAcceptHeader(endpoint) 55 | .POST(publisher) 56 | .build(); 57 | } 58 | 59 | private static HttpRequest putRequest(BodyPublisher publisher, String endpoint) { 60 | return buildWithAcceptHeader(endpoint) 61 | .PUT(publisher) 62 | .build(); 63 | } 64 | 65 | private static HttpRequest.Builder buildWithAcceptHeader(String endpoint) { 66 | return HttpRequest.newBuilder(path(endpoint)) 67 | .header(ACCEPT.toString(), APPLICATION_JSON); 68 | } 69 | 70 | private static String getContentTypeValue(HttpResponse response) { 71 | return response.headers().firstValue(CONTENT_TYPE.toString()).get(); 72 | } 73 | 74 | @Test 75 | void testSlashInt() throws IOException, InterruptedException { 76 | var request = getRequest("/int"); 77 | 78 | var response = client.send(request, ofString()); 79 | 80 | assertEquals(200, response.statusCode()); 81 | assertDoesNotThrow(() -> Integer.parseInt(response.body())); 82 | 83 | assertEquals( 84 | "application/json+framework;charset=utf-8", 85 | getContentTypeValue(response) 86 | ); 87 | } 88 | 89 | @Test 90 | void testSlashInt2() throws IOException, InterruptedException { 91 | var request = getRequest("/int2"); 92 | 93 | var response = client.send(request, ofString()); 94 | 95 | assertEquals(200, response.statusCode()); 96 | assertDoesNotThrow(() -> Integer.parseInt(response.body())); 97 | assertEquals("text/plain;charset=utf-8", getContentTypeValue(response)); 98 | } 99 | 100 | @ParameterizedTest 101 | @ValueSource(ints = {1, 2, 11, 100, 400}) 102 | void testSlashInt3(int value) throws IOException, InterruptedException { 103 | var request = postRequest(BodyPublishers.ofString(String.valueOf(value)), "/int3"); 104 | 105 | var response = client.send(request, ofString()); 106 | 107 | assertEquals(response.statusCode(), 200); 108 | assertEquals(Integer.parseInt(response.body()), value); 109 | assertEquals(getContentTypeValue(response), APPLICATION_JSON); 110 | } 111 | 112 | @Test 113 | void testSlashVoid() throws IOException, InterruptedException { 114 | var request = postRequest(noBody(), "/void"); 115 | 116 | var response = client.send(request, ofString()); 117 | 118 | assertEquals(response.statusCode(), 204); 119 | assertEquals(response.body(), ""); 120 | } 121 | 122 | @Test 123 | void testSlashReturnErrorResponse() throws IOException, InterruptedException { 124 | var request = putRequest(noBody(), "/return-error-response"); 125 | 126 | var response = client.send(request, ofString()); 127 | 128 | assertEquals(500, response.statusCode()); 129 | assertEquals("", response.body()); 130 | } 131 | 132 | @Test 133 | void testSlashResource() throws IOException, InterruptedException { 134 | var request = getRequest("/resource"); 135 | 136 | var response = client.send(request, ofString()); 137 | 138 | assertEquals(200, response.statusCode()); 139 | assertEquals("{\"key\":\"value\"}", response.body()); 140 | } 141 | 142 | @Test 143 | void testSlashError() throws IOException, InterruptedException { 144 | var request = getRequest("/error"); 145 | 146 | var response = client.send(request, ofString()); 147 | 148 | assertEquals(500, response.statusCode()); 149 | assertEquals("{\"errorMessage\": \"Expected error message\"}", response.body()); 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /netty-web/src/test/java/io/jd/framework/tests/SimpleRequestTest.java: -------------------------------------------------------------------------------- 1 | package io.jd.framework.tests; 2 | 3 | import io.jd.framework.webapp.HttpMethod; 4 | import org.junit.jupiter.params.ParameterizedTest; 5 | import org.junit.jupiter.params.provider.EnumSource; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertNull; 9 | import static org.junit.jupiter.params.provider.EnumSource.Mode.EXCLUDE; 10 | import static org.junit.jupiter.params.provider.EnumSource.Mode.INCLUDE; 11 | 12 | class SimpleRequestTest { 13 | 14 | @ParameterizedTest 15 | @EnumSource(value = HttpMethod.class, names = {"POST", "PUT", "PATCH"}, mode = EXCLUDE) 16 | void shouldNotPassBodyForSomeHttpMethods(HttpMethod httpMethod) { 17 | var request = SimpleRequest.of(httpMethod, "body"); 18 | 19 | assertNull(request.body()); 20 | } 21 | 22 | @ParameterizedTest 23 | @EnumSource(value = HttpMethod.class, names = {"POST", "PUT", "PATCH"}, mode = INCLUDE) 24 | void shouldPassBodyForSomeHttpMethods(HttpMethod httpMethod) { 25 | String exampleBody = "body"; 26 | 27 | var request = SimpleRequest.of(httpMethod, exampleBody); 28 | 29 | assertEquals(exampleBody, request.body()); 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'build-your-own-framework' 2 | 3 | include( 4 | ':framework', 5 | ':netty-web', 6 | ':testapp', 7 | ':testapp-transactional', 8 | ':testapp-web' 9 | ) 10 | -------------------------------------------------------------------------------- /testapp-transactional/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'application' 4 | } 5 | 6 | group 'io.jd.testapp-transactional' 7 | version '0.0.1' 8 | 9 | repositories { 10 | mavenCentral() 11 | } 12 | 13 | dependencies { 14 | implementation(project(":framework")) 15 | annotationProcessor(project(":framework")) 16 | 17 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' 18 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' 19 | } 20 | 21 | ext { 22 | javaMainClass = "io.jd.testapp.FrameworkApp" 23 | } 24 | 25 | application { 26 | mainClassName = javaMainClass 27 | } 28 | 29 | test { 30 | useJUnitPlatform() 31 | } 32 | -------------------------------------------------------------------------------- /testapp-transactional/src/main/java/io/jd/testapp/DeclarativeTransactionsParticipationService.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | import jakarta.inject.Singleton; 4 | import jakarta.transaction.Transactional; 5 | 6 | @Singleton 7 | public class DeclarativeTransactionsParticipationService implements ParticipationService { 8 | private final ParticipantRepository participantRepository; 9 | private final EventRepository eventRepository; 10 | 11 | public DeclarativeTransactionsParticipationService(ParticipantRepository participantRepository, EventRepository eventRepository) { 12 | this.participantRepository = participantRepository; 13 | this.eventRepository = eventRepository; 14 | } 15 | 16 | @Override 17 | @Transactional 18 | public void participate(ParticipantId participantId, EventId eventId) { 19 | var participant = participantRepository.getParticipant(participantId); 20 | var event = eventRepository.findEvent(eventId); 21 | eventRepository.store(event.addParticipant(participant)); 22 | 23 | System.out.printf("Participant: '%s' takes part in event: '%s'%n", participant, event); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /testapp-transactional/src/main/java/io/jd/testapp/Event.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public record Event() { 4 | Event addParticipant(Participant participant) { 5 | return new Event(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /testapp-transactional/src/main/java/io/jd/testapp/EventId.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public record EventId() { 4 | } 5 | -------------------------------------------------------------------------------- /testapp-transactional/src/main/java/io/jd/testapp/EventRepository.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public interface EventRepository { 4 | Event findEvent(EventId eventId); 5 | 6 | void store(Event event); 7 | } 8 | -------------------------------------------------------------------------------- /testapp-transactional/src/main/java/io/jd/testapp/EventRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | import jakarta.inject.Singleton; 4 | 5 | @Singleton 6 | public class EventRepositoryImpl implements EventRepository { 7 | 8 | @Override 9 | public void store(Event event) { 10 | } 11 | 12 | @Override 13 | public Event findEvent(EventId eventId) { 14 | return new Event(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /testapp-transactional/src/main/java/io/jd/testapp/FrameworkApp.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | import io.jd.framework.BeanProvider; 4 | import io.jd.framework.BeanProviderFactory; 5 | 6 | public class FrameworkApp { 7 | public static void main(String[] args) { 8 | BeanProvider provider = BeanProviderFactory.getInstance(); 9 | ParticipationService participationService = provider.provide(ParticipationService.class); 10 | participationService.participate(new ParticipantId(), new EventId()); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /testapp-transactional/src/main/java/io/jd/testapp/Participant.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public record Participant() { 4 | } 5 | -------------------------------------------------------------------------------- /testapp-transactional/src/main/java/io/jd/testapp/ParticipantId.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public record ParticipantId() { 4 | } 5 | -------------------------------------------------------------------------------- /testapp-transactional/src/main/java/io/jd/testapp/ParticipantRepository.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public interface ParticipantRepository { 4 | Participant getParticipant(ParticipantId participantId); 5 | } 6 | -------------------------------------------------------------------------------- /testapp-transactional/src/main/java/io/jd/testapp/ParticipantRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | import jakarta.inject.Singleton; 4 | 5 | @Singleton 6 | public class ParticipantRepositoryImpl implements ParticipantRepository { 7 | @Override 8 | public Participant getParticipant(ParticipantId participantId) { 9 | return new Participant(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /testapp-transactional/src/main/java/io/jd/testapp/ParticipationService.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public interface ParticipationService { 4 | void participate(ParticipantId participantId, EventId eventId); 5 | } 6 | -------------------------------------------------------------------------------- /testapp-transactional/src/main/java/io/jd/testapp/TransactionalManagerStub.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | import jakarta.inject.Singleton; 4 | import jakarta.transaction.Transaction; 5 | import jakarta.transaction.TransactionManager; 6 | 7 | @Singleton 8 | public class TransactionalManagerStub implements TransactionManager { 9 | 10 | @Override 11 | public void begin() { 12 | System.out.println("Begin transaction"); 13 | } 14 | 15 | @Override 16 | public void commit() throws SecurityException, IllegalStateException { 17 | System.out.println("Commit transaction"); 18 | } 19 | 20 | @Override 21 | public void rollback() throws IllegalStateException, SecurityException { 22 | System.out.println("Rollback transaction"); 23 | } 24 | 25 | @Override 26 | public int getStatus() { 27 | return 0; 28 | } 29 | 30 | @Override 31 | public Transaction getTransaction() { 32 | return null; 33 | } 34 | 35 | @Override 36 | public void resume(Transaction tobj) throws IllegalStateException { 37 | 38 | } 39 | 40 | @Override 41 | public void setRollbackOnly() throws IllegalStateException { 42 | 43 | } 44 | 45 | @Override 46 | public void setTransactionTimeout(int seconds) { 47 | 48 | } 49 | 50 | @Override 51 | public Transaction suspend() { 52 | return null; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /testapp-web/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'application' 4 | } 5 | 6 | group 'io.jd.testapp-transactional' 7 | version '0.0.1' 8 | 9 | repositories { 10 | mavenCentral() 11 | } 12 | 13 | dependencies { 14 | implementation(project(":framework")) 15 | implementation(project(":netty-web")) 16 | implementation('com.fasterxml.jackson.core:jackson-databind:2.14.2') 17 | annotationProcessor(project(":framework")) 18 | 19 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' 20 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' 21 | } 22 | 23 | ext { 24 | javaMainClass = "io.jd.testapp.FrameworkApp" 25 | } 26 | 27 | application { 28 | mainClassName = javaMainClass 29 | } 30 | 31 | test { 32 | useJUnitPlatform() 33 | } 34 | -------------------------------------------------------------------------------- /testapp-web/src/main/java/io/jd/testapp/DeclarativeTransactionsParticipationService.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | import jakarta.inject.Singleton; 4 | import jakarta.transaction.Transactional; 5 | 6 | @Singleton 7 | public class DeclarativeTransactionsParticipationService implements ParticipationService { 8 | private final ParticipantRepository participantRepository; 9 | private final EventRepository eventRepository; 10 | 11 | public DeclarativeTransactionsParticipationService(ParticipantRepository participantRepository, EventRepository eventRepository) { 12 | this.participantRepository = participantRepository; 13 | this.eventRepository = eventRepository; 14 | } 15 | 16 | @Override 17 | @Transactional 18 | public void participate(ParticipantId participantId, EventId eventId) { 19 | System.out.printf("ParticipantId: '%s' takes part in eventId: '%s'%n", participantId, eventId); 20 | var participant = participantRepository.getParticipant(participantId); 21 | var event = eventRepository.findEvent(eventId); 22 | eventRepository.store(event.addParticipant(participant)); 23 | 24 | System.out.printf("Participant: '%s' takes part in event: '%s'%n", participant, event); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /testapp-web/src/main/java/io/jd/testapp/Event.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public record Event() { 4 | Event addParticipant(Participant participant) { 5 | return new Event(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /testapp-web/src/main/java/io/jd/testapp/EventId.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public record EventId(String value) { 4 | } 5 | -------------------------------------------------------------------------------- /testapp-web/src/main/java/io/jd/testapp/EventRepository.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public interface EventRepository { 4 | Event findEvent(EventId eventId); 5 | 6 | void store(Event event); 7 | } 8 | -------------------------------------------------------------------------------- /testapp-web/src/main/java/io/jd/testapp/EventRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | import jakarta.inject.Singleton; 4 | 5 | @Singleton 6 | public class EventRepositoryImpl implements EventRepository { 7 | 8 | @Override 9 | public void store(Event event) { 10 | } 11 | 12 | @Override 13 | public Event findEvent(EventId eventId) { 14 | return new Event(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /testapp-web/src/main/java/io/jd/testapp/FrameworkApp.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | import io.jd.framework.BeanProvider; 4 | import io.jd.framework.BeanProviderFactory; 5 | import io.jd.framework.tests.ServerContainer; 6 | 7 | public class FrameworkApp { 8 | public static void main(String[] args) throws Exception { 9 | BeanProvider provider = BeanProviderFactory.getInstance(); 10 | ServerContainer container = provider.provide(ServerContainer.class); 11 | container.start(); 12 | System.out.printf("Port: %s%n", container.port()); 13 | 14 | // wait for input to close 15 | System.in.read(); 16 | container.stop(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /testapp-web/src/main/java/io/jd/testapp/Participant.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public record Participant() { 4 | } 5 | -------------------------------------------------------------------------------- /testapp-web/src/main/java/io/jd/testapp/ParticipantId.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public record ParticipantId(String value) { 4 | } 5 | -------------------------------------------------------------------------------- /testapp-web/src/main/java/io/jd/testapp/ParticipantRepository.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public interface ParticipantRepository { 4 | Participant getParticipant(ParticipantId participantId); 5 | } 6 | -------------------------------------------------------------------------------- /testapp-web/src/main/java/io/jd/testapp/ParticipantRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | import jakarta.inject.Singleton; 4 | 5 | @Singleton 6 | public class ParticipantRepositoryImpl implements ParticipantRepository { 7 | @Override 8 | public Participant getParticipant(ParticipantId participantId) { 9 | return new Participant(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /testapp-web/src/main/java/io/jd/testapp/ParticipationController.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import io.jd.framework.webapp.HttpMethod; 5 | import io.jd.framework.webapp.Request; 6 | import io.jd.framework.webapp.RequestHandle; 7 | import jakarta.inject.Singleton; 8 | 9 | import java.io.IOException; 10 | import java.util.Map; 11 | 12 | @Singleton 13 | public class ParticipationController { 14 | private final ObjectMapper objectMapper = new ObjectMapper(); 15 | 16 | private final ParticipationService participationService; 17 | 18 | public ParticipationController(ParticipationService participationService) { 19 | this.participationService = participationService; 20 | } 21 | 22 | @RequestHandle(value = "/participate", method = HttpMethod.POST) 23 | String participate(Request request) throws IOException { 24 | var participationDTO = objectMapper.readValue(request.body(), ParticipationDTO.class); 25 | participationService.participate(participationDTO.participantId(), participationDTO.eventId()); 26 | return objectMapper.writeValueAsString(Map.of("accepted", participationDTO)); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /testapp-web/src/main/java/io/jd/testapp/ParticipationDTO.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | public record ParticipationDTO(ParticipantId participantId, EventId eventId) { 7 | @JsonCreator 8 | ParticipationDTO(@JsonProperty("participationId") String participationId, @JsonProperty("eventId") String eventId) { 9 | this(new ParticipantId(participationId), new EventId(eventId)); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /testapp-web/src/main/java/io/jd/testapp/ParticipationService.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public interface ParticipationService { 4 | void participate(ParticipantId participantId, EventId eventId); 5 | } 6 | -------------------------------------------------------------------------------- /testapp-web/src/main/java/io/jd/testapp/TransactionalManagerStub.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | import jakarta.inject.Singleton; 4 | import jakarta.transaction.Transaction; 5 | import jakarta.transaction.TransactionManager; 6 | 7 | @Singleton 8 | public class TransactionalManagerStub implements TransactionManager { 9 | 10 | @Override 11 | public void begin() { 12 | System.out.println("Begin transaction"); 13 | } 14 | 15 | @Override 16 | public void commit() throws SecurityException, IllegalStateException { 17 | System.out.println("Commit transaction"); 18 | } 19 | 20 | @Override 21 | public void rollback() throws IllegalStateException, SecurityException { 22 | System.out.println("Rollback transaction"); 23 | } 24 | 25 | @Override 26 | public int getStatus() { 27 | return 0; 28 | } 29 | 30 | @Override 31 | public Transaction getTransaction() { 32 | return null; 33 | } 34 | 35 | @Override 36 | public void resume(Transaction tobj) throws IllegalStateException { 37 | 38 | } 39 | 40 | @Override 41 | public void setRollbackOnly() throws IllegalStateException { 42 | 43 | } 44 | 45 | @Override 46 | public void setTransactionTimeout(int seconds) { 47 | 48 | } 49 | 50 | @Override 51 | public Transaction suspend() { 52 | return null; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /testapp/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'io.jd.testapp' 6 | version '0.0.1' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | implementation(project(":framework")) 14 | annotationProcessor(project(":framework")) 15 | 16 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' 17 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' 18 | } 19 | 20 | test { 21 | useJUnitPlatform() 22 | } 23 | -------------------------------------------------------------------------------- /testapp/src/main/java/io/jd/testapp/Event.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public record Event() { 4 | Event addParticipant(Participant participant) { 5 | return new Event(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /testapp/src/main/java/io/jd/testapp/EventId.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public record EventId() { 4 | } 5 | -------------------------------------------------------------------------------- /testapp/src/main/java/io/jd/testapp/EventRepository.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public interface EventRepository { 4 | Event findEvent(EventId eventId); 5 | 6 | void store(Event event); 7 | } 8 | -------------------------------------------------------------------------------- /testapp/src/main/java/io/jd/testapp/EventRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | import jakarta.inject.Singleton; 4 | 5 | @Singleton 6 | public class EventRepositoryImpl implements EventRepository { 7 | 8 | @Override 9 | public void store(Event event) { 10 | } 11 | 12 | @Override 13 | public Event findEvent(EventId eventId) { 14 | return new Event(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /testapp/src/main/java/io/jd/testapp/FrameworkApp.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | import io.jd.framework.BeanProvider; 4 | import io.jd.framework.BeanProviderFactory; 5 | 6 | public class FrameworkApp { 7 | public static void main(String[] args) { 8 | BeanProvider provider = BeanProviderFactory.getInstance(); 9 | ParticipationService participationService = provider.provide(ParticipationService.class); 10 | participationService.participate(new ParticipantId(), new EventId()); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /testapp/src/main/java/io/jd/testapp/ManualTransactionParticipationService.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | import jakarta.inject.Singleton; 4 | import jakarta.transaction.SystemException; 5 | import jakarta.transaction.TransactionManager; 6 | 7 | 8 | @Singleton 9 | public class ManualTransactionParticipationService implements ParticipationService { 10 | private final ParticipantRepository participantRepository; 11 | private final EventRepository eventRepository; 12 | private final TransactionManager transactionManager; 13 | 14 | public ManualTransactionParticipationService(ParticipantRepository participantRepository, EventRepository eventRepository, TransactionManager transactionManager) { 15 | this.participantRepository = participantRepository; 16 | this.eventRepository = eventRepository; 17 | this.transactionManager = transactionManager; 18 | } 19 | 20 | @Override 21 | public void participate(ParticipantId participantId, EventId eventId) { 22 | try { 23 | transactionManager.begin(); 24 | var participant = participantRepository.getParticipant(participantId); 25 | var event = eventRepository.findEvent(eventId); 26 | eventRepository.store(event.addParticipant(participant)); 27 | 28 | System.out.printf("Participant: '%s' takes part in event: '%s'%n", participant, event); 29 | 30 | transactionManager.commit(); 31 | } catch (Exception e) { 32 | rollback(); 33 | throw new RuntimeException(e); 34 | } 35 | } 36 | 37 | private void rollback() { 38 | try { 39 | transactionManager.rollback(); 40 | } catch (SystemException e) { 41 | throw new RuntimeException(e); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /testapp/src/main/java/io/jd/testapp/NoFrameworkApp.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public class NoFrameworkApp { 4 | public static void main(String[] args) { 5 | ParticipationService participationService = new ManualTransactionParticipationService( 6 | new ParticipantRepositoryImpl(), 7 | new EventRepositoryImpl(), 8 | new TransactionalManagerStub() 9 | ); 10 | participationService.participate(new ParticipantId(), new EventId()); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /testapp/src/main/java/io/jd/testapp/Participant.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public record Participant() { 4 | } 5 | -------------------------------------------------------------------------------- /testapp/src/main/java/io/jd/testapp/ParticipantId.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public record ParticipantId() { 4 | } 5 | -------------------------------------------------------------------------------- /testapp/src/main/java/io/jd/testapp/ParticipantRepository.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public interface ParticipantRepository { 4 | Participant getParticipant(ParticipantId participantId); 5 | } 6 | -------------------------------------------------------------------------------- /testapp/src/main/java/io/jd/testapp/ParticipantRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | import jakarta.inject.Singleton; 4 | 5 | @Singleton 6 | public class ParticipantRepositoryImpl implements ParticipantRepository { 7 | @Override 8 | public Participant getParticipant(ParticipantId participantId) { 9 | return new Participant(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /testapp/src/main/java/io/jd/testapp/ParticipationService.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | public interface ParticipationService { 4 | void participate(ParticipantId participantId, EventId eventId); 5 | } 6 | -------------------------------------------------------------------------------- /testapp/src/main/java/io/jd/testapp/TransactionalManagerStub.java: -------------------------------------------------------------------------------- 1 | package io.jd.testapp; 2 | 3 | import jakarta.inject.Singleton; 4 | import jakarta.transaction.Transaction; 5 | import jakarta.transaction.TransactionManager; 6 | 7 | @Singleton 8 | public class TransactionalManagerStub implements TransactionManager { 9 | 10 | @Override 11 | public void begin() { 12 | System.out.println("Begin transaction"); 13 | } 14 | 15 | @Override 16 | public void commit() throws SecurityException, IllegalStateException { 17 | System.out.println("Commit transaction"); 18 | } 19 | 20 | @Override 21 | public void rollback() throws IllegalStateException, SecurityException { 22 | System.out.println("Rollback transaction"); 23 | } 24 | 25 | @Override 26 | public int getStatus() { 27 | return 0; 28 | } 29 | 30 | @Override 31 | public Transaction getTransaction() { 32 | return null; 33 | } 34 | 35 | @Override 36 | public void resume(Transaction tobj) throws IllegalStateException { 37 | 38 | } 39 | 40 | @Override 41 | public void setRollbackOnly() throws IllegalStateException { 42 | 43 | } 44 | 45 | @Override 46 | public void setTransactionTimeout(int seconds) { 47 | 48 | } 49 | 50 | @Override 51 | public Transaction suspend() { 52 | return null; 53 | } 54 | } 55 | --------------------------------------------------------------------------------