├── .idea ├── .gitignore ├── codeStyles │ └── codeStyleConfig.xml ├── vcs.xml ├── google-java-format.xml ├── inspectionProfiles │ └── profiles_settings.xml ├── markdown.xml ├── compiler.xml ├── misc.xml ├── jarRepositories.xml ├── encodings.xml ├── write_your_own_java_framework.iml └── uiDesigner.xml ├── .settings ├── org.eclipse.core.resources.prefs ├── org.eclipse.m2e.core.prefs └── org.eclipse.jdt.core.prefs ├── orm ├── src │ └── main │ │ └── java │ │ └── com │ │ └── github │ │ └── forax │ │ └── framework │ │ └── orm │ │ ├── Repository.java │ │ ├── Id.java │ │ ├── GeneratedValue.java │ │ ├── Table.java │ │ ├── Column.java │ │ ├── Query.java │ │ ├── Utils.java │ │ └── ORM.java ├── pom.xml └── README.md ├── interceptor ├── src │ └── main │ │ └── java │ │ └── org │ │ └── github │ │ └── forax │ │ └── framework │ │ └── interceptor │ │ ├── Invocation.java │ │ ├── Interceptor.java │ │ ├── AroundAdvice.java │ │ ├── Utils.java │ │ └── InterceptorRegistry.java ├── pom.xml ├── CODE_COMMENTS.md └── README.md ├── .gitignore ├── injector ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── github │ │ │ └── forax │ │ │ └── framework │ │ │ └── injector │ │ │ ├── Inject.java │ │ │ ├── Utils2.java │ │ │ ├── Utils.java │ │ │ ├── AnnotationScanner.java │ │ │ └── InjectorRegistry.java │ └── test │ │ └── java │ │ └── com │ │ └── github │ │ └── forax │ │ └── framework │ │ └── injector │ │ └── AnnotationScannerTest.java ├── pom.xml ├── CODE_COMMENTS2.md ├── README2.md ├── CODE_COMMENTS.md └── README.md ├── mapper ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── github │ │ │ └── forax │ │ │ └── framework │ │ │ └── mapper │ │ │ ├── JSONProperty.java │ │ │ ├── JSONWriter.java │ │ │ ├── Utils.java │ │ │ ├── ToyJSONParser.java │ │ │ └── JSONReader.java │ └── test │ │ └── java │ │ └── com │ │ └── github │ │ └── forax │ │ └── framework │ │ └── mapper │ │ ├── ToyJSONParserTest.java │ │ └── JSONWriterTest.java ├── pom.xml ├── README.md ├── CODE_COMMENTS.md ├── README2.md └── CODE_COMMENTS2.md ├── .github └── workflows │ └── maven.yml ├── .project ├── README.md ├── pom.xml ├── .classpath ├── JDBC.md └── LICENSE /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.settings/org.eclipse.core.resources.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | encoding/=UTF-8 3 | -------------------------------------------------------------------------------- /.settings/org.eclipse.m2e.core.prefs: -------------------------------------------------------------------------------- 1 | activeProfiles= 2 | eclipse.preferences.version=1 3 | resolveWorkspaceProjects=true 4 | version=1 5 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/google-java-format.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /orm/src/main/java/com/github/forax/framework/orm/Repository.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.orm; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | public interface Repository { 7 | List findAll(); 8 | Optional findById(ID id); 9 | T save(T entity); 10 | } -------------------------------------------------------------------------------- /interceptor/src/main/java/org/github/forax/framework/interceptor/Invocation.java: -------------------------------------------------------------------------------- 1 | package org.github.forax.framework.interceptor; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | @FunctionalInterface 6 | public interface Invocation { 7 | Object proceed(Object instance, Method method, Object[] args) throws Throwable; 8 | } 9 | -------------------------------------------------------------------------------- /interceptor/src/main/java/org/github/forax/framework/interceptor/Interceptor.java: -------------------------------------------------------------------------------- 1 | package org.github.forax.framework.interceptor; 2 | 3 | import java.lang.reflect.Method; 4 | import java.util.concurrent.Callable; 5 | 6 | @FunctionalInterface 7 | public interface Interceptor { 8 | Object intercept(Object instance, Method method, Object[] args, Invocation invocation) throws Throwable; 9 | } -------------------------------------------------------------------------------- /interceptor/src/main/java/org/github/forax/framework/interceptor/AroundAdvice.java: -------------------------------------------------------------------------------- 1 | package org.github.forax.framework.interceptor; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | public interface AroundAdvice { 6 | void before(Object instance, Method method, Object[] args) throws Throwable; 7 | void after(Object instance, Method method, Object[] args, Object result) throws Throwable; 8 | } 9 | -------------------------------------------------------------------------------- /orm/src/main/java/com/github/forax/framework/orm/Id.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.orm; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.METHOD; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Retention(RUNTIME) 10 | @Target(METHOD) 11 | public @interface Id { } -------------------------------------------------------------------------------- /orm/src/main/java/com/github/forax/framework/orm/GeneratedValue.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.orm; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.METHOD; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Retention(RUNTIME) 10 | @Target(METHOD) 11 | public @interface GeneratedValue {} -------------------------------------------------------------------------------- /orm/src/main/java/com/github/forax/framework/orm/Table.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.orm; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.TYPE; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Retention(RUNTIME) 10 | @Target(TYPE) 11 | public @interface Table { 12 | String value(); 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | target/ 26 | /bin/ 27 | -------------------------------------------------------------------------------- /orm/src/main/java/com/github/forax/framework/orm/Column.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.orm; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.METHOD; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Retention(RUNTIME) 10 | @Target(METHOD) 11 | public @interface Column { 12 | String value(); 13 | } 14 | -------------------------------------------------------------------------------- /orm/src/main/java/com/github/forax/framework/orm/Query.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.orm; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.METHOD; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Retention(RUNTIME) 10 | @Target(METHOD) 11 | public @interface Query { 12 | String value(); 13 | } 14 | -------------------------------------------------------------------------------- /.idea/markdown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /injector/src/main/java/com/github/forax/framework/injector/Inject.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.injector; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.CONSTRUCTOR; 7 | import static java.lang.annotation.ElementType.METHOD; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | @Retention(RUNTIME) 11 | @Target({METHOD, CONSTRUCTOR}) 12 | public @interface Inject { } 13 | -------------------------------------------------------------------------------- /mapper/src/main/java/com/github/forax/framework/mapper/JSONProperty.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.mapper; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.METHOD; 7 | import static java.lang.annotation.ElementType.RECORD_COMPONENT; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | @Retention(RUNTIME) 11 | @Target({METHOD, RECORD_COMPONENT}) 12 | public @interface JSONProperty { 13 | String value(); 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ master ] 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | java: [ 23 ] 13 | name: Java ${{ matrix.java }} 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: setup 17 | uses: actions/setup-java@v1 18 | with: 19 | java-version: ${{ matrix.java }} 20 | - name: build 21 | run: | 22 | mvn -B package 23 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | write_your_own_java_framework 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.m2e.core.maven2Builder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.m2e.core.maven2Nature 21 | org.eclipse.jdt.core.javanature 22 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Write your own java framework 2 | Understand how Spring, JakartaEE, Jackson, Guice and Hibernate works by rewriting a toy version of them 3 | 4 | [Tips and Tricks used in the implementations](COMPANION.md) 5 | 6 | - [Writing objects to JSON](mapper/README.md) ☆ and [Reading objects from JSON](mapper/README2.md) ☆☆☆ 7 | - [Dependency Injection](injector/README.md) ☆ and [Annotation classpath scanning](injector/README2.md) ☆ 8 | - [Interceptor and Aspect Oriented Programming](interceptor/README.md) ☆☆ 9 | - [Object Relational Mapping (ORM)](orm/README.md) ☆☆☆ 10 | 11 | The number of ☆ indicates the implementation complexity (☆ is easier than ☆☆☆). 12 | 13 | -------------------------------------------------------------------------------- /injector/src/main/java/com/github/forax/framework/injector/Utils2.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.injector; 2 | 3 | import java.io.IOException; 4 | import java.net.URL; 5 | import java.util.Enumeration; 6 | 7 | public class Utils2 { 8 | private Utils2() { 9 | throw new AssertionError(); 10 | } 11 | 12 | public static Enumeration getResources(String folderName, ClassLoader classLoader) { 13 | try { 14 | return classLoader.getResources(folderName); 15 | } catch (IOException e) { 16 | throw new IllegalStateException(e); 17 | } 18 | } 19 | 20 | public static Class loadClass(String className, ClassLoader classLoader) { 21 | try { 22 | return Class.forName(className, /*initialize=*/ false, classLoader); 23 | } catch (ClassNotFoundException e) { 24 | throw (NoClassDefFoundError) new NoClassDefFoundError().initCause(e); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled 3 | org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate 4 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=16 5 | org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve 6 | org.eclipse.jdt.core.compiler.compliance=16 7 | org.eclipse.jdt.core.compiler.debug.lineNumber=generate 8 | org.eclipse.jdt.core.compiler.debug.localVariable=generate 9 | org.eclipse.jdt.core.compiler.debug.sourceFile=generate 10 | org.eclipse.jdt.core.compiler.problem.assertIdentifier=error 11 | org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled 12 | org.eclipse.jdt.core.compiler.problem.enumIdentifier=error 13 | org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning 14 | org.eclipse.jdt.core.compiler.release=enabled 15 | org.eclipse.jdt.core.compiler.source=16 16 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /orm/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | java-framework 7 | com.github.forax.framework 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | com.github.forax.framework 13 | orm 14 | 1.0-SNAPSHOT 15 | 16 | 17 | 18 | 19 | org.apache.maven.plugins 20 | maven-compiler-plugin 21 | 22 | 25 23 | 25 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /mapper/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | java-framework 7 | com.github.forax.framework 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | com.github.forax.framework 13 | mapper 14 | 1.0-SNAPSHOT 15 | 16 | 17 | 18 | 19 | org.apache.maven.plugins 20 | maven-compiler-plugin 21 | 22 | 25 23 | 25 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /injector/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | java-framework 7 | com.github.forax.framework 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | com.github.forax.framework 13 | injector 14 | 1.0-SNAPSHOT 15 | 16 | 17 | 18 | 19 | org.apache.maven.plugins 20 | maven-compiler-plugin 21 | 22 | 25 23 | 25 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /interceptor/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | java-framework 7 | com.github.forax.framework 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | com.github.forax.framework 13 | interceptor 14 | 1.0-SNAPSHOT 15 | 16 | 17 | 18 | 19 | org.apache.maven.plugins 20 | maven-compiler-plugin 21 | 22 | 25 23 | 25 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /interceptor/src/main/java/org/github/forax/framework/interceptor/Utils.java: -------------------------------------------------------------------------------- 1 | package org.github.forax.framework.interceptor; 2 | 3 | import java.lang.reflect.InvocationTargetException; 4 | import java.lang.reflect.Method; 5 | import java.util.AbstractList; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.RandomAccess; 10 | 11 | final class Utils { 12 | private Utils() { 13 | throw new AssertionError(); 14 | } 15 | 16 | public static Object invokeMethod(Object object, Method method, Object... args) { 17 | try { 18 | return method.invoke(object, args); 19 | } catch (IllegalArgumentException e) { 20 | throw new AssertionError("can not call " + method + " on " + object + " with " + Arrays.toString(args), e); 21 | } catch (IllegalAccessException e) { 22 | throw (IllegalAccessError) new IllegalAccessError().initCause(e); 23 | } catch (InvocationTargetException e) { 24 | throw rethrow(e.getCause()); 25 | } 26 | } 27 | 28 | @SuppressWarnings("unchecked") // very wrong but works 29 | private static AssertionError rethrow(Throwable cause) throws T { 30 | throw (T) cause; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.idea/write_your_own_java_framework.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.github.forax.framework 5 | java-framework 6 | pom 7 | 1.0-SNAPSHOT 8 | java-framework 9 | http://maven.apache.org 10 | 11 | 12 | UTF-8 13 | 14 | 15 | 16 | mapper 17 | injector 18 | interceptor 19 | orm 20 | 21 | 22 | 23 | 24 | org.junit.jupiter 25 | junit-jupiter-api 26 | 5.13.4 27 | test 28 | 29 | 30 | org.junit.jupiter 31 | junit-jupiter-engine 32 | 5.13.4 33 | test 34 | 35 | 36 | com.h2database 37 | h2 38 | 1.4.200 39 | 40 | 41 | 42 | 43 | 44 | 45 | org.apache.maven.plugins 46 | maven-compiler-plugin 47 | 3.14.0 48 | 49 | 25 50 | 51 | 52 | 53 | 54 | org.apache.maven.plugins 55 | maven-surefire-plugin 56 | 3.5.3 57 | 58 | --enable-preview 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /orm/src/main/java/com/github/forax/framework/orm/Utils.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.orm; 2 | 3 | import java.beans.BeanInfo; 4 | import java.beans.IntrospectionException; 5 | import java.beans.Introspector; 6 | import java.lang.reflect.Constructor; 7 | import java.lang.reflect.InvocationTargetException; 8 | import java.lang.reflect.Method; 9 | 10 | final class Utils { 11 | private Utils() { 12 | throw new AssertionError(); 13 | } 14 | 15 | public static BeanInfo beanInfo(Class beanType) { 16 | try { 17 | return Introspector.getBeanInfo(beanType); 18 | } catch (IntrospectionException e) { 19 | throw new IllegalStateException(e); 20 | } 21 | } 22 | 23 | public static Constructor defaultConstructor(Class beanType) { 24 | try { 25 | return beanType.getConstructor(); 26 | } catch (NoSuchMethodException e) { 27 | throw (NoSuchMethodError) new NoSuchMethodError("no public default constructor").initCause(e); 28 | } 29 | } 30 | 31 | public static Object invokeMethod(Object bean, Method method, Object... args) { 32 | try { 33 | return method.invoke(bean, args); 34 | } catch (IllegalArgumentException e) { 35 | throw new AssertionError(e); 36 | } catch (IllegalAccessException e) { 37 | throw (IllegalAccessError) new IllegalAccessError().initCause(e); 38 | } catch (InvocationTargetException e) { 39 | throw rethrow(e.getCause()); 40 | } 41 | } 42 | 43 | public static T newInstance(Constructor constructor, Object... args) { 44 | try { 45 | return constructor.newInstance(args); 46 | } catch (IllegalArgumentException e) { 47 | throw new AssertionError(e); 48 | } catch (InstantiationException e) { 49 | throw (InstantiationError) new InstantiationError().initCause(e); 50 | } catch (IllegalAccessException e) { 51 | throw (IllegalAccessError) new IllegalAccessError().initCause(e); 52 | } catch (InvocationTargetException e) { 53 | throw rethrow(e.getCause()); 54 | } 55 | } 56 | 57 | @SuppressWarnings("unchecked") // very wrong but works 58 | static AssertionError rethrow(Throwable cause) throws T { 59 | throw (T) cause; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /injector/src/main/java/com/github/forax/framework/injector/Utils.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.injector; 2 | 3 | import java.beans.BeanInfo; 4 | import java.beans.IntrospectionException; 5 | import java.beans.Introspector; 6 | import java.lang.reflect.Constructor; 7 | import java.lang.reflect.InvocationTargetException; 8 | import java.lang.reflect.Method; 9 | import java.lang.reflect.UndeclaredThrowableException; 10 | 11 | final class Utils { 12 | private Utils() { 13 | throw new AssertionError(); 14 | } 15 | 16 | public static BeanInfo beanInfo(Class beanType) { 17 | try { 18 | return Introspector.getBeanInfo(beanType); 19 | } catch (IntrospectionException e) { 20 | throw new IllegalStateException(e); 21 | } 22 | } 23 | 24 | public static void invokeMethod(Object instance, Method method, Object... args) { 25 | try { 26 | method.invoke(instance, args); 27 | } catch (IllegalArgumentException e) { 28 | throw new AssertionError(e); 29 | } catch (IllegalAccessException e) { 30 | throw (IllegalAccessError) new IllegalAccessError().initCause(e); 31 | } catch (InvocationTargetException e) { 32 | throw rethrow(e.getCause()); 33 | } 34 | } 35 | 36 | public static Constructor defaultConstructor(Class beanType) { 37 | try { 38 | return beanType.getConstructor(); 39 | } catch (NoSuchMethodException e) { 40 | throw (NoSuchMethodError) new NoSuchMethodError("no public default constructor " + beanType.getName()).initCause(e); 41 | } 42 | } 43 | 44 | public static T newInstance(Constructor constructor, Object... args) { 45 | try { 46 | return constructor.newInstance(args); 47 | } catch (IllegalArgumentException e) { 48 | throw new AssertionError(e); 49 | } catch (InstantiationException e) { 50 | throw (InstantiationError) new InstantiationError().initCause(e); 51 | } catch (IllegalAccessException e) { 52 | throw (IllegalAccessError) new IllegalAccessError().initCause(e); 53 | } catch (InvocationTargetException e) { 54 | throw rethrow(e.getCause()); 55 | } 56 | } 57 | 58 | @SuppressWarnings("unchecked") // very wrong but works 59 | private static AssertionError rethrow(Throwable cause) throws T { 60 | throw (T) cause; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /injector/CODE_COMMENTS2.md: -------------------------------------------------------------------------------- 1 | # Code comments 2 | 3 | 4 | ### Q1 5 | 6 | ```java 7 | static Stream findAllJavaFilesInFolder(Path folder) throws IOException{ 8 | return Files.list(folder) 9 | .map(path -> path.getFileName().toString()) 10 | .filter(filename -> filename.endsWith(".class")) 11 | .map(filename -> filename.substring(0, filename.length() - ".class".length())); 12 | } 13 | ``` 14 | 15 | 16 | ### Q2 17 | 18 | ```java 19 | static List> findAllClasses(String packageName, ClassLoader classLoader) { 20 | var urls = Utils2.getResources(packageName.replace('.', '/'), classLoader); 21 | var urlList = Collections.list(urls); 22 | if (urlList.isEmpty()) { 23 | throw new IllegalStateException("no folder for package " + packageName + " found"); 24 | } 25 | return urlList.stream() 26 | .flatMap(url -> { 27 | try { 28 | var folder = Path.of(url.toURI()); 29 | return findAllJavaFilesInFolder(folder) 30 | .>map(className -> Utils2.loadClass(packageName + '.' + className, classLoader)); 31 | } catch (IOException | URISyntaxException e) { 32 | throw new IllegalStateException("error while looking for classes of package " + packageName, e); 33 | } 34 | }) 35 | .toList(); 36 | } 37 | ``` 38 | 39 | 40 | ### Q3 41 | 42 | ```java 43 | private final HashMap, Consumer>> actionMap = new HashMap<>(); 44 | 45 | public void addAction(Class annotationClass, Consumer> action) { 46 | Objects.requireNonNull(annotationClass); 47 | Objects.requireNonNull(action); 48 | var result = actionMap.putIfAbsent(annotationClass, action); 49 | if (result != null) { 50 | throw new IllegalStateException("an action is already registered for annotation " + annotationClass); 51 | } 52 | } 53 | ``` 54 | 55 | 56 | ### Q4 57 | 58 | ```java 59 | public void scanClassPathPackageForAnnotations(Class classInPackage) { 60 | Objects.requireNonNull(classInPackage); 61 | var packageName = classInPackage.getPackageName(); 62 | var classLoader = classInPackage.getClassLoader(); 63 | for(var clazz: findAllClasses(packageName, classLoader)) { 64 | for (var annotation : clazz.getAnnotations()) { 65 | actionMap.getOrDefault(annotation.annotationType(), __ -> {}).accept(clazz); 66 | } 67 | } 68 | } 69 | ``` 70 | -------------------------------------------------------------------------------- /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /injector/src/main/java/com/github/forax/framework/injector/AnnotationScanner.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.injector; 2 | 3 | import java.io.IOException; 4 | import java.lang.annotation.Annotation; 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | import java.lang.reflect.Constructor; 10 | import java.lang.reflect.Modifier; 11 | import java.net.URISyntaxException; 12 | import java.nio.file.Files; 13 | import java.nio.file.Path; 14 | import java.util.Arrays; 15 | import java.util.Collections; 16 | import java.util.Comparator; 17 | import java.util.HashMap; 18 | import java.util.HashSet; 19 | import java.util.LinkedHashSet; 20 | import java.util.List; 21 | import java.util.Objects; 22 | import java.util.Set; 23 | import java.util.function.Consumer; 24 | import java.util.stream.Stream; 25 | 26 | public class AnnotationScanner { 27 | // package for testing 28 | static Stream findAllJavaFilesInFolder(Path folder) throws IOException{ 29 | return Files.list(folder) 30 | .map(path -> path.getFileName().toString()) 31 | .filter(filename -> filename.endsWith(".class")) 32 | .map(filename -> filename.substring(0, filename.length() - ".class".length())); 33 | } 34 | 35 | // package for testing 36 | static List> findAllClasses(String packageName, ClassLoader classLoader) { 37 | var urls = Utils2.getResources(packageName.replace('.', '/'), classLoader); 38 | var urlList = Collections.list(urls); 39 | if (urlList.isEmpty()) { 40 | throw new IllegalStateException("no folder for package " + packageName + " found"); 41 | } 42 | return urlList.stream() 43 | .flatMap(url -> { 44 | try { 45 | var folder = Path.of(url.toURI()); 46 | return findAllJavaFilesInFolder(folder) 47 | .>map(className -> Utils2.loadClass(packageName + '.' + className, classLoader)); 48 | } catch (IOException | URISyntaxException e) { 49 | throw new IllegalStateException("error while looking for classes of package " + packageName, e); 50 | } 51 | }) 52 | .toList(); 53 | } 54 | 55 | private final HashMap, Consumer>> actionMap = new HashMap<>(); 56 | 57 | public void addAction(Class annotationClass, Consumer> action) { 58 | Objects.requireNonNull(annotationClass); 59 | Objects.requireNonNull(action); 60 | var result = actionMap.putIfAbsent(annotationClass, action); 61 | if (result != null) { 62 | throw new IllegalStateException("an action is already registered for annotation " + annotationClass); 63 | } 64 | } 65 | 66 | public void scanClassPathPackageForAnnotations(Class classInPackage) { 67 | Objects.requireNonNull(classInPackage); 68 | var packageName = classInPackage.getPackageName(); 69 | var classLoader = classInPackage.getClassLoader(); 70 | for(var clazz: findAllClasses(packageName, classLoader)) { 71 | for (var annotation : clazz.getAnnotations()) { 72 | actionMap.getOrDefault(annotation.annotationType(), __ -> {}).accept(clazz); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /mapper/src/main/java/com/github/forax/framework/mapper/JSONWriter.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.mapper; 2 | 3 | import java.beans.IntrospectionException; 4 | import java.beans.PropertyDescriptor; 5 | import java.util.Arrays; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Objects; 9 | import java.util.function.Function; 10 | 11 | import static java.util.stream.Collectors.joining; 12 | 13 | public final class JSONWriter { 14 | private interface Generator { 15 | String generate(JSONWriter writer, Object object); 16 | } 17 | 18 | private static final ClassValue GENERATOR_CLASS_VALUE = new ClassValue<>() { 19 | @Override 20 | protected Generator computeValue(Class type) { 21 | var properties = type.isRecord()? recordProperties(type): beanProperties(type); 22 | var generators = properties.stream() 23 | .map(property -> { 24 | var getter = property.getReadMethod(); 25 | var propertyAnnotation = getter.getAnnotation(JSONProperty.class); 26 | var propertyName = propertyAnnotation == null? property.getName(): propertyAnnotation.value(); 27 | var key = "\"" + propertyName + "\": "; 28 | return (writer, o) -> key + writer.toJSON(Utils.invokeMethod(o, getter)); 29 | }) 30 | .toList(); 31 | return (writer, object) -> generators.stream() 32 | .map(generator -> generator.generate(writer, object)) 33 | .collect(joining(", ", "{", "}")); 34 | } 35 | }; 36 | 37 | private static List beanProperties(Class type) { 38 | var beanInfo = Utils.beanInfo(type); 39 | return Arrays.stream(beanInfo.getPropertyDescriptors()) 40 | .filter(property -> !property.getName().equals("class")) 41 | .toList(); 42 | } 43 | 44 | private static List recordProperties(Class type) { 45 | return Arrays.stream(type.getRecordComponents()) 46 | .map(component -> { 47 | try { 48 | return new PropertyDescriptor(component.getName(), component.getAccessor(), null); 49 | } catch (IntrospectionException e) { 50 | throw new AssertionError(e); 51 | } 52 | }) 53 | .toList(); 54 | } 55 | 56 | private final HashMap, Generator> map = new HashMap<>(); 57 | 58 | public void configure(Class type, Function function) { 59 | Objects.requireNonNull(type); 60 | Objects.requireNonNull(function); 61 | var result = map.putIfAbsent(type, (_, object) -> function.apply(type.cast(object))); 62 | if (result != null) { 63 | throw new IllegalStateException("already a function registered for type " + type.getName()); 64 | } 65 | } 66 | 67 | public String toJSON(Object o) { 68 | return switch (o) { 69 | case null -> "null"; 70 | case Boolean _, Integer _, Long _, Float _, Double _ -> o.toString(); 71 | case String value -> "\"" + value + "\""; 72 | default -> { 73 | var type = o.getClass(); 74 | var generator = map.get(type); 75 | if (generator == null) { 76 | generator = GENERATOR_CLASS_VALUE.get(type); 77 | } 78 | yield generator.generate(this, o); 79 | } 80 | }; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /mapper/src/main/java/com/github/forax/framework/mapper/Utils.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.mapper; 2 | 3 | import java.beans.BeanInfo; 4 | import java.beans.IntrospectionException; 5 | import java.beans.Introspector; 6 | import java.lang.reflect.Constructor; 7 | import java.lang.reflect.GenericArrayType; 8 | import java.lang.reflect.InvocationTargetException; 9 | import java.lang.reflect.Method; 10 | import java.lang.reflect.ParameterizedType; 11 | import java.lang.reflect.RecordComponent; 12 | import java.lang.reflect.Type; 13 | import java.lang.reflect.TypeVariable; 14 | import java.lang.reflect.WildcardType; 15 | import java.util.AbstractList; 16 | import java.util.Arrays; 17 | import java.util.List; 18 | import java.util.RandomAccess; 19 | 20 | final class Utils { 21 | private Utils() { 22 | throw new AssertionError(); 23 | } 24 | 25 | public static BeanInfo beanInfo(Class beanType) { 26 | try { 27 | return Introspector.getBeanInfo(beanType); 28 | } catch (IntrospectionException e) { 29 | throw new IllegalStateException(e); 30 | } 31 | } 32 | 33 | public static Object invokeMethod(Object instance, Method method, Object... args) { 34 | try { 35 | return method.invoke(instance, args); 36 | } catch (IllegalArgumentException e) { 37 | throw new AssertionError(e); 38 | } catch (IllegalAccessException e) { 39 | throw (IllegalAccessError) new IllegalAccessError().initCause(e); 40 | } catch (InvocationTargetException e) { 41 | throw rethrow(e.getCause()); 42 | } 43 | } 44 | 45 | @SuppressWarnings("unchecked") // very wrong but works 46 | private static AssertionError rethrow(Throwable cause) throws T { 47 | throw (T) cause; 48 | } 49 | 50 | public static Constructor defaultConstructor(Class beanType) { 51 | try { 52 | return beanType.getConstructor(); 53 | } catch (NoSuchMethodException e) { 54 | throw (NoSuchMethodError) new NoSuchMethodError("no public default constructor " + beanType.getName()).initCause(e); 55 | } 56 | } 57 | 58 | public static Constructor canonicalConstructor(Class recordClass, RecordComponent[] components) { 59 | try { 60 | return recordClass.getConstructor(Arrays.stream(components).map(RecordComponent::getType).toArray(Class[]::new)); 61 | } catch (NoSuchMethodException e) { 62 | throw (NoSuchMethodError) new NoSuchMethodError("no public canonical constructor " + recordClass.getName()).initCause(e); 63 | } 64 | } 65 | 66 | public static T newInstance(Constructor constructor, Object... args) { 67 | try { 68 | return constructor.newInstance(args); 69 | } catch (IllegalArgumentException e) { 70 | throw new AssertionError(e); 71 | } catch (InstantiationException e) { 72 | throw (InstantiationError) new InstantiationError().initCause(e); 73 | } catch (IllegalAccessException e) { 74 | throw (IllegalAccessError) new IllegalAccessError().initCause(e); 75 | } catch (InvocationTargetException e) { 76 | throw rethrow(e.getCause()); 77 | } 78 | } 79 | 80 | public static Class erase(Type type) { 81 | return switch (type) { 82 | case Class clazz -> clazz; 83 | case ParameterizedType parameterizedType -> erase(parameterizedType.getRawType()); 84 | case GenericArrayType genericArrayType -> erase(genericArrayType.getGenericComponentType()).arrayType(); 85 | case TypeVariable typeVariable -> erase(typeVariable.getBounds()[0]); 86 | case WildcardType wildcardType -> erase(wildcardType.getLowerBounds()[0]); 87 | default -> throw new AssertionError("unknown type " + type.getTypeName()); 88 | }; 89 | } 90 | } -------------------------------------------------------------------------------- /mapper/src/test/java/com/github/forax/framework/mapper/ToyJSONParserTest.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.mapper; 2 | 3 | import com.github.forax.framework.mapper.ToyJSONParser.JSONVisitor; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.ArrayDeque; 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertAll; 14 | import static org.junit.jupiter.api.Assertions.assertEquals; 15 | 16 | class ToyJSONParserTest { 17 | private static Object asJava(String text) { 18 | var visitor = new JSONVisitor() { 19 | private Object result; 20 | private final ArrayDeque stack = new ArrayDeque<>(); 21 | 22 | @Override 23 | @SuppressWarnings("unchecked") 24 | public void value(String key, Object value) { 25 | var data = stack.peek(); 26 | if (data instanceof Map map) { 27 | ((Map) map).put(key, value); 28 | return; 29 | } 30 | if (data instanceof List list) { 31 | ((List) list).add(value); 32 | return; 33 | } 34 | throw new AssertionError(); 35 | } 36 | 37 | @Override 38 | public void startObject(String key) { 39 | stack.push(new HashMap()); 40 | } 41 | 42 | @Override 43 | public void endObject(String key) { 44 | var data = stack.pop(); 45 | if (stack.isEmpty()) { 46 | result = data; 47 | } else { 48 | value(key, data); 49 | } 50 | } 51 | 52 | @Override 53 | public void startArray(String key) { 54 | stack.push(new ArrayList<>()); 55 | } 56 | 57 | @Override 58 | public void endArray(String key) { 59 | var data = stack.pop(); 60 | if (stack.isEmpty()) { 61 | result = data; 62 | } else { 63 | value(key, data); 64 | } 65 | } 66 | }; 67 | ToyJSONParser.parse(text, visitor); 68 | return visitor.result; 69 | } 70 | 71 | @Test 72 | public void parseObjects() { 73 | assertAll( 74 | () -> assertEquals(Map.of(), asJava("{}")), 75 | () -> assertEquals(Map.of(), asJava("{ }")), 76 | () -> assertEquals(Map.of( 77 | "key2", false, 78 | "key3", true, 79 | "key4", 123, 80 | "key5", 145.4, 81 | "key6", "string" 82 | ), asJava(""" 83 | { 84 | "key2": false, 85 | "key3": true, 86 | "key4": 123, 87 | "key5": 145.4, 88 | "key6": "string" 89 | } 90 | """)), 91 | () -> assertEquals(Map.of("foo", "bar"), asJava(""" 92 | { 93 | "foo": "bar" 94 | } 95 | """)), 96 | () -> assertEquals(Map.of("foo", "bar", "bob-one", 42), asJava(""" 97 | { 98 | "foo": "bar", 99 | "bob-one": 42 100 | } 101 | """)) 102 | ); 103 | } 104 | 105 | @Test 106 | public void parseObjectsWithNull() { 107 | assertEquals(new HashMap() {{ 108 | put("foo", null); 109 | }}, asJava(""" 110 | { 111 | "foo": null 112 | } 113 | """)); 114 | } 115 | 116 | @Test 117 | public void parseArrays() { 118 | assertAll( 119 | () -> assertEquals(List.of(), asJava("[]")), 120 | () -> assertEquals(List.of(), asJava("[ ]")), 121 | () -> assertEquals( 122 | List.of(false,true,123,145.4,"string"), 123 | asJava(""" 124 | [ 125 | false, true, 123, 145.4, "string" 126 | ] 127 | """)), 128 | () -> assertEquals(List.of("foo", "bar"), asJava(""" 129 | [ 130 | "foo", 131 | "bar" 132 | ] 133 | """)), 134 | () -> assertEquals(List.of("foo", "bar", "bob-one", 42), asJava(""" 135 | [ "foo", "bar", "bob-one", 42 ]\ 136 | """)) 137 | ); 138 | } 139 | 140 | @Test 141 | public void parseArraysWithNull() { 142 | assertEquals(Arrays.asList(13.4, null), asJava(""" 143 | [ 13.4, null ] 144 | """)); 145 | } 146 | } -------------------------------------------------------------------------------- /interceptor/src/main/java/org/github/forax/framework/interceptor/InterceptorRegistry.java: -------------------------------------------------------------------------------- 1 | package org.github.forax.framework.interceptor; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.lang.reflect.Method; 5 | import java.lang.reflect.Proxy; 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Objects; 11 | import java.util.stream.Stream; 12 | 13 | public final class InterceptorRegistry { 14 | /* 15 | private final HashMap, List> adviceMap = new HashMap<>(); 16 | 17 | public void addAroundAdvice(Class annotationClass, AroundAdvice advice) { 18 | Objects.requireNonNull(annotationClass, "annotationClass is null"); 19 | Objects.requireNonNull(advice, "advice is null"); 20 | adviceMap.computeIfAbsent(annotationClass, __ -> new ArrayList<>()).add(advice); 21 | } 22 | 23 | // package private for test 24 | List findAdvices(Method method) { 25 | return Arrays.stream(method.getAnnotations()) 26 | .flatMap(annotation -> adviceMap.getOrDefault(annotation.annotationType(), List.of()).stream()) 27 | .toList(); 28 | } 29 | 30 | public T createProxy(Class type, T instance) { 31 | Objects.requireNonNull(type, "type is null"); 32 | Objects.requireNonNull(instance, "instance is null"); 33 | return type.cast(Proxy.newProxyInstance(type.getClassLoader(), 34 | new Class[] { type }, 35 | (proxy, method, args) -> { 36 | var advices = findAdvices(method); 37 | for (var advice: advices) { 38 | advice.pre(instance, method, args); 39 | } 40 | var result = Utils.invokeMethod(instance, method, args); 41 | for (var advice: advices) { 42 | advice.post(instance, method, args, result); 43 | } 44 | return result; 45 | })); 46 | }*/ 47 | 48 | 49 | private final HashMap, List> interceptorMap = new HashMap<>(); 50 | private final HashMap invocationCache = new HashMap<>(); 51 | 52 | public void addAroundAdvice(Class annotationClass, AroundAdvice advice) { 53 | Objects.requireNonNull(annotationClass, "annotationClass is null"); 54 | Objects.requireNonNull(advice, "advice is null"); 55 | addInterceptor(annotationClass, (instance, method, args, invocation) -> { 56 | advice.before(instance, method, args); 57 | var result = invocation.proceed(instance, method, args); 58 | advice.after(instance, method, args, result); 59 | return result; 60 | }); 61 | } 62 | 63 | public void addInterceptor(Class annotationClass, Interceptor interceptor) { 64 | Objects.requireNonNull(annotationClass, "annotationClass is null"); 65 | Objects.requireNonNull(interceptor, "interceptor is null"); 66 | interceptorMap.computeIfAbsent(annotationClass, __ -> new ArrayList<>()).add(interceptor); 67 | invocationCache.clear(); 68 | } 69 | 70 | // package private 71 | List findInterceptors(Method method) { 72 | return Stream.of( 73 | Arrays.stream(method.getDeclaringClass().getAnnotations()), 74 | Arrays.stream(method.getAnnotations()), 75 | Arrays.stream(method.getParameterAnnotations()).flatMap(Arrays::stream)) 76 | .flatMap(s -> s) 77 | .map(Annotation::annotationType) 78 | .distinct() 79 | .flatMap(annotationType -> interceptorMap.getOrDefault(annotationType, List.of()).stream()) 80 | .toList(); 81 | } 82 | 83 | // package private 84 | static Invocation getInvocation(List interceptors) { 85 | return interceptors.reversed().stream() 86 | .reduce(Utils::invokeMethod, 87 | (invocation, interceptor) -> (instance, method, args) -> interceptor.intercept(instance, method, args, invocation), 88 | (_1, _2) -> { throw new AssertionError(); }); 89 | } 90 | 91 | private Invocation getInvocationFromCache(Method method) { 92 | return invocationCache.computeIfAbsent(method, m -> getInvocation(findInterceptors(m))); 93 | } 94 | 95 | public T createProxy(Class type, T instance) { 96 | Objects.requireNonNull(type, "type is null"); 97 | Objects.requireNonNull(instance, "instance is null"); 98 | return type.cast(Proxy.newProxyInstance(type.getClassLoader(), 99 | new Class[] { type }, 100 | (proxy, method, args) -> getInvocationFromCache(method).proceed(instance, method, args))); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /injector/src/main/java/com/github/forax/framework/injector/InjectorRegistry.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.injector; 2 | 3 | import com.sun.source.tree.Tree; 4 | 5 | import java.beans.PropertyDescriptor; 6 | import java.io.IOException; 7 | import java.io.UncheckedIOException; 8 | import java.lang.module.ModuleFinder; 9 | import java.lang.reflect.Constructor; 10 | import java.lang.reflect.Modifier; 11 | import java.net.URISyntaxException; 12 | import java.net.URL; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | import java.util.Arrays; 16 | import java.util.Comparator; 17 | import java.util.Enumeration; 18 | import java.util.HashMap; 19 | import java.util.HashSet; 20 | import java.util.LinkedHashSet; 21 | import java.util.List; 22 | import java.util.Objects; 23 | import java.util.Optional; 24 | import java.util.Spliterators; 25 | import java.util.TreeSet; 26 | import java.util.function.Supplier; 27 | import java.util.stream.Stream; 28 | import java.util.stream.StreamSupport; 29 | 30 | import static java.util.Objects.requireNonNull; 31 | 32 | public final class InjectorRegistry { 33 | public InjectorRegistry() { } 34 | 35 | private final HashMap, Supplier> map = new HashMap<>(); 36 | 37 | public void registerInstance(Class type, T instance) { 38 | Objects.requireNonNull(type); 39 | Objects.requireNonNull(instance); 40 | registerProvider(type, () -> instance); 41 | } 42 | 43 | public void registerProvider(Class type, Supplier provider) { 44 | Objects.requireNonNull(type); 45 | Objects.requireNonNull(provider); 46 | var result = map.putIfAbsent(type, provider); 47 | if (result != null) { 48 | throw new IllegalStateException("provider of " + type.getName() + " already registered"); 49 | } 50 | } 51 | 52 | private Supplier lookupProvider(Class type) { 53 | var provider = map.get(type); 54 | if (provider == null) { 55 | throw new IllegalStateException("no provider of " + type.getName()); 56 | } 57 | return provider; 58 | } 59 | 60 | public T lookupInstance(Class type) { 61 | Objects.requireNonNull(type); 62 | var provider = lookupProvider(type); 63 | return type.cast(provider.get()); 64 | } 65 | 66 | // package private for testing 67 | static List findInjectableProperties(Class type) { 68 | var beanInfo = Utils.beanInfo(type); 69 | return Arrays.stream(beanInfo.getPropertyDescriptors()) 70 | .filter(property -> { 71 | var setter = property.getWriteMethod(); 72 | return setter != null && setter.isAnnotationPresent(Inject.class); 73 | }) 74 | .toList(); 75 | } 76 | 77 | private void initInstance(Object instance, List properties) { 78 | for(var property: properties) { 79 | var setter = property.getWriteMethod(); 80 | var value = lookupInstance(property.getPropertyType()); 81 | Utils.invokeMethod(instance, setter, value); 82 | } 83 | } 84 | 85 | private static Optional> findConstructorAnnotatedWithInject(Class providerClass) { 86 | var constructors = Arrays.stream(providerClass.getConstructors()) 87 | .filter(constructor -> constructor.isAnnotationPresent(Inject.class)) 88 | .toList(); 89 | return switch(constructors.size()) { 90 | case 0 -> Optional.empty(); 91 | case 1 -> Optional.of(constructors.getFirst()); 92 | default -> throw new IllegalStateException("more than one constructor annotated with @Inject in " + providerClass.getName()); 93 | }; 94 | } 95 | 96 | public void registerProviderClass(Class type, Class providerClass) { 97 | requireNonNull(type); 98 | requireNonNull(providerClass); 99 | var properties = findInjectableProperties(providerClass); 100 | var constructor = findConstructorAnnotatedWithInject(providerClass) 101 | .orElseGet(() -> Utils.defaultConstructor(providerClass)); 102 | var parameterTypes = constructor.getParameterTypes(); 103 | registerProvider(type, () -> { 104 | Object[] args = Arrays.stream(parameterTypes) 105 | .map(this::lookupInstance) 106 | .toArray(); 107 | var instance = type.cast(Utils.newInstance(constructor, args)); 108 | initInstance(instance, properties); 109 | return instance; 110 | }); 111 | } 112 | 113 | public void registerProviderClass(Class providerClass) { 114 | registerProviderClassImpl(providerClass); 115 | } 116 | 117 | private void registerProviderClassImpl(Class providerClass) { 118 | registerProviderClass(providerClass, providerClass); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /injector/README2.md: -------------------------------------------------------------------------------- 1 | # Annotation classpath scanning 2 | 3 | This is the part 2 of the implementation of an injector, 4 | it reuses the code written for the [part 1](README.md) (only in the tests). 5 | 6 | The idea of the classpath scanning is to find all classes of a package annotated with some pre-registered annotation 7 | types, and auto-automatically execute the action corresponding to the annotation on those classes. 8 | 9 | Here is an example, let suppose we have an annotation `Component` defined like this 10 | ```java 11 | @Target(ElementType.TYPE) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | public @interface Component { 14 | } 15 | ``` 16 | 17 | Then we have two classes `Service`and `Dependency` annotated with `@Component` defined as such 18 | ```java 19 | @Component 20 | public class Service { 21 | private final Dependency dependency; 22 | 23 | @Inject 24 | public Service(Dependency dependency) { 25 | this.dependency = Objects.requireNonNull(dependency); 26 | } 27 | 28 | public Dependency getDependency() { 29 | return dependency; 30 | } 31 | } 32 | 33 | @Component 34 | public class Dependency { 35 | } 36 | ``` 37 | 38 | An annotation scanner is a class that will scan all the classes of the package, here all the classes of the package 39 | containing the class `Application` and for each class annotated with `@Component` execute the action, here, 40 | register the class as [provider class](README.md#our-injector) in the registry. 41 | 42 | So when calling `lookupInstance` on the registry, the registry knows how to create a `Service`and a `Dependency`. 43 | 44 | ```java 45 | public class Application { 46 | public static void main(String[] args) { 47 | var registry = new InjectorRegistry(); 48 | var scanner = new AnnotationScanner(); 49 | scanner.addAction(Component.class, registry::registerProviderClass); 50 | scanner.scanClassPathPackageForAnnotations(Application.class); 51 | var service = registry.lookupInstance(Service.class); 52 | } 53 | } 54 | ``` 55 | 56 | ### What do I need to implement it ? 57 | 58 | For the implementation, 59 | [ClassLoader.getResources()](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/ClassLoader.html#getResources(java.lang.String)) 60 | with as parameter a package name with the dots ('.') replaced by slash ('/') returns all the `URL`s of the folders 61 | containing the classes (you can have more than one folder, by example one folder in src and one folder in test). 62 | 63 | [Class.forName](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Class.html#forName(java.lang.String,boolean,java.lang.ClassLoader))(name, /*initialize=*/ false, classloader) 64 | loads a `Class`without initializing the class (without running its static block). 65 | 66 | Those two methods are already available in the class [Utils2.java](src/main/java/com/github/forax/framework/injector/Utils2.java) 67 | with the exceptions correctly managed. 68 | 69 | 70 | ## Let's implement it 71 | 72 | 1. First, we want to implement a method `findAllJavaFilesInFolder(path)` (not public) that takes a `Path` and returns 73 | the name of all the Java class in the folder. A Java class is file with a filename that ends with ".class". 74 | The name of a Java class is the name of the file without the extension ".class". 75 | Implement the method `findAllJavaFilesInFolder` and 76 | check that the tests in the nested class "Q1" all pass. 77 | 78 | 2. Then we want to implement a method `findAllClasses(packageName, classloader)` (not public) that return a list 79 | of the classes (the `Class`) contained in the package `packageName` loaded by the `classloader` 80 | taken as argument. 81 | Implement the method `findAllClasses` and check that the tests in the nested class "Q2" all pass. 82 | 83 | Note: `Utils2.getResources(packageName.replace('.', '/'), classLoader)` returns the URLs of the folders that 84 | contains the class files of a package. From a URL, you can get a URI and you can construct a Path 85 | from a URI. `Utils2.loadClass(packageName + '.' + className, classLoader)` loads a class 86 | from the qualified name of a class (the name with '.' in it). 87 | 88 | 3. We now want to add the method `addAction(annotationClass, action)` that take `annotationClass` the class of 89 | an annotation and `action` a function that takes a class and return `void`. 90 | `addAction` register the action for the annotation class and only one action can be registered 91 | for an annotation class 92 | Implement the method `addAction` and check that the tests in the nested class "Q3" all pass. 93 | 94 | 4. We want to implement the method `scanClassPathPackageForAnnotations(class)` 95 | that takes a class as parameter, find the corresponding package, load all the class from the folders 96 | corresponding to the package, find all annotations of the classes and run the action each time 97 | corresponding to the annotation classes. 98 | Implement the method `scanClassPathPackageForAnnotations` and check that the tests in the nested class "Q4" all pass. 99 | 100 | Note: there is a method 101 | [getPackageName()](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Class.html#getPackageName()) 102 | to find the name of a package of a class and a method 103 | [getClassLoader](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Class.html#getClassLoader()) 104 | to get the classloader of a class. 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /mapper/README.md: -------------------------------------------------------------------------------- 1 | # Writing objects to JSON 2 | 3 | The idea is to implement an object, the `JSONWriter`, that is able to convert an object to a 4 | [JSON](https://json.org) text. 5 | 6 | A `JSONWriter` is able to convert 7 | - basic JSON type like boolean, int or String 8 | - can be configured to handle specific type like `MonthDay` of `java.time` 9 | - recursive types, types composed of other types, likes Java Beans or records 10 | 11 | Here is an example of a `Person` defined as a record, with the `Address` defined as a bean. 12 | 13 | ```java 14 | class Address { 15 | private boolean international; 16 | 17 | public boolean isInternational() { 18 | return international; 19 | } 20 | } 21 | record Person(@JSONProperty("birth-day") MonthDay birthday, Address address) { } 22 | ``` 23 | 24 | We can create a `JSONWriter`, configure it to use a user defined format for instances of the class `MonthDay` 25 | and calls `toJSON()` to get the corresponding JSON text. 26 | 27 | ```java 28 | var writer = new JSONWriter(); 29 | writer.configure(MonthDay.class, monthDay -> writer.toJSON(monthDay.getMonth() + "-" + monthDay.getDayOfMonth())); 30 | 31 | var person = new Person(MonthDay.of(4, 17), new Address()); 32 | var json = writer.toJSON(person); // {"birth-day": "APRIL-17", "address": {"international": false}} 33 | ``` 34 | 35 | 36 | ## Let's implement it 37 | 38 | The unit tests are in [JSONWriterTest.java](src/test/java/com/github/forax/framework/mapper/JSONWriterTest.java) 39 | 40 | 1. Create the class `JSONWriter` and adds the method `toJSON()` that works only with 41 | JSON primitive values, `null`, `true`, `false`, any integers or doubles and strings. 42 | You can use a switch on type for that. 43 | Then check that the tests in the nested class "Q1" all pass. 44 | 45 | 2. Adds the support of [Java Beans](../COMPANION.md#java-bean-and-beaninfo) by modifying `toJSON()` to get the `BeanInfo`. 46 | Get the properties from it and use a stream with a `collect(Collectors.joining())` 47 | to add the '{' and '}' and separate the values by a comma. 48 | Then check that the tests in the nested class "Q2" all pass. 49 | 50 | Note: the method `Utils.beanInfo()` already provides a way to get the `BeanInfo` of a class. 51 | the method `Utils.invoke()` deals with the exception correctly when calling a `Method`. 52 | 53 | 3. The problem with the current solution is that the `BeanInfo` and the properties are computed each times 54 | even if the properties of a class are always the same. 55 | The idea is to declare a [ClassValue](../COMPANION.md#classvalue) that caches an array of properties for a class. 56 | So modify the method `toJSON()` to use a `ClassValue`. 57 | All the tests from the previous questions should still pass. 58 | 59 | 4. We can cache more values, by example the property name and the getter are always the same for a pair of key/value. 60 | We can observe that from the JSONWriter POV, there are two kinds of type, 61 | - either it's a primitive those only need the object to generate the JSON text 62 | - or it's a bean type, those need the object, and the writer to recursively call `writer.toJSON()` 63 | on the properties 64 | Thus to represent the computation, we can declare a private functional interface `Generator` that takes 65 | a `JSONWriter` and an `Object` as parameter. 66 | ```java 67 | private interface Generator { 68 | String generate(JSONWriter writer, Object bean); 69 | } 70 | ``` 71 | Change your code to use `ClassValue` instead of a `ClassValue`, 72 | and modify the implementation of the method `toJSON()` accordingly. 73 | All the tests from the previous questions should still pass. 74 | 75 | 5. Adds a method `configure()` that takes a `Class` and a lambda that takes an instance of that class 76 | and returns a string and modify `toJSON()` to work with instances of the configured classes. 77 | Internally, a HashMap that associates a class to the computation of the JSON text using the lambda. 78 | Then check that the tests in the nested class "Q5" all pass. 79 | 80 | **Note**: the lambda takes a value and returns a value thus it can be typed by a `java.util.function.Function`. 81 | The type of the class, and the type of the first parameter of the lambda are the same, 82 | you need to introduce a type parameter for that. Exactly the type of the first parameter of the 83 | lambda is a super type of the type of the class. 84 | 85 | 6. JSON keys can use any identifier not only the ones that are valid in Java. 86 | For that, we introduce an annotation `@JSONProperty` defined like this 87 | ```java 88 | @Retention(RUNTIME) 89 | @Target({METHOD, RECORD_COMPONENT}) 90 | public @interface JSONProperty { 91 | String value(); 92 | } 93 | ``` 94 | To support that, add a check if the getter is annotated with the annotation `@JSONProperty` 95 | and in that case, use the name provided by the annotation instead of the name of the property. 96 | Then check that the tests in the nested class "Q6" all pass 97 | 98 | 7. Modify the code to support not only Java beans but also [records](../COMPANION.md#record) by refactoring 99 | your code to have two private methods that takes a Class and returns either the properties of the bean 100 | or the properties of the records. 101 | ```java 102 | private static List beanProperties(Class type) { 103 | // TODO 104 | } 105 | 106 | private static List recordProperties(Class type) { 107 | // TODO 108 | } 109 | ``` 110 | Change the code so `toJSON()` works with both records and beans. 111 | Then check that the tests in the nested class "Q1" all pass 112 | -------------------------------------------------------------------------------- /injector/CODE_COMMENTS.md: -------------------------------------------------------------------------------- 1 | # Code comments 2 | 3 | 4 | ### Q1 5 | 6 | ```java 7 | public final class InjectorRegistry { 8 | private final HashMap, Object> map = new HashMap<>(); 9 | 10 | public InjectorRegistry() {} 11 | 12 | public void registerInstance(Class type, Object instance) { 13 | Objects.requireNonNull(type); 14 | Objects.requireNonNull(instance); 15 | var result = map.putIfAbsent(type, instance); 16 | if (result != null) { 17 | throw new IllegalStateException("instance of " + type.getName() + " already registered"); 18 | } 19 | } 20 | 21 | public Object lookupInstance(Class type) { 22 | Objects.requireNonNull(type); 23 | var instance = map.get(type); 24 | if (instance == null) { 25 | throw new IllegalStateException("no instance of " + type.getName()); 26 | } 27 | return instance; 28 | } 29 | } 30 | ``` 31 | 32 | 33 | ### Q2 34 | 35 | ```java 36 | public final class InjectorRegistry { 37 | private final HashMap, Object> map = new HashMap<>(); 38 | 39 | public InjectorRegistry() {} 40 | 41 | public void registerInstance(Class type, T instance) { 42 | Objects.requireNonNull(type); 43 | Objects.requireNonNull(instance); 44 | var result = map.putIfAbsent(type, instance); 45 | if (result != null) { 46 | throw new IllegalStateException("instance of " + type.getName() + " already registered"); 47 | } 48 | } 49 | 50 | public T lookupInstance(Class type) { 51 | Objects.requireNonNull(type); 52 | var instance = map.get(type); 53 | if (instance == null) { 54 | throw new IllegalStateException("no instance of " + type.getName()); 55 | } 56 | return type.cast(instance); 57 | } 58 | } 59 | ``` 60 | 61 | 62 | ### Q3 63 | 64 | ```java 65 | public final class InjectorRegistry { 66 | private final HashMap, Supplier> map = new HashMap<>(); 67 | 68 | public void registerInstance(Class type, T instance) { 69 | Objects.requireNonNull(type); 70 | Objects.requireNonNull(instance); 71 | registerProvider(type, () -> instance); 72 | } 73 | 74 | public void registerProvider(Class type, Supplier provider) { 75 | Objects.requireNonNull(type); 76 | Objects.requireNonNull(provider); 77 | var result = map.putIfAbsent(type, provider); 78 | if (result != null) { 79 | throw new IllegalStateException("provider of " + type.getName() + " already registered"); 80 | } 81 | } 82 | 83 | private Supplier lookupProvider(Class type) { 84 | var provider = map.get(type); 85 | if (provider == null) { 86 | throw new IllegalStateException("no provider of " + type.getName()); 87 | } 88 | return provider; 89 | } 90 | 91 | public T lookupInstance(Class type) { 92 | Objects.requireNonNull(type); 93 | var provider = lookupProvider(type); 94 | return type.cast(provider.get()); 95 | } 96 | } 97 | ``` 98 | 99 | 100 | ### Q4 101 | 102 | ```java 103 | // package private for testing 104 | static List findInjectableProperties(Class type) { 105 | var beanInfo = Utils.beanInfo(type); 106 | return Arrays.stream(beanInfo.getPropertyDescriptors()) 107 | .filter(property -> { 108 | var setter = property.getWriteMethod(); 109 | return setter != null && setter.isAnnotationPresent(Inject.class); 110 | }) 111 | .toList(); 112 | } 113 | ``` 114 | 115 | 116 | ### Q5 117 | 118 | ```java 119 | private void initInstance(Object instance, List properties) { 120 | for(var property: properties) { 121 | var setter = property.getWriteMethod(); 122 | var value = lookupInstance(property.getPropertyType()); 123 | Utils.invokeMethod(instance, setter, value); 124 | } 125 | } 126 | 127 | public void registerProviderClass(Class type, Class providerClass) { 128 | Objects.requireNonNull(type); 129 | Objects.requireNonNull(providerClass); 130 | var properties = findInjectableProperties(providerClass); 131 | var constructor = Utils.defaultConstructor(providerClass); 132 | registerProvider(type, () -> { 133 | var instance = Utils.newInstance(constructor); 134 | initInstance(instance, properties); 135 | return instance; 136 | }); 137 | } 138 | ``` 139 | 140 | 141 | ### Q6 142 | 143 | ```java 144 | private static Optional> findConstructorAnnotatedWithInject(Class providerClass) { 145 | var constructors = Arrays.stream(providerClass.getConstructors()) 146 | .filter(constructor -> constructor.isAnnotationPresent(Inject.class)) 147 | .toList(); 148 | return switch(constructors.size()) { 149 | case 0 -> Optional.empty(); 150 | case 1 -> Optional.of(constructors.get(0)); 151 | default -> throw new IllegalStateException("more than one constructor annotated with @Inject in " + providerClass.getName()); 152 | }; 153 | } 154 | 155 | public void registerProviderClass(Class type, Class providerClass) { 156 | Objects.requireNonNull(type); 157 | Objects.requireNonNull(providerClass); 158 | var properties = findInjectableProperties(providerClass); 159 | var constructor = findConstructorAnnotatedWithInject(providerClass) 160 | .orElseGet(() -> Utils.defaultConstructor(providerClass)); 161 | var parameterTypes = constructor.getParameterTypes(); 162 | registerProvider(type, () -> { 163 | Object[] args = Arrays.stream(parameterTypes) 164 | .map(this::lookupInstance) 165 | .toArray(); 166 | var instance = type.cast(Utils.newInstance(constructor, args)); 167 | initInstance(instance, properties); 168 | return instance; 169 | }); 170 | } 171 | ``` 172 | 173 | 174 | ### Q7 175 | 176 | ```java 177 | public void registerProviderClass(Class providerClass) { 178 | registerProviderClassImpl(providerClass); 179 | } 180 | 181 | private void registerProviderClassImpl(Class providerClass) { 182 | registerProviderClass(providerClass, providerClass); 183 | } 184 | ``` -------------------------------------------------------------------------------- /injector/src/test/java/com/github/forax/framework/injector/AnnotationScannerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.injector; 2 | 3 | import org.junit.jupiter.api.Nested; 4 | import org.junit.jupiter.api.Tag; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.io.IOException; 8 | import java.lang.annotation.ElementType; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.RetentionPolicy; 11 | import java.lang.annotation.Target; 12 | import java.nio.file.Files; 13 | import java.util.List; 14 | import java.util.Objects; 15 | 16 | import static org.junit.jupiter.api.Assertions.assertAll; 17 | import static org.junit.jupiter.api.Assertions.assertEquals; 18 | import static org.junit.jupiter.api.Assertions.assertNotNull; 19 | import static org.junit.jupiter.api.Assertions.assertThrows; 20 | import static org.junit.jupiter.api.Assertions.assertTrue; 21 | 22 | @SuppressWarnings("unused") 23 | public class AnnotationScannerTest { 24 | 25 | @Nested 26 | public class Q1 { 27 | @Test 28 | public void test() throws IOException { 29 | var folder = Files.createTempDirectory("annotation-scanner"); 30 | var textPath = Files.writeString(folder.resolve("text.txt"), "this is a text"); 31 | var javaPath = Files.writeString(folder.resolve("AFakeJava.class"), "this is a fake java class"); 32 | try { 33 | 34 | List list; 35 | try(var stream = AnnotationScanner.findAllJavaFilesInFolder(folder)) { 36 | list = stream.toList(); 37 | } 38 | assertEquals(List.of("AFakeJava"), list); 39 | 40 | } finally { 41 | Files.delete(javaPath); 42 | Files.delete(textPath); 43 | Files.delete(folder); 44 | } 45 | } 46 | } // end of Q1 47 | 48 | 49 | @Nested 50 | public class Q2 { 51 | static class AnotherClass {} 52 | 53 | @Test @Tag("Q2") 54 | public void findAllClasses() { 55 | var packageName = Q2.class.getPackageName(); 56 | var classLoader = Q2.class.getClassLoader(); 57 | var list = AnnotationScanner.findAllClasses(packageName, classLoader); 58 | assertTrue(list.contains(AnotherClass.class)); 59 | } 60 | 61 | @Test @Tag("Q2") 62 | public void findAllClassesPrecondition() { 63 | var packageName = "a.package.that.do.not.exist"; 64 | var classLoader = Q2.class.getClassLoader(); 65 | assertThrows(IllegalStateException.class, () -> AnnotationScanner.findAllClasses(packageName, classLoader)); 66 | } 67 | } // end of Q2 68 | 69 | @Nested 70 | public class Q3 { 71 | @Target(ElementType.TYPE) 72 | @Retention(RetentionPolicy.RUNTIME) 73 | public @interface Hello { 74 | } 75 | 76 | @Test @Tag("Q3") 77 | public void addAction() { 78 | var scanner = new AnnotationScanner(); 79 | scanner.addAction(Hello.class, (Class type) -> {}); 80 | } 81 | 82 | @Test @Tag("Q3") 83 | public void addActionSameAnnotationClassTwice() { 84 | var scanner = new AnnotationScanner(); 85 | scanner.addAction(Hello.class, __ -> {}); 86 | assertThrows(IllegalStateException.class, () -> scanner.addAction(Hello.class, __ -> {})); 87 | } 88 | 89 | @Test @Tag("Q3") 90 | public void addActionPrecondition() { 91 | var scanner = new AnnotationScanner(); 92 | assertAll( 93 | () -> assertThrows(NullPointerException.class, () -> scanner.addAction(Hello.class, null)), 94 | () -> assertThrows(NullPointerException.class, () -> scanner.addAction(null, __ -> {})) 95 | ); 96 | } 97 | } // end of Q3 98 | 99 | 100 | // Q4 101 | 102 | @Nested 103 | public class Q4 { 104 | @Target(ElementType.TYPE) 105 | @Retention(RetentionPolicy.RUNTIME) 106 | public @interface Entity { 107 | } 108 | 109 | @Entity 110 | static class AnnotatedClass {} 111 | 112 | @Test @Tag("Q4") 113 | public void scanClassPathPackageForAnnotations() { 114 | var scanner = new AnnotationScanner(); 115 | scanner.addAction(Entity.class, type -> assertEquals(AnnotatedClass.class, type)); 116 | scanner.scanClassPathPackageForAnnotations(Q4.class); 117 | } 118 | 119 | 120 | @Target(ElementType.TYPE) 121 | @Retention(RetentionPolicy.RUNTIME) 122 | public @interface Component { 123 | } 124 | 125 | @Component 126 | static class Service { 127 | public Service() { 128 | } 129 | } 130 | 131 | @Test @Tag("Q4") 132 | public void scanAndInjectSimple() { 133 | var registry = new InjectorRegistry(); 134 | var scanner = new AnnotationScanner(); 135 | scanner.addAction(Component.class, registry::registerProviderClass); 136 | scanner.scanClassPathPackageForAnnotations(Q4.class); 137 | var service = registry.lookupInstance(Service.class); 138 | assertNotNull(service); 139 | } 140 | 141 | 142 | @Component 143 | public static class Dependency { } 144 | 145 | @Component 146 | static class ServiceWithDependency { 147 | private final Dependency dependency; 148 | 149 | @Inject 150 | public ServiceWithDependency(Dependency dependency) { 151 | this.dependency = Objects.requireNonNull(dependency); 152 | } 153 | 154 | public Dependency getDependency() { 155 | return dependency; 156 | } 157 | } 158 | 159 | @Test @Tag("Q4") 160 | public void scanAndInjectWithDependency() { 161 | var registry = new InjectorRegistry(); 162 | var scanner = new AnnotationScanner(); 163 | scanner.addAction(Component.class, registry::registerProviderClass); 164 | scanner.scanClassPathPackageForAnnotations(Q4.class); 165 | var service = registry.lookupInstance(ServiceWithDependency.class); 166 | assertNotNull(service); 167 | assertNotNull(service.getDependency()); 168 | } 169 | 170 | 171 | public static class NonAnnotatedDependency { } 172 | 173 | @Component 174 | static class EntityWithANonAnnotatedDependency { 175 | private final NonAnnotatedDependency nonAnnotatedDependency; 176 | 177 | @Inject 178 | public EntityWithANonAnnotatedDependency(NonAnnotatedDependency nonAnnotatedDependency) { 179 | this.nonAnnotatedDependency = nonAnnotatedDependency; 180 | } 181 | } 182 | 183 | @Test @Tag("Q4") 184 | public void scanAndInjectWithMissingDependency() { 185 | var registry = new InjectorRegistry(); 186 | var scanner = new AnnotationScanner(); 187 | scanner.addAction(Component.class, registry::registerProviderClass); 188 | scanner.scanClassPathPackageForAnnotations(Q4.class); 189 | assertThrows(IllegalStateException.class, () -> registry.lookupInstance(EntityWithANonAnnotatedDependency.class)); 190 | } 191 | 192 | @Test @Tag("Q4") 193 | public void scanClassPathPackageForAnnotationsPrecondition() { 194 | var scanner = new AnnotationScanner(); 195 | assertThrows(NullPointerException.class, () -> scanner.scanClassPathPackageForAnnotations(null)); 196 | } 197 | 198 | } // end of Q4 199 | } -------------------------------------------------------------------------------- /mapper/src/main/java/com/github/forax/framework/mapper/ToyJSONParser.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.mapper; 2 | 3 | import static java.lang.Double.parseDouble; 4 | import static java.lang.Integer.parseInt; 5 | import static java.util.regex.Pattern.compile; 6 | import static java.util.stream.Collectors.joining; 7 | import static java.util.stream.IntStream.rangeClosed; 8 | import static com.github.forax.framework.mapper.ToyJSONParser.Kind.*; 9 | 10 | import java.util.Arrays; 11 | import java.util.regex.Matcher; 12 | import java.util.regex.Pattern; 13 | 14 | /** 15 | * A Toy JSON parser that do not recognize correctly, unicode characters, escaped strings 16 | * and i'm sure many more features. 17 | * 18 | * @see #parse(String, JSONVisitor) 19 | */ 20 | class ToyJSONParser { 21 | private ToyJSONParser() { 22 | throw new AssertionError(); 23 | } 24 | 25 | enum Kind { 26 | NULL("(null)"), 27 | TRUE("(true)"), 28 | FALSE("(false)"), 29 | DOUBLE("([0-9]*\\.[0-9]*)"), 30 | INTEGER("([0-9]+)"), 31 | STRING("\"([^\\\"]*)\""), 32 | LEFT_CURLY("(\\{)"), 33 | RIGHT_CURLY("(\\})"), 34 | LEFT_BRACKET("(\\[)"), 35 | RIGHT_BRACKET("(\\])"), 36 | COLON("(\\:)"), 37 | COMMA("(\\,)"), 38 | BLANK("([ \t]+)") 39 | ; 40 | 41 | private final String regex; 42 | 43 | Kind(String regex) { 44 | this.regex = regex; 45 | } 46 | 47 | private static final Kind[] VALUES = values(); 48 | } 49 | 50 | private record Token(Kind kind, String text, int location) { 51 | private boolean is(Kind kind) { 52 | return this.kind == kind; 53 | } 54 | 55 | private String expect(Kind kind) { 56 | if (this.kind != kind) { 57 | throw error(kind); 58 | } 59 | return text; 60 | } 61 | 62 | public IllegalStateException error(Kind... expectedKinds) { 63 | return new IllegalStateException("expect " + Arrays.stream(expectedKinds).map(Kind::name).collect(joining(", ")) + " but recognized " + kind + " at " + location); 64 | } 65 | } 66 | 67 | private record Lexer(Matcher matcher) { 68 | private Token next() { 69 | for(;;) { 70 | if (!matcher.find()) { 71 | throw new IllegalStateException("no token recognized"); 72 | } 73 | var index = rangeClosed(1, matcher.groupCount()).filter(i -> matcher.group(i) != null).findFirst().orElseThrow(); 74 | var kind = Kind.VALUES[index - 1]; 75 | if (kind != Kind.BLANK) { 76 | return new Token(kind, matcher.group(index), matcher.start(index)); 77 | } 78 | } 79 | } 80 | } 81 | 82 | /** 83 | * Methods called when a JSON text is parsed. 84 | * @see #parse(String, JSONVisitor) 85 | */ 86 | public interface JSONVisitor { 87 | /** 88 | * Called during the parsing or the content of an object or an array. 89 | * 90 | * @param key the key of the value if inside an object, {@code null} otherwise. 91 | * @param value the value 92 | */ 93 | void value(String key, Object value); 94 | 95 | /** 96 | * Called during the parsing at the beginning of an object. 97 | * @param key the key of the value if inside an object, {@code null} otherwise. 98 | * 99 | * @see #endObject(String) 100 | */ 101 | void startObject(String key); 102 | 103 | /** 104 | * Called during the parsing at the end of an object. 105 | * @param key the key of the value if inside an object, {@code null} otherwise. 106 | * 107 | * @see #startObject(String) 108 | */ 109 | void endObject(String key); 110 | 111 | /** 112 | * Called during the parsing at the beginning of an array. 113 | * @param key the key of the value if inside an object, {@code null} otherwise. 114 | * 115 | * @see #endArray(String) 116 | */ 117 | void startArray(String key); 118 | 119 | /** 120 | * Called during the parsing at the end of an array. 121 | * @param key the key of the value if inside an object, {@code null} otherwise. 122 | * 123 | * @see #startArray(String) 124 | */ 125 | void endArray(String key); 126 | } 127 | 128 | private static final Pattern PATTERN = compile(Arrays.stream(Kind.VALUES).map(k -> k.regex).collect(joining("|"))); 129 | 130 | /** 131 | * Parse a JSON text and calls the visitor methods when an array, an object or a value is parsed. 132 | * 133 | * @param input a JSON text 134 | * @param visitor the visitor to call when parsing the JSON text 135 | */ 136 | public static void parse(String input, JSONVisitor visitor) { 137 | var lexer = new Lexer(PATTERN.matcher(input)); 138 | try { 139 | parse(lexer, visitor); 140 | } catch(IllegalStateException e) { 141 | throw new IllegalStateException(e.getMessage() + "\n while parsing " + input, e); 142 | } 143 | } 144 | 145 | private static void parse(Lexer lexer, JSONVisitor visitor) { 146 | var token = lexer.next(); 147 | switch(token.kind) { 148 | case LEFT_CURLY -> { 149 | visitor.startObject(null); 150 | parseObject(null, lexer, visitor); 151 | } 152 | case LEFT_BRACKET -> { 153 | visitor.startArray(null); 154 | parseArray(null, lexer, visitor); 155 | } 156 | default -> throw token.error(LEFT_CURLY, LEFT_BRACKET); 157 | } 158 | } 159 | 160 | private static void parseValue(String currentKey, Token token, Lexer lexer, JSONVisitor visitor) { 161 | switch (token.kind) { 162 | case NULL -> visitor.value(currentKey, null); 163 | case FALSE -> visitor.value(currentKey, false); 164 | case TRUE -> visitor.value(currentKey, true); 165 | case INTEGER -> visitor.value(currentKey, parseInt(token.text)); 166 | case DOUBLE -> visitor.value(currentKey, parseDouble(token.text)); 167 | case STRING -> visitor.value(currentKey, token.text); 168 | case LEFT_CURLY -> { 169 | visitor.startObject(currentKey); 170 | parseObject(currentKey, lexer, visitor); 171 | } 172 | case LEFT_BRACKET -> { 173 | visitor.startArray(currentKey); 174 | parseArray(currentKey, lexer, visitor); 175 | } 176 | default -> throw token.error(NULL, FALSE, TRUE, INTEGER, DOUBLE, STRING, LEFT_BRACKET, RIGHT_CURLY); 177 | } 178 | } 179 | 180 | private static void parseObject(String currentKey, Lexer lexer, JSONVisitor visitor) { 181 | var token = lexer.next(); 182 | if (token.is(RIGHT_CURLY)) { 183 | visitor.endObject(currentKey); 184 | return; 185 | } 186 | for(;;) { 187 | var key = token.expect(STRING); 188 | lexer.next().expect(COLON); 189 | token = lexer.next(); 190 | parseValue(key, token, lexer, visitor); 191 | token = lexer.next(); 192 | if (token.is(RIGHT_CURLY)) { 193 | visitor.endObject(currentKey); 194 | return; 195 | } 196 | token.expect(COMMA); 197 | token = lexer.next(); 198 | } 199 | } 200 | 201 | private static void parseArray(String currentKey, Lexer lexer, JSONVisitor visitor) { 202 | var token = lexer.next(); 203 | if (token.is(RIGHT_BRACKET)) { 204 | visitor.endArray(currentKey); 205 | return; 206 | } 207 | for(;;) { 208 | parseValue(null, token, lexer, visitor); 209 | token = lexer.next(); 210 | if (token.is(RIGHT_BRACKET)) { 211 | visitor.endArray(currentKey); 212 | return; 213 | } 214 | token.expect(COMMA); 215 | token = lexer.next(); 216 | } 217 | } 218 | } -------------------------------------------------------------------------------- /JDBC.md: -------------------------------------------------------------------------------- 1 | # JDBC 2 | 3 | ## H2 Database 4 | 5 | ``` 6 | 7 | com.h2database 8 | h2 9 | 1.4.200 10 | 11 | ``` 12 | 13 | [All H2 commands](https://h2database.com/html/commands.html) 14 | 15 | ## DataSource, Connection and Transaction 16 | 17 | [DataSource](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/javax/sql/DataSource.html) 18 | 19 | 20 | ```java 21 | Path path = ... 22 | DataSource dataSource = new JdbcDataSource(); 23 | dataSource.setURL("jdbc:h2:" + path); 24 | ``` 25 | 26 | ```java 27 | DataSource dataSource = new JdbcDataSource(); 28 | dataSource.setURL("jdbc:h2:mem:test"); 29 | ``` 30 | 31 | [Connection](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Connection.html) 32 | 33 | ```java 34 | try(Connection connection = dataSource.getConnection()) { 35 | ... 36 | } 37 | ``` 38 | 39 | ### Transaction 40 | 41 | [Connection.setAutoCommit](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Connection.html#setAutoCommit(boolean)) 42 | [commit]([https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Connection.html#commit()) 43 | [rollback](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Connection.html#rollback()) 44 | 45 | ```java 46 | try(Connection connection = dataSource.getConnection()) { 47 | connection.setAutoCommit(false); 48 | try { 49 | ... 50 | connection.commit(); 51 | } catch(...) { 52 | connection.rollback(); 53 | } 54 | } 55 | ``` 56 | 57 | 58 | ### Transaction semantics 59 | 60 | [TRANSACTION_READ_COMMITTED](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Connection.html#TRANSACTION_READ_COMMITTED) 61 | see modification from other transactions 62 | 63 | [TRANSACTION_REPEATABLE_READ](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Connection.html#TRANSACTION_REPEATABLE_READ) 64 | don't see row modifications from other transactions but can see new rows 65 | 66 | [TRANSACTION_SERIALIZABLE](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Connection.html#TRANSACTION_SERIALIZABLE) 67 | don't see any modifications from other transactions 68 | 69 | ## Statement and PreparedStatement 70 | 71 | [Connection.createStatement()](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Connection.html#createStatement()) 72 | [Connection.prepareStatement()](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Connection.html#prepareStatement(java.lang.String)) 73 | 74 | ```java 75 | Connection connection = ... 76 | String sqlQuery = """ 77 | INSERT INTO FOO (id, name) VALUES (42, 'James Bond'); 78 | """; 79 | try(Statement statement = connection.createStatement()) { 80 | statement.executeUpdate(query); 81 | } 82 | connection.commit(); 83 | ``` 84 | 85 | ```java 86 | Connection connection = ... 87 | String sqlQuery = """ 88 | INSERT INTO FOO (id, name) VALUES (?, ?); 89 | """; 90 | try(PreparedStement statement = connection.prepareStatement(query)) { 91 | statement.setObject(1, 42); 92 | statement.setObject(2, "James Bond"); 93 | statement.executeUpdate(); 94 | } 95 | connection.commit(); 96 | ``` 97 | 98 | 99 | ## Create a Table 100 | 101 | [CREATE TABLE](https://h2database.com/html/commands.html#create_table) 102 | 103 | [Statement.executeUpdate()](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Statement.html#executeUpdate(java.lang.String)) 104 | 105 | ```java 106 | Connection connection = ... 107 | String query = """ 108 | CREATE TABLE FOO ( 109 | id BIGINT, 110 | name VARCHAR(255), 111 | PRIMARY KEY (id) 112 | ); 113 | """; 114 | try(Statement statement = connection.createStatement()) { 115 | statement.executeUpdate(query); 116 | } 117 | connection.commit(); 118 | ``` 119 | 120 | 121 | ## Insert data 122 | 123 | [INSERT INTO](https://h2database.com/html/commands.html#insert) 124 | 125 | [PreparedStatement.setObject()](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/PreparedStatement.html#setObject(int,java.lang.Object)) 126 | 127 | ```java 128 | Connection connection = ... 129 | String sqlQuery = """ 130 | INSERT INTO FOO (id, name) VALUES (?, ?); 131 | """; 132 | try(PreparedStatement statement = connection.prepareStatement(sqlQuery)) { 133 | statement.setObject(1, 42); 134 | statement.setObject(2, "James Bond"); 135 | statement.executeUpdate(); 136 | } 137 | connection.commit(); 138 | ``` 139 | 140 | 141 | ## Merge data 142 | 143 | [MERGE INTO](https://h2database.com/html/commands.html#merge_into) 144 | 145 | ```java 146 | Connection connection = ... 147 | String sqlQuery = """ 148 | MERGE INTO FOO (id, name) VALUES (?, ?); 149 | """; 150 | try(PreparedStatement statement = connection.prepareStatement(sqlQuery)) { 151 | statement.setObject(1, 42); 152 | statement.setObject(2, "James Bond"); 153 | statement.executeUpdate(); 154 | } 155 | connection.commit(); 156 | ``` 157 | 158 | 159 | ## Query 160 | 161 | [Connection.prepareStatement()](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Connection.html#prepareStatement(java.lang.String)) 162 | 163 | [PreparedStatement.executeQuery](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/PreparedStatement.html#executeQuery()) 164 | 165 | [ResultSet.next()](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/ResultSet.html#next()) 166 | 167 | [ResultSet.getObject()](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/ResultSet.html#getObject(int)) 168 | 169 | 170 | ```java 171 | Connection connection = ... 172 | String sqlQuery = """ 173 | SELECT (id, name) FROM FOO WHERE name = ?; 174 | """; 175 | try(PreparedStatement statement = connection.prepareStatement(sqlQuery)) { 176 | statement.setObject(1, "James Bond"); 177 | try(ResultSet resultSet = statement.executeQuery()) { 178 | while(resultSet.next()) { 179 | Long id = (Long) resultSet.getObject(1); 180 | String name = (String) resultSet.getObject(2); 181 | ... 182 | } 183 | } 184 | } 185 | connection.commit(); 186 | ``` 187 | 188 | 189 | ## Generated Primary Key 190 | 191 | [H2 AUTO_INCREMENT](https://stackoverflow.com/questions/9353167/auto-increment-id-in-h2-database#9356818) 192 | 193 | ```java 194 | String query = """ 195 | CREATE TABLE FOO ( 196 | id BIGINT AUTO_INCREMENT, 197 | name VARCHAR(255), 198 | PRIMARY KEY (id) 199 | ); 200 | """; 201 | ``` 202 | 203 | [Connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS)](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Connection.html#prepareStatement(java.lang.String,int)) 204 | 205 | [Statement.getGeneratedKeys()](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Statement.html#getGeneratedKeys()) 206 | 207 | [ResultSet](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/ResultSet.html) 208 | 209 | [ResultSet.next()](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/ResultSet.html#next()) 210 | 211 | [ResultSet.getObject()](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/ResultSet.html#getObject(int)) 212 | 213 | ```java 214 | Connection connection = ... 215 | String sqlQuery = """ 216 | INSERT INTO FOO (name) VALUES (?); 217 | """; 218 | try(PreparedStatement statement = connection.prepareStatement(sqlQuery, Statement.RETURN_GENERATED_KEYS)) { 219 | statement.setObject(1, 42); 220 | statement.setObject(2, "James Bond"); 221 | statement.executeUpdate(query); 222 | try(ResultSet resultSet = statement.getGeneratedKeys()) { 223 | if (resultSet.next()) { 224 | Long key = (Long) resultSet.getObject(1); 225 | ... 226 | } 227 | } 228 | } 229 | connection.commit(); 230 | ``` 231 | -------------------------------------------------------------------------------- /mapper/src/main/java/com/github/forax/framework/mapper/JSONReader.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.mapper; 2 | 3 | import java.beans.PropertyDescriptor; 4 | import java.lang.reflect.Constructor; 5 | import java.lang.reflect.ParameterizedType; 6 | import java.lang.reflect.Type; 7 | import java.util.ArrayDeque; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.Objects; 13 | import java.util.Optional; 14 | import java.util.function.Function; 15 | import java.util.function.Supplier; 16 | import java.util.stream.Collectors; 17 | import java.util.stream.IntStream; 18 | import java.util.stream.Stream; 19 | 20 | public class JSONReader { 21 | private record BeanData(Constructor constructor, Map propertyMap) { 22 | PropertyDescriptor findProperty(String key) { 23 | var property = propertyMap.get(key); 24 | if (property == null) { 25 | throw new IllegalStateException("unknown key " + key + " for bean " + constructor.getDeclaringClass().getName()); 26 | } 27 | return property; 28 | } 29 | } 30 | 31 | private static final ClassValue BEAN_DATA_CLASS_VALUE = new ClassValue<>() { 32 | @Override 33 | protected BeanData computeValue(Class type) { 34 | var beanInfo = Utils.beanInfo(type); 35 | var constructor = Utils.defaultConstructor(type); 36 | var map = Arrays.stream(beanInfo.getPropertyDescriptors()) 37 | .filter(property -> !property.getName().equals("class")) 38 | .collect(Collectors.toMap(PropertyDescriptor::getName, Function.identity())); 39 | return new BeanData(constructor, map); 40 | } 41 | }; 42 | 43 | public record ObjectBuilder(Function typeProvider, 44 | Supplier supplier, 45 | Populater populater, 46 | Function finisher) { 47 | public interface Populater { 48 | void populate(T instance, String key, Object value); 49 | } 50 | 51 | public ObjectBuilder { 52 | Objects.requireNonNull(typeProvider); 53 | Objects.requireNonNull(supplier); 54 | Objects.requireNonNull(populater); 55 | Objects.requireNonNull(finisher); 56 | } 57 | 58 | public static ObjectBuilder bean(Class beanClass) { 59 | Objects.requireNonNull(beanClass); 60 | var beanData = BEAN_DATA_CLASS_VALUE.get(beanClass); 61 | return new ObjectBuilder<>( 62 | key -> beanData.findProperty(key).getWriteMethod().getGenericParameterTypes()[0], 63 | () -> Utils.newInstance(beanData.constructor), 64 | (instance, key, value) -> { 65 | var property = beanData.findProperty(key); 66 | Utils.invokeMethod(instance, property.getWriteMethod(), value); 67 | }, 68 | Function.identity() 69 | ); 70 | } 71 | 72 | public static ObjectBuilder> list(Type elementType) { 73 | Objects.requireNonNull(elementType); 74 | return new ObjectBuilder<>( 75 | key -> elementType, 76 | ArrayList::new, 77 | (list, key, value) -> list.add(value), 78 | List::copyOf 79 | ); 80 | } 81 | 82 | public static ObjectBuilder record(Class recordClass) { 83 | Objects.requireNonNull(recordClass); 84 | var components = recordClass.getRecordComponents(); 85 | var map = IntStream.range(0, components.length) 86 | .boxed() 87 | .collect(Collectors.toMap(i -> components[i].getName(), Function.identity())); 88 | var constructor = Utils.canonicalConstructor(recordClass, components); 89 | return new ObjectBuilder<>( 90 | key -> components[map.get(key)].getGenericType(), 91 | () -> new Object[components.length], 92 | (array, key, value) -> array[map.get(key)] = value, 93 | array -> Utils.newInstance(constructor, array) 94 | ); 95 | } 96 | } 97 | 98 | @FunctionalInterface 99 | public interface TypeMatcher { 100 | Optional> match(Type type); 101 | } 102 | 103 | private final ArrayList typeMatchers = new ArrayList<>(); 104 | 105 | public void addTypeMatcher(TypeMatcher typeMatcher) { 106 | Objects.requireNonNull(typeMatcher); 107 | typeMatchers.add(typeMatcher); 108 | } 109 | 110 | ObjectBuilder findObjectBuilder(Type type) { 111 | return typeMatchers.reversed().stream() 112 | .flatMap(typeMatcher -> typeMatcher.match(type).stream()) 113 | .findFirst() 114 | .orElseGet(() -> ObjectBuilder.bean(Utils.erase(type))); 115 | } 116 | 117 | private record Context(ObjectBuilder objectBuilder, T result) { 118 | private static Context create(ObjectBuilder objectBuilder) { 119 | return new Context<>(objectBuilder, objectBuilder.supplier.get()); 120 | } 121 | 122 | private void populate(String key, Object value) { 123 | objectBuilder.populater.populate(result, key, value); 124 | } 125 | 126 | private Object finish() { 127 | return objectBuilder.finisher.apply(result); 128 | } 129 | } 130 | 131 | public T parseJSON(String text, Class expectedClass) { 132 | return expectedClass.cast(parseJSON(text, (Type) expectedClass)); 133 | } 134 | 135 | public Object parseJSON(String text, Type expectedType) { 136 | Objects.requireNonNull(text); 137 | Objects.requireNonNull(expectedType); 138 | var stack = new ArrayDeque>(); 139 | var visitor = new ToyJSONParser.JSONVisitor() { 140 | private Object result; 141 | 142 | @Override 143 | public void value(String key, Object value) { 144 | var context = stack.peek(); 145 | context.populate(key, value); 146 | } 147 | 148 | @Override 149 | public void startObject(String key) { 150 | var context = stack.peek(); 151 | var type = context == null ? expectedType : context.objectBuilder.typeProvider.apply(key); 152 | var objectbuilder = findObjectBuilder(type); 153 | stack.push(Context.create(objectbuilder)); 154 | } 155 | 156 | @Override 157 | public void endObject(String key) { 158 | var instance = stack.pop().finish(); 159 | if (stack.isEmpty()) { 160 | result = instance; 161 | return; 162 | } 163 | var context = stack.peek(); 164 | context.populate(key, instance); 165 | } 166 | 167 | @Override 168 | public void startArray(String key) { 169 | startObject(key); 170 | } 171 | 172 | @Override 173 | public void endArray(String key) { 174 | endObject(key); 175 | } 176 | }; 177 | ToyJSONParser.parse(text, visitor); 178 | return visitor.result; 179 | } 180 | 181 | public interface TypeReference { } 182 | 183 | private static Type findElemntType(TypeReference typeReference) { 184 | var typeReferenceType = Arrays.stream(typeReference.getClass().getGenericInterfaces()) 185 | .flatMap(t -> t instanceof ParameterizedType parameterizedType? Stream.of(parameterizedType): null) 186 | .filter(t -> t.getRawType() == TypeReference.class) 187 | .findFirst() 188 | .orElseThrow(() -> new IllegalArgumentException("invalid TypeReference " + typeReference)); 189 | return typeReferenceType.getActualTypeArguments()[0]; 190 | } 191 | 192 | public T parseJSON(String text, TypeReference typeReference) { 193 | var elementType = findElemntType(typeReference); 194 | @SuppressWarnings("unchecked") 195 | var result = (T) parseJSON(text, elementType); 196 | return result; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /mapper/src/test/java/com/github/forax/framework/mapper/JSONWriterTest.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.mapper; 2 | 3 | import org.junit.jupiter.api.Nested; 4 | import org.junit.jupiter.api.Tag; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.sql.Timestamp; 8 | import java.time.LocalDateTime; 9 | import java.time.LocalTime; 10 | import java.time.MonthDay; 11 | import java.time.format.DateTimeFormatter; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertAll; 14 | import static org.junit.jupiter.api.Assertions.assertEquals; 15 | import static org.junit.jupiter.api.Assertions.assertThrows; 16 | import static org.junit.jupiter.api.Assertions.assertTrue; 17 | 18 | @SuppressWarnings({"unused", "static-method"}) 19 | public class JSONWriterTest { 20 | 21 | @Nested 22 | public class Q1 { 23 | @Test @Tag("Q1") 24 | public void toJSONPrimitive() { 25 | var writer = new JSONWriter(); 26 | assertAll( 27 | () -> assertEquals("null", writer.toJSON(null)), 28 | () -> assertEquals("true", writer.toJSON(true)), 29 | () -> assertEquals("false", writer.toJSON(false)), 30 | () -> assertEquals("3", writer.toJSON(3)), 31 | () -> assertEquals("4.0", writer.toJSON(4.0)), 32 | () -> assertEquals("\"foo\"", writer.toJSON("foo")) 33 | ); 34 | } 35 | } // end of Q1 36 | 37 | 38 | public static class Car { 39 | private final String owner; 40 | 41 | public Car(String owner) { 42 | this.owner = owner; 43 | } 44 | 45 | public String getOwner() { 46 | return owner; 47 | } 48 | } 49 | 50 | public static class Alien { 51 | private final String name; 52 | private final String planet; 53 | 54 | public Alien(String name, String planet) { 55 | this.name = name; 56 | this.planet = planet; 57 | } 58 | 59 | public String getName() { 60 | return name; 61 | } 62 | 63 | public String getPlanet() { 64 | return planet; 65 | } 66 | } 67 | 68 | @Nested 69 | public class Q2 { 70 | @Test @Tag("Q2") 71 | public void toJSONWithASimpleClass() { 72 | var writer = new JSONWriter(); 73 | var car = new Car("Marty"); 74 | var json = writer.toJSON(car); 75 | assertEquals(""" 76 | {"owner": "Marty"}\ 77 | """, json); 78 | } 79 | 80 | @Test @Tag("Q2") 81 | public void toJSONWithAClass() { 82 | var writer = new JSONWriter(); 83 | var alien = new Alien("Elvis", "Proxima Centauri"); 84 | var json = writer.toJSON(alien); 85 | var expected1 = """ 86 | {"name": "Elvis", "planet": "Proxima Centauri"}\ 87 | """; 88 | var expected2 = """ 89 | {"planet": "Proxima Centauri", "name": "Elvis"}\ 90 | """; 91 | assertTrue( 92 | json.equals(expected1) || json.equals(expected2), 93 | "error: " + json + "\n expects either " + expected1 + " or " + expected2 94 | ); 95 | } 96 | 97 | @Test @Tag("Q2") 98 | public void toJSONEmptyClass() { 99 | class Empty { } 100 | var writer = new JSONWriter(); 101 | var empty = new Empty(); 102 | var json = writer.toJSON(empty); 103 | assertEquals("{}", json); 104 | } 105 | 106 | } // end of Q2 107 | 108 | public static class StartDate { 109 | private final LocalDateTime time; 110 | 111 | public StartDate(LocalDateTime time) { 112 | this.time = time; 113 | } 114 | 115 | public LocalDateTime getTime() { 116 | return time; 117 | } 118 | } 119 | 120 | 121 | @Nested 122 | public class Q5 { 123 | @Test @Tag("Q5") 124 | public void toJSONWithConfigure() { 125 | var writer = new JSONWriter(); 126 | writer.configure(LocalDateTime.class, time -> time.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); 127 | assertEquals("2021-06-16T20:53:17", writer.toJSON(LocalDateTime.of(2021, 6, 16, 20, 53, 17))); 128 | } 129 | 130 | @Test @Tag("Q5") 131 | public void toJSONBeanWithConfigure() { 132 | var writer = new JSONWriter(); 133 | writer.configure(LocalDateTime.class, time -> time.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); 134 | var startDate = new StartDate(LocalDateTime.of(2021, 7, 1, 20, 7)); 135 | var json = writer.toJSON(startDate); 136 | assertEquals(""" 137 | {"time": 2021-07-01T20:07:00}\ 138 | """, json); 139 | } 140 | 141 | @Test @Tag("Q5") 142 | public void configureTwice() { 143 | var writer = new JSONWriter(); 144 | writer.configure(LocalTime.class, __ -> "foo"); 145 | assertThrows(IllegalStateException.class, () -> writer.configure(LocalTime.class, __ -> "bar")); 146 | } 147 | 148 | @Test @Tag("Q5") 149 | public void configurePreconditions() { 150 | var writer = new JSONWriter(); 151 | assertAll( 152 | () -> assertThrows(NullPointerException.class, () -> writer.configure(null, String::toString)), 153 | () -> assertThrows(NullPointerException.class, () -> writer.configure(Timestamp.class, null)) 154 | ); 155 | } 156 | 157 | } // end of Q5 158 | 159 | public static final class Person { 160 | private final String firstName; 161 | private final String lastName; 162 | 163 | public Person(String firstName, String lastName) { 164 | this.firstName = firstName; 165 | this.lastName = lastName; 166 | } 167 | 168 | @JSONProperty("first-name") 169 | public String getFirstName() { 170 | return firstName; 171 | } 172 | 173 | @JSONProperty("last-name") 174 | public String getLastName() { 175 | return lastName; 176 | } 177 | } 178 | 179 | @Nested 180 | public class Q6 { 181 | @Test @Tag("Q6") 182 | public void toJSONWithJSONProperty() { 183 | var writer = new JSONWriter(); 184 | var person = new Person("Bob", "Hunky"); 185 | var json = writer.toJSON(person); 186 | assertEquals(""" 187 | {"first-name": "Bob", "last-name": "Hunky"}\ 188 | """, 189 | json); 190 | } 191 | 192 | } // end of Q6 193 | 194 | 195 | public static class AddressInfo { 196 | private boolean international; 197 | 198 | public boolean isInternational() { 199 | return international; 200 | } 201 | } 202 | 203 | public record PersonInfo(@JSONProperty("birth-day") MonthDay birthday, AddressInfo address) { } 204 | 205 | @Nested 206 | public class Q7 { 207 | @Test @Tag("Q7") 208 | public void toJSONWithARecord() { 209 | record Person(String name, int age) { } 210 | var writer = new JSONWriter(); 211 | var person = new Person("Ana", 37); 212 | var json = writer.toJSON(person); 213 | assertEquals(""" 214 | {"name": "Ana", "age": 37}\ 215 | """, 216 | json); 217 | } 218 | 219 | @Test @Tag("Q7") 220 | public void toJSONEmptyRecord() { 221 | record Empty() { } 222 | var writer = new JSONWriter(); 223 | var empty = new Empty(); 224 | var json = writer.toJSON(empty); 225 | assertEquals("{}", json); 226 | } 227 | 228 | @Test @Tag("Q7") 229 | public void toJSONRecursive() { 230 | record Address(String street) { } 231 | record Person(String name, Address address) { } 232 | var writer = new JSONWriter(); 233 | var person = new Person("Bob", new Address("21 Jump Street")); 234 | var json = writer.toJSON(person); 235 | assertEquals(""" 236 | {"name": "Bob", "address": {"street": "21 Jump Street"}}\ 237 | """, 238 | json); 239 | } 240 | 241 | @Test @Tag("Q7") 242 | public void toJSONFullExample() { 243 | var writer = new JSONWriter(); 244 | writer.configure(MonthDay.class, monthDay -> writer.toJSON(monthDay.getMonth() + "-" + monthDay.getDayOfMonth())); 245 | var person = new PersonInfo(MonthDay.of(4, 17), new AddressInfo()); 246 | var json = writer.toJSON(person); 247 | assertEquals(""" 248 | {"birth-day": "APRIL-17", "address": {"international": false}}\ 249 | """, 250 | json); 251 | } 252 | 253 | } // end of Q7 254 | } 255 | -------------------------------------------------------------------------------- /mapper/CODE_COMMENTS.md: -------------------------------------------------------------------------------- 1 | # Code comments 2 | 3 | ### Q1 4 | 5 | ```java 6 | public String toJSON(Object o) { 7 | return switch (o) { 8 | case null -> "null"; 9 | case Boolean _, Integer _, Long _, Float _, Double _ -> o.toString(); 10 | case String value -> "\"" + value + "\""; 11 | default -> throw new IllegalArgumentException("not supported yet"); 12 | }; 13 | } 14 | ``` 15 | 16 | ### Q2 17 | 18 | ```java 19 | public String toJSON(Object o) { 20 | return switch (o) { 21 | case null -> "null"; 22 | case Boolean _, Integer _, Long _, Float _, Double _ -> o.toString(); 23 | case String value -> "\"" + value + "\""; 24 | default -> { 25 | var beanInfo = Utils.beanInfo(o.getClass()); 26 | return Arrays.stream(beanInfo.getPropertyDescriptors()) 27 | .filter(property -> !property.getName().equals("class")) 28 | .map(property -> { 29 | var name = property.getName(); 30 | var getter = property.getReadMethod(); 31 | var value = Utils.invoke(o, getter); 32 | return "\"" + name + "\": " + toJSON(value); 33 | }) 34 | .collect(Collectors.joining(", ", "{", "}")); 35 | } 36 | }; 37 | 38 | } 39 | ``` 40 | 41 | ### Q3 42 | 43 | ```java 44 | private interface Generator { 45 | String generate(JSONWriter writer, Object object); 46 | } 47 | 48 | private static final ClassValue PROPERTIES_CLASS_VALUE = new ClassValue<>() { 49 | @Override 50 | protected PropertyDescriptor[] computeValue(Class type) { 51 | var beanInfo = Utils.beanInfo(type); 52 | var list = Arrays.stream(beanInfo.getPropertyDescriptors()) 53 | .filter(property -> !property.getName().equals("class")) 54 | .toArray(PropertyDescriptor[]::new); 55 | } 56 | }; 57 | 58 | public String toJSON(Object o) { 59 | return switch (o) { 60 | case null -> "null"; 61 | case Boolean _, Integer _, Long _, Float _, Double _ -> o.toString(); 62 | case String value -> "\"" + value + "\""; 63 | default -> { 64 | var properties = PROPERTIES_CLASS_VALUE.get(o.getClass()); 65 | return Arrays.stream(properties 66 | .map(property -> { 67 | var name = property.getName(); 68 | var getter = property.getReadMethod(); 69 | var value = Utils.invoke(o, getter); 70 | return "\"" + name + "\": " + writer.toJSON(value); 71 | }) 72 | .collect(joining(", ", "{", "}")); 73 | } 74 | ``` 75 | 76 | ### Q4 77 | 78 | ```java 79 | private interface Generator { 80 | String generate(JSONWriter writer, Object object); 81 | } 82 | 83 | private static final ClassValue GENERATOR_CLASS_VALUE = new ClassValue<>() { 84 | @Override 85 | protected Generator computeValue(Class type) { 86 | var beanInfo = Utils.beanInfo(type); 87 | var list = Arrays.stream(beanInfo.getPropertyDescriptors()) 88 | .filter(property -> !property.getName().equals("class")) 89 | .map(property -> { 90 | var key = "\"" + property.getName() + "\": "; 91 | var getter = property.getReadMethod(); 92 | return (writer, o) -> key + writer.toJSON(Utils.invoke(o, getter)); 93 | }) 94 | .toList(); 95 | return (writer, object) -> list.stream() 96 | .map(generator -> generator.generate(writer, object)) 97 | .collect(joining(", ", "{", "}")); 98 | } 99 | }; 100 | 101 | public String toJSON(Object o) { 102 | return switch (o) { 103 | case null -> "null"; 104 | case Boolean _, Integer _, Long _, Float _, Double _ -> o.toString(); 105 | case String value -> "\"" + value + "\""; 106 | default -> { 107 | var generator = GENERATOR_CLASS_VALUE.get(o.getClass()); 108 | return generator.generate(this, o); 109 | } 110 | }; 111 | } 112 | ``` 113 | 114 | ### Q5 115 | 116 | ```java 117 | private interface Generator { 118 | String generate(JSONWriter writer, Object object); 119 | } 120 | 121 | private final HashMap, Generator> map = new HashMap<>(); 122 | 123 | public void configure(Class type, Function function) { 124 | Objects.requireNonNull(type); 125 | Objects.requireNonNull(function); 126 | var result = map.putIfAbsent(type, (writer, object) -> function.apply(type.cast(object))); 127 | if (result != null) { 128 | throw new IllegalStateException("already a function registered for type " + type.getName()); 129 | } 130 | } 131 | 132 | public String toJSON(Object o) { 133 | return switch (o) { 134 | case null -> "null"; 135 | case Boolean _, Integer _, Long _, Float _, Double _ -> o.toString(); 136 | case String value -> "\"" + value + "\""; 137 | default -> { 138 | var type = o.getClass(); 139 | var generator = map.get(type); 140 | if (generator == null) { 141 | generator = GENERATOR_CLASS_VALUE.get(type); 142 | } 143 | return generator.generate(this, o); 144 | } 145 | }; 146 | } 147 | ``` 148 | 149 | ### Q6 150 | 151 | ```java 152 | private static final ClassValue GENERATOR_CLASS_VALUE = new ClassValue<>() { 153 | @Override 154 | protected Generator computeValue(Class type) { 155 | var beanInfo = Utils.beanInfo(type); 156 | var list = Arrays.stream(beanInfo.getPropertyDescriptors()) 157 | .filter(property -> !property.getName().equals("class")) 158 | .map(property -> { 159 | var getter = property.getReadMethod(); 160 | var propertyAnnotation = getter.getAnnotation(JSONProperty.class); 161 | var propertyName = propertyAnnotation == null? property.getName(): propertyAnnotation.value(); 162 | var key = "\"" + propertyName + "\": "; 163 | return (writer, o) -> key + writer.toJSON(Utils.invoke(o, getter)); 164 | }) 165 | .toList(); 166 | return (writer, object) -> list.stream() 167 | .map(generator -> generator.generate(writer, object)) 168 | .collect(joining(", ", "{", "}")); 169 | } 170 | }; 171 | ``` 172 | 173 | ### Q7 174 | 175 | ```java 176 | private static final ClassValue GENERATOR_CLASS_VALUE = new ClassValue<>() { 177 | @Override 178 | protected Generator computeValue(Class type) { 179 | var properties = type.isRecord()? recordProperties(type): beanProperties(type); 180 | var generators = properties.stream() 181 | .map(property -> { 182 | var getter = property.getReadMethod(); 183 | var propertyAnnotation = getter.getAnnotation(JSONProperty.class); 184 | var propertyName = propertyAnnotation == null? property.getName(): propertyAnnotation.value(); 185 | var key = "\"" + propertyName + "\": "; 186 | return (writer, o) -> key + writer.toJSON(Utils.invokeMethod(o, getter)); 187 | }) 188 | .toList(); 189 | return (writer, object) -> generators.stream() 190 | .map(generator -> generator.generate(writer, object)) 191 | .collect(joining(", ", "{", "}")); 192 | } 193 | }; 194 | 195 | private static List beanProperties(Class type) { 196 | var beanInfo = Utils.beanInfo(type); 197 | return Arrays.stream(beanInfo.getPropertyDescriptors()) 198 | .filter(property -> !property.getName().equals("class")) 199 | .toList(); 200 | } 201 | 202 | private static List recordProperties(Class type) { 203 | return Arrays.stream(type.getRecordComponents()) 204 | .map(component -> { 205 | try { 206 | return new PropertyDescriptor(component.getName(), component.getAccessor(), null); 207 | } catch (IntrospectionException e) { 208 | throw new AssertionError(e); 209 | } 210 | }) 211 | .toList(); 212 | } 213 | ``` 214 | -------------------------------------------------------------------------------- /interceptor/CODE_COMMENTS.md: -------------------------------------------------------------------------------- 1 | # Code comments 2 | 3 | ### Q1 4 | 5 | ```java 6 | public final class InterceptorRegistry { 7 | private AroundAdvice advice; 8 | 9 | public void addAroundAdvice(Class annotationClass, AroundAdvice advice) { 10 | Objects.requireNonNull(annotationClass, "annotationClass is null"); 11 | Objects.requireNonNull(advice, "advice is null"); 12 | this.advice = advice; 13 | } 14 | 15 | public T createProxy(Class type, T instance) { 16 | Objects.requireNonNull(type, "type is null"); 17 | Objects.requireNonNull(instance, "instance is null"); 18 | return type.cast( 19 | Proxy.newProxyInstance(type.getClassLoader(), new Class[] {type}, 20 | (proxy, method, args) -> { 21 | if (advice != null) { 22 | advice.pre(instance, method, args); 23 | } 24 | var result = Utils.invokeMethod(instance, method, args); 25 | if (advice != null) { 26 | advice.post(instance, method, args, result); 27 | } 28 | return result; 29 | })); 30 | } 31 | } 32 | ``` 33 | 34 | ### Q2 35 | 36 | ```java 37 | public final class InterceptorRegistry { 38 | private final HashMap, List> adviceMap = new HashMap<>(); 39 | 40 | public void addAroundAdvice(Class annotationClass, AroundAdvice advice) { 41 | Objects.requireNonNull(annotationClass, "annotationClass is null"); 42 | Objects.requireNonNull(advice, "advice is null"); 43 | adviceMap.computeIfAbsent(annotationClass, __ -> new ArrayList<>()).add(advice); 44 | } 45 | 46 | // package private for test 47 | List findAdvices(Method method) { 48 | return Arrays.stream(method.getAnnotations()) 49 | .flatMap(annotation -> adviceMap.getOrDefault(annotation.annotationType(), List.of()).stream()) 50 | .toList(); 51 | } 52 | 53 | public T createProxy(Class type, T instance) { 54 | Objects.requireNonNull(type, "type is null"); 55 | Objects.requireNonNull(instance, "instance is null"); 56 | return type.cast( 57 | Proxy.newProxyInstance(type.getClassLoader(), new Class[] {type}, 58 | (proxy, method, args) -> { 59 | var advices = findAdvices(method); 60 | for (var advice : advices) { 61 | advice.pre(instance, method, args); 62 | } 63 | var result = Utils.invokeMethod(instance, method, args); 64 | for (var advice : advices) { 65 | advice.post(instance, method, args, result); 66 | } 67 | return result; 68 | })); 69 | } 70 | } 71 | ``` 72 | 73 | 74 | ### Q3 75 | 76 | ```java 77 | public final class InterceptorRegistry { 78 | ... 79 | 80 | private final HashMap, List> interceptorMap = new HashMap<>(); 81 | 82 | public void addInterceptor(Class annotationClass, Interceptor interceptor) { 83 | Objects.requireNonNull(annotationClass, "annotationClass is null"); 84 | Objects.requireNonNull(interceptor, "interceptor is null"); 85 | interceptorMap.computeIfAbsent(annotationClass, __ -> new ArrayList<>()).add(interceptor); 86 | } 87 | 88 | // package private 89 | List findInterceptors(Method method) { 90 | return Arrays.stream(method.getAnnotations()) 91 | .flatMap(annotation -> interceptorMap.getOrDefault(annotation.annotationType(), List.of()).stream()) 92 | .toList(); 93 | } 94 | } 95 | ``` 96 | 97 | ### Q4 98 | 99 | ```java 100 | public final class InterceptorRegistry { 101 | ... 102 | // package private 103 | static Invocation getInvocation(List interceptors) { 104 | return Utils.reverseList(interceptors).stream() 105 | .reduce(Utils::invokeMethod, 106 | (invocation, interceptor) -> (instance, method, args) -> interceptor.intercept(instance, method, args, invocation), 107 | (_1, _2) -> { throw new AssertionError(); }); 108 | } 109 | } 110 | ``` 111 | 112 | ### Q5 113 | 114 | ```java 115 | public final class InterceptorRegistry { 116 | private final HashMap, List> interceptorMap = new HashMap<>(); 117 | 118 | public void addAroundAdvice(Class annotationClass, AroundAdvice advice) { 119 | Objects.requireNonNull(annotationClass, "annotationClass is null"); 120 | Objects.requireNonNull(advice, "advice is null"); 121 | addInterceptor(annotationClass, (instance, method, args, invocation) -> { 122 | advice.pre(instance, method, args); 123 | var result = invocation.invoke(instance, method, args); 124 | advice.post(instance, method, args, result); 125 | return result; 126 | }); 127 | } 128 | 129 | public void addInterceptor(Class annotationClass, Interceptor interceptor) { 130 | Objects.requireNonNull(annotationClass, "annotationClass is null"); 131 | Objects.requireNonNull(interceptor, "interceptor is null"); 132 | interceptorMap.computeIfAbsent(annotationClass, __ -> new ArrayList<>()).add(interceptor); 133 | invocationCache.clear(); 134 | } 135 | 136 | // package private 137 | List findInterceptors(Method method) { 138 | return Stream.of( 139 | Arrays.stream(method.getDeclaringClass().getAnnotations()), 140 | Arrays.stream(method.getAnnotations()), 141 | Arrays.stream(method.getParameterAnnotations()).flatMap(Arrays::stream)) 142 | .flatMap(s -> s) 143 | .map(Annotation::annotationType) 144 | .distinct() 145 | .flatMap(annotationType -> interceptorMap.getOrDefault(annotationType, List.of()).stream()) 146 | .toList(); 147 | } 148 | 149 | // package private 150 | static Invocation getInvocation(List interceptors) { 151 | return Utils.reverseList(interceptors).stream() 152 | .reduce(Utils::invokeMethod, 153 | (invocation, interceptor) -> (instance, method, args) -> interceptor.intercept(instance, method, args, invocation), 154 | (_1, _2) -> { throw new AssertionError(); }); 155 | } 156 | 157 | public T createProxy(Class type, T instance) { 158 | Objects.requireNonNull(type, "type is null"); 159 | Objects.requireNonNull(instance, "instance is null"); 160 | return type.cast(Proxy.newProxyInstance(type.getClassLoader(), 161 | new Class[] { type }, 162 | (proxy, method, args) -> getInvocation(findInterceptors(method)).invoke(instance, method, args))); 163 | } 164 | } 165 | ``` 166 | 167 | ### Q6 168 | 169 | We can now add a cache, again using `computeIfAbsent()`, so calling the same method even with different proxies 170 | always return the same instance of `Invocation`. 171 | We also need to invalidate the cache each time `addInterceptor()` is called. 172 | 173 | ```java 174 | public final class InterceptorRegistry { 175 | private final HashMap, List> interceptorMap = new HashMap<>(); 176 | private final HashMap invocationCache = new HashMap<>(); 177 | 178 | ... 179 | public void addInterceptor(Class annotationClass, Interceptor interceptor) { 180 | Objects.requireNonNull(annotationClass, "annotationClass is null"); 181 | Objects.requireNonNull(interceptor, "interceptor is null"); 182 | interceptorMap.computeIfAbsent(annotationClass, __ -> new ArrayList<>()).add(interceptor); 183 | invocationCache.clear(); 184 | } 185 | 186 | ... 187 | private Invocation getInvocationFromCache(Method method) { 188 | return invocationCache.computeIfAbsent(method, m -> getInvocation(findInterceptors(m))); 189 | } 190 | 191 | public T createProxy(Class type, T instance) { 192 | Objects.requireNonNull(type, "type is null"); 193 | Objects.requireNonNull(instance, "instance is null"); 194 | return type.cast(Proxy.newProxyInstance(type.getClassLoader(), 195 | new Class[] { type }, 196 | (proxy, method, args) -> getInvocationFromCache(method).invoke(instance, method, args))); 197 | } 198 | } 199 | 200 | ``` 201 | 202 | 203 | ### Q7 204 | 205 | To support annotation from the declaring interface, the method or a parameter of the method, 206 | we create a Stream of the three Streams and we flatMap it. 207 | We use `distinct` here, because the same interceptor can be registered on different annotations 208 | but we want to call it once. 209 | 210 | ```java 211 | // package private 212 | List findInterceptors(Method method) { 213 | return Stream.of( 214 | Arrays.stream(method.getDeclaringClass().getAnnotations()), 215 | Arrays.stream(method.getAnnotations()), 216 | Arrays.stream(method.getParameterAnnotations()).flatMap(Arrays::stream)) 217 | .flatMap(s -> s) 218 | .map(Annotation::annotationType) 219 | .distinct() 220 | .flatMap(annotationType -> interceptorMap.getOrDefault(annotationType, List.of()).stream()) 221 | .toList(); 222 | } 223 | ``` -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /injector/README.md: -------------------------------------------------------------------------------- 1 | # Dependency Injection 2 | 3 | An `InjectorRegistry` is a class able to provide instances of classes from recipes of object creation. 4 | There are two ways to get such instances either explicitly using `lookupInstance(type)` or 5 | implicitly on constructors or setters annotated by the annotation `@Inject`. 6 | 7 | 8 | ### Protocols of injection 9 | 10 | Usual injection framework like [Guice](https://github.com/google/guice), 11 | [Spring](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-dependencies) 12 | or [CDI/Weld](https://docs.oracle.com/javaee/6/tutorial/doc/giwhb.html) 13 | provides 3 ways to implicitly get an instance of a class 14 | - constructor based dependency injection, the constructor annotated with 15 | [@Inject](https://javax-inject.github.io/javax-inject/) 16 | is called with the instances as arguments, it's considered as the best way to get a dependency 17 | - setter based dependency injection, after a call to the default constructor, the setters are called. 18 | The main drawback is that the setters can be called in any order (the order may depend on 19 | the version of the compiler/VM used) 20 | - field based dependency injection, after a call to the default constructor, the fields are filled with the instances, 21 | this methods bypass the default security model of Java using **deep reflection**, relying on either 22 | not having a module declared or the package being open in the module-info.java. Because of that, 23 | this is not the recommended way of doing injection. 24 | 25 | We will only implement the constructor based and setter based dependency injection. 26 | 27 | 28 | ### Early vs Late checking 29 | 30 | When injecting instances, an error can occur if the `InjectorRegistry` has no recipe to create 31 | an instance of a class. Depending on the implementation of the injector, the error can be 32 | detected either 33 | - when a class that asks for injection is registered 34 | - when an instance of a class asking for injection is requested 35 | 36 | The former is better than the later because the configuration error are caught earlier, 37 | but here, because we want to implement a simple injector, all configuration errors will appear 38 | late when an instance is requested. 39 | 40 | 41 | ### Configuration 42 | 43 | There are several ways to configure an injector, it can be done 44 | - using an XML file, this the historical way (circa 2000-2005) to do the configuration. 45 | - using classpath/modulepath scanning. All the classes of the application are scanned and classes with 46 | annotated members are added to the injector. The drawback of this method is that this kind of scanning 47 | is quite slow, slowing down the startup time of the application. 48 | Recent frameworks like Quarkus or Spring Native move the annotation discovery at compile time using 49 | an annotation processor to alleviate that issue. 50 | - using an API to explicitly register the recipe to get the dependency. 51 | 52 | We will implement the explicit API while the classpath scanning is implemented in [the part 2](README2.md). 53 | 54 | 55 | ### Our injector 56 | 57 | The class `InjectorRegistry` has 4 methods 58 | - `lookupInstance(type)` which returns an instance of a type using a recipe previously registered 59 | - `registerInstance(type, object)` register the only instance (singleton) to always return for a type 60 | - `registerProvider(type, supplier)` register a supplier to call to get the instance for a type 61 | - `registerProviderClass(type, class)` register a bean class that will be instantiated for a type 62 | 63 | As an example, suppose we have a record `Point` and a bean `Circle` with a constructor `Circle` annotated 64 | with `@Inject` and a setter `setName` of `String` also annotated with `@Inject`. 65 | 66 | ```java 67 | record Point(int x, int y) {} 68 | 69 | class Circle { 70 | private final Point center; 71 | private String name; 72 | 73 | @Inject 74 | public Circle(Point center) { 75 | this.center = center; 76 | } 77 | 78 | @Inject 79 | public void setName(String name) { 80 | this.name = name; 81 | } 82 | } 83 | ``` 84 | 85 | We can register the `Point(0, 0)` as the instance that will always be returned when an instance of `Point` is requested. 86 | We can register a `Supplier` (here, one that always return "hello") when an instance of `String` is requested. 87 | We can register a class `Circle.class` (the second parameter), that will be instantiated when an instance of `Circle` 88 | is requested. 89 | 90 | ```java 91 | var registry = new InjectorRegistry(); 92 | registry.registerInstance(Point.class, new Point(0, 0)); 93 | registry.registerProvider(String.class, () -> "hello"); 94 | registry.registerProviderClass(Circle.class, Circle.class); 95 | 96 | var circle = registry.lookupInstance(Circle.class); 97 | System.out.println(circle.center); // Point(0, 0) 98 | System.out.println(circle.name); // hello 99 | ``` 100 | 101 | 102 | ## Let's implement it 103 | 104 | The unit tests are in [InjectorRegistryTest.java](src/test/java/com/github/forax/framework/injector/InjectorRegistryTest.java) 105 | 106 | 1. Create a class `InjectorRegistry` and add the methods `registerInstance(type, instance)` and 107 | `lookupInstance(type)` that respectively registers an instance into a `Map` and retrieves an instance for 108 | a type. `registerInstance(type, instance)` should allow registering only one instance per type and 109 | `lookupInstance(type)` should throw an exception if no instance have been registered for a type. 110 | Then check that the tests in the nested class "Q1" all pass. 111 | 112 | Note: for now, the instance does not have to be an instance of the type `type`. 113 | You can use [Map.putIfAbsent()](https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/util/Map.html#putIfAbsent(K,V)) 114 | to detect if there is already a pair/entry with the same key in the `Map` in one call. 115 | 116 | 117 | 2. We want to enforce that the instance has to be an instance of the type taken as parameter. 118 | For that, declare a `T` and say that the type of the `Class`and the type of the instance is the same. 119 | Then use the same trick for `lookupInstance(type)` and check that the tests in the nested class "Q2" all pass. 120 | 121 | Note: inside `lookupInstance(type)`, now that we now that the instance we return has to be 122 | an instance of the type, we can use 123 | [Class.cast()](https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/lang/Class.html#cast(java.lang.Object)) 124 | to avoid an unsafe cast. 125 | 126 | 127 | 3. We now want to add the method `registerProvider(type, supplier)` that register a supplier (a function with 128 | no parameter that return a value) that will be called each time an instance is requested. 129 | An astute reader can remark that a supplier can always return the same instance thus we do not need two `Map`s, 130 | but only one that stores suppliers. 131 | Add the method `registerProvider(type, supplier)` and modify your implementation to support it. 132 | Then check that the tests in the nested class "Q3" all pass. 133 | 134 | 135 | 4. In order to implement the injection using setters, we need to find all the 136 | [Bean properties](../COMPANION.md#java-bean-and-beaninfo) 137 | that have a setter [annotated](../COMPANION.md#methodisannotationpresent-methodgetannotation-methodgetannotations) 138 | with `@Inject`. 139 | Write a helper method `findInjectableProperties(class)` that takes a class as parameter and returns a list of 140 | all properties (`PropertyDescriptor`) that have a setter annotated with `@Inject`. 141 | Then check that the tests in the nested class "Q4" all pass. 142 | 143 | Note: The class `Utils` already defines a method `beanInfo()`. 144 | 145 | 146 | 5. We want to add a method `registerProviderClass(type, providerClass)` that takes a type and a class, 147 | the `providerClass` implementing that type and register a recipe that create a new instance of 148 | `providerClass` by calling the default constructor. This instance is initialized by calling all the 149 | setters annotated with `@Inject` with an instance of the corresponding property type 150 | (obtained by calling `lookupInstance`). 151 | Write the method `registerProviderClass(type, providerClass)` and 152 | check that the tests in the nested class "Q5" all pass. 153 | 154 | Note: The class `Utils` defines the methods `defaultConstructor()`, `newInstance()` and `invokeMethod()`. 155 | 156 | 157 | 6. We want to add the support of constructor injection. 158 | The idea is that either only one of the 159 | [public constructors](../COMPANION.md#classgetmethod-classgetmethods-classgetconstructors) 160 | of the `providerClass` is annotated with `@Inject` or a public default constructor 161 | (a constructor with no parameter) should exist. 162 | Modify the code that instantiate the `providerClass` to use that constructor to 163 | creates an instance. 164 | Then check that the tests in the nested class "Q6" all pass. 165 | 166 | 167 | 7. To finish, we want to add a user-friendly overload of `registerProviderClass`, 168 | `registerProviderClass(providerClass)` that takes only a `providerClass` 169 | and is equivalent to `registerProviderClass(providerClass, providerClass)`. 170 | -------------------------------------------------------------------------------- /interceptor/README.md: -------------------------------------------------------------------------------- 1 | # Interceptor 2 | 3 | An interceptor is a function that is called before/after a method call to react to the arguments or return value 4 | of that method call. To select which interceptor will be run on which method call, we register 5 | an interceptor with an annotation class and all methods annotated with an annotation of that class will be 6 | intercepted by this interceptor. 7 | 8 | ## Advice and interceptor 9 | 10 | There are more or less two different kind of API to intercept a method call. 11 | - the around advice, an interface with two methods, `before` and `after`that are respectively called 12 | before and after a call. 13 | ```java 14 | public interface AroundAdvice { 15 | void before(Object instance, Method method, Object[] args) throws Throwable; 16 | void after(Object instance, Method method, Object[] args, Object result) throws Throwable; 17 | } 18 | ``` 19 | The `instance` is the object on which the method is be called, `method` is the method called, 20 | `args` are the arguments of the call (or `null` is there is no argument). 21 | The last parameter of the method `after`, `result` is the returned value of the method call. 22 | 23 | - one single method that takes as last parameter a way to call the next interceptor 24 | ```java 25 | @FunctionalInterface 26 | public interface Interceptor { 27 | Object intercept(Method method, Object proxy, Object[] args, Invocation invocation) throws Throwable; 28 | } 29 | ``` 30 | with `Invocation` a functional interface corresponding to the next interceptor i.e. an interface 31 | with an abstract method bound to a specific interceptor (partially applied if you prefer). 32 | ``java 33 | @FunctionalInterface 34 | public interface Invocation { 35 | Object proceed(Object instance, Method method, Object[] args) throws Throwable; 36 | } 37 | ``` 38 | 39 | The interceptor API is more powerful and can be used to simulate the around advice API. 40 | 41 | 42 | ## Interceptors and/or Aspect Oriented Programming 43 | 44 | The interface we are implementing here, is very similar to 45 | [Spring method interceptor](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/aopalliance/intercept/MethodInterceptor.html), 46 | [CDI interceptor](https://docs.oracle.com/javaee/6/tutorial/doc/gkhjx.html) or 47 | [Guice interceptor](https://www.baeldung.com/guice). 48 | 49 | All of them are using the same API provided by the 50 | [Aspect Oriented Programming Alliance](http://aopalliance.sourceforge.net/) 51 | which is a group created to define a common API for interceptors in Java. 52 | Compared to the API we are about to implement, the AOP Alliance API encapsulates the parameters 53 | (instance, method, args, link to the next interceptor) inside the interface `MethodInvocation`. 54 | 55 | [Aspect Oriented Programming, AOP](https://en.wikipedia.org/wiki/Aspect-oriented_programming) is a more general 56 | conceptual framework from the beginning of 2000s, an interceptor is equivalent to the around advice. 57 | 58 | 59 | ## An example 60 | 61 | The API works in two steps, first register an advice (or an interceptor) for an annotation, 62 | then creates a proxy of an interface. When a method of the proxy is called through the interface, 63 | if the method is annotated, the corresponding advices/interceptors will be called. 64 | 65 | For example, if we want to implement an advice that will check that the arguments of a method are not null. 66 | First we need to define an annotation 67 | 68 | ```java 69 | @Retention(RUNTIME) 70 | @Target(METHOD) 71 | @interface CheckNotNull { } 72 | ``` 73 | 74 | If we want to check the argument of a method of an interface, we need to annotate it with `@CheckNotNull` 75 | ```java 76 | interface Hello { 77 | @CheckNotNull String say(String message, String name); 78 | } 79 | ``` 80 | 81 | We also have an implementation of that interface, that provides the behavior the user want 82 | ```java 83 | class HelloImpl implements Hello { 84 | @Override 85 | public String say(String message, String name) { 86 | return message + " " + name; 87 | } 88 | } 89 | ``` 90 | 91 | Step 1, we create an interceptor registry and add an around advice that checks that the arguments are not null 92 | ```java 93 | var registry = new InterceptorRegistry(); 94 | registry.addAroundAdvice(CheckNotNull.class, new AroundAdvice() { 95 | @Override 96 | public void before(Object delegate, Method method, Object[] args) { 97 | Arrays.stream(args).forEach(Objects::requireNonNull); 98 | } 99 | 100 | @Override 101 | public void after(Object delegate, Method method, Object[] args, Object result) {} 102 | }); 103 | ``` 104 | 105 | Step 2, we create a proxy in between the interface and the implementation 106 | ```java 107 | var proxy = registry.createProxy(Hello.class, hello); 108 | 109 | assertAll( 110 | () -> assertEquals("hello around advice", proxy.say("hello", "around advice")), 111 | () -> assertThrows(NullPointerException.class, () -> proxy.say("hello", null)) 112 | ); 113 | ``` 114 | 115 | We can test the proxy with several arguments, null or not 116 | ```java 117 | assertAll( 118 | () -> assertEquals("hello around advice", proxy.say("hello", "around advice")), 119 | () -> assertThrows(NullPointerException.class, () -> proxy.say("hello", null)) 120 | ); 121 | ``` 122 | 123 | 124 | ## The interceptor registry 125 | 126 | An `InterceptorRegistry` is a class that manage the interceptors, it defines three public methods 127 | - `addAroundAdvice(annotationClass, aroundAdvice)` register an around advice for an annotation 128 | - `addInterceptor(annotationClass, interceptor)` register an interceptor for an annotation 129 | - `createProxy(interfaceType, instance)` create a proxy that for each annotated methods will call 130 | the advices/interceptors before calling the method on the instance. 131 | 132 | 133 | 134 | ## Let's implement it 135 | 136 | The idea is to gradually implement the class `InterceptorRegistry`, first by implementing the support 137 | for around advice then add the support of interceptor and retrofit around advices to be implemented 138 | as interceptors. To finish, add a cache avoiding recomputing of the linked list of invocations 139 | at each call. 140 | 141 | 142 | 1. Create a class `InterceptorRegistry` with two public methods 143 | - a method `addAroundAdvice(annotationClass, aroundAdvice)` that for now do not care about the 144 | `annotationClass` and store the advice in a field. 145 | - a method `createProxy(type, delegate)` that creates a [dynamic proxy](../COMPANION.md#dynamic-proxy) 146 | implementing the interface and calls the method `before` and `after` of the around advice 147 | (if one is defined) around the call of each method using `Utils.invokeMethod()`. 148 | Check that the tests in the nested class "Q1" all pass. 149 | 150 | 151 | 2. Change the implementation of `addAroundAdvice` to store all advices by annotation class. 152 | And add a package private instance method `findAdvices(method)` that takes a `java.lang.reflect.Method` as 153 | parameter and returns a list of all advices that should be called. 154 | An around advice is called for a method if that method is annotated with an annotation of 155 | the annotation class on which the advice is registered. 156 | The idea is to gather [all annotations](../COMPANION.md#annotation) of that method 157 | and find all corresponding advices. 158 | Once the method `findAdvices` works, modify the method `createProxy`to use it. 159 | Check that the tests in the nested class "Q2" all pass. 160 | 161 | 162 | 3. We now want to be support the interceptor API, and for now we will implement it as an addon, 163 | without changing the support of the around advices. 164 | Add a method `addInterceptor(annotationClass, interceptor)` and a method 165 | `finInterceptors(method)` that respectively add an interceptor for an annotation class and 166 | returns a list of all interceptors to call for a method. 167 | Check that the tests in the nested class "Q3" all pass. 168 | 169 | 170 | 4. We want to add a method `getInvocation(interceptorList)` that takes a list of interceptors 171 | as parameter and returns an Invocation which when it is called will call the first interceptor 172 | with as last argument an Invocation allowing to call the second interceptor, etc. 173 | The last invocation will call the method on the instance with the arguments. 174 | Because each Invocation need to know the next Invocation, the chained list of Invocation 175 | need to be constructed from the last one to the first one. 176 | To loop over the interceptors in reverse order, you can use the method `List.reversed()` 177 | which return a reversed list without moving the elements of the initial list. 178 | Add the method `getInvocation`. 179 | Check that the tests in the nested class "Q4" all pass. 180 | 181 | 182 | 5. We know want to change the implementation to only uses interceptor internally 183 | and rewrite the method `addAroundAdvice` to use an interceptor that will calls 184 | the around advice. 185 | Change the implementation of `addAroundAdvice` to use an interceptor, and modify the 186 | code of `createProxy` to use interceptors instead of advices. 187 | Check that the tests in the nested class "Q5" all pass. 188 | Note: given that the method `findAdvices` is now useless, the test Q2.findAdvices() should be commented. 189 | 190 | 191 | 6. Add a cache avoiding recomputing a new `Invocation` each time a method is called. 192 | When the cache should be invalidated ? Change the code to invalidate the cache when necessary. 193 | Check that the tests in the nested class "Q6" all pass. 194 | 195 | 196 | 7. We currently support only annotations on methods, we want to be able to intercept methods if the annotation 197 | is not only declared on that method but on the declaring interface of that method or on one of the parameter 198 | of that method. 199 | Modify the method `findInterceptors(method)`. 200 | Check that the tests in the nested class "Q7" all pass. 201 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /orm/src/main/java/com/github/forax/framework/orm/ORM.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.framework.orm; 2 | 3 | import javax.sql.DataSource; 4 | import java.beans.BeanInfo; 5 | import java.beans.Introspector; 6 | import java.beans.PropertyDescriptor; 7 | import java.io.Serial; 8 | import java.lang.reflect.Constructor; 9 | import java.lang.reflect.ParameterizedType; 10 | import java.lang.reflect.Proxy; 11 | import java.sql.Connection; 12 | import java.sql.ResultSet; 13 | import java.sql.SQLException; 14 | import java.sql.Statement; 15 | import java.util.ArrayList; 16 | import java.util.Arrays; 17 | import java.util.List; 18 | import java.util.Locale; 19 | import java.util.Map; 20 | import java.util.Objects; 21 | import java.util.StringJoiner; 22 | import java.util.stream.Stream; 23 | 24 | public final class ORM { 25 | private ORM() { 26 | throw new AssertionError(); 27 | } 28 | 29 | @FunctionalInterface 30 | public interface TransactionBlock { 31 | void run() throws SQLException; 32 | } 33 | 34 | private static final Map, String> TYPE_MAPPING = Map.of( 35 | int.class, "INTEGER", 36 | Integer.class, "INTEGER", 37 | long.class, "BIGINT", 38 | Long.class, "BIGINT", 39 | String.class, "VARCHAR(255)" 40 | ); 41 | 42 | private static Class findBeanTypeFromRepository(Class repositoryType) { 43 | var repositorySupertype = Arrays.stream(repositoryType.getGenericInterfaces()) 44 | .flatMap(superInterface -> { 45 | if (superInterface instanceof ParameterizedType parameterizedType 46 | && parameterizedType.getRawType() == Repository.class) { 47 | return Stream.of(parameterizedType); 48 | } 49 | return null; 50 | }) 51 | .findFirst() 52 | .orElseThrow(() -> new IllegalArgumentException("invalid repository interface " + repositoryType.getName())); 53 | var typeArgument = repositorySupertype.getActualTypeArguments()[0]; 54 | if (typeArgument instanceof Class beanType) { 55 | return beanType; 56 | } 57 | throw new IllegalArgumentException("invalid type argument " + typeArgument + " for repository interface " + repositoryType.getName()); 58 | } 59 | 60 | private static class UncheckedSQLException extends RuntimeException { 61 | @Serial 62 | private static final long serialVersionUID = 42L; 63 | 64 | private UncheckedSQLException(SQLException cause) { 65 | super(cause); 66 | } 67 | 68 | @Override 69 | public SQLException getCause() { 70 | return (SQLException) super.getCause(); 71 | } 72 | } 73 | 74 | 75 | // --- do not change the code above 76 | 77 | private static final ThreadLocal CONNECTION_THREAD_LOCAL = new ThreadLocal<>(); 78 | 79 | public static void transaction(DataSource dataSource, TransactionBlock block) throws SQLException { 80 | Objects.requireNonNull(dataSource); 81 | Objects.requireNonNull(block); 82 | try(var connection = dataSource.getConnection()) { 83 | connection.setAutoCommit(false); 84 | CONNECTION_THREAD_LOCAL.set(connection); 85 | try { 86 | block.run(); 87 | connection.commit(); 88 | } catch(RuntimeException | SQLException e) { 89 | var cause = (e instanceof UncheckedSQLException unchecked)? unchecked.getCause(): e; 90 | try { 91 | connection.rollback(); 92 | } catch(SQLException suppressed) { 93 | cause.addSuppressed(suppressed); 94 | } 95 | throw Utils.rethrow(cause); 96 | } finally{ 97 | CONNECTION_THREAD_LOCAL.remove(); 98 | } 99 | } 100 | } 101 | 102 | // package private for tests 103 | static Connection currentConnection() { 104 | var connection = CONNECTION_THREAD_LOCAL.get(); 105 | if (connection == null) { 106 | throw new IllegalStateException("no connection available"); 107 | } 108 | return connection; 109 | } 110 | 111 | // package private for tests 112 | static String findTableName(Class beanType) { 113 | var table = beanType.getAnnotation(Table.class); 114 | var name = table == null? beanType.getSimpleName(): table.value(); 115 | return name.toUpperCase(Locale.ROOT); 116 | } 117 | 118 | // package private for tests 119 | static String findColumnName(PropertyDescriptor property) { 120 | var getter = property.getReadMethod(); 121 | var column = getter.getAnnotation(Column.class); 122 | var name = column == null? property.getName(): column.value(); 123 | return name.toUpperCase(Locale.ROOT); 124 | } 125 | 126 | private static String createTableQuery(Class beanType) { 127 | var beanInfo = Utils.beanInfo(beanType); 128 | var joiner = new StringJoiner(",\n", "(\n", "\n)"); 129 | for(var property: beanInfo.getPropertyDescriptors()) { 130 | var propertyName = property.getName(); 131 | if (propertyName.equals("class")) { // skip, not user defined 132 | continue; 133 | } 134 | var columnName = findColumnName(property); 135 | var propertyType = property.getPropertyType(); 136 | var typeName = TYPE_MAPPING.get(propertyType); 137 | if (typeName == null) { 138 | throw new UnsupportedOperationException("unknown type mapping for type " + propertyType.getName()); 139 | } 140 | var nullable = propertyType.isPrimitive()? " NOT NULL": ""; 141 | var getter = property.getReadMethod(); 142 | var autoincrement = getter.isAnnotationPresent(GeneratedValue.class)? " AUTO_INCREMENT": ""; 143 | joiner.add(columnName + ' ' + typeName + nullable + autoincrement); 144 | if (getter.isAnnotationPresent(Id.class)) { 145 | joiner.add("PRIMARY KEY (" + columnName + ')'); 146 | } 147 | } 148 | var tableName = findTableName(beanType); 149 | return "CREATE TABLE " + tableName + joiner + ";"; 150 | } 151 | 152 | public static void createTable(Class beanType) throws SQLException { 153 | var sqlQuery = createTableQuery(beanType); 154 | //System.err.println(sqlQuery); 155 | var connection = currentConnection(); 156 | try(var statement = connection.createStatement()) { 157 | statement.executeUpdate(sqlQuery); 158 | } 159 | } 160 | 161 | // package private for tests 162 | static Object toEntityClass(ResultSet resultSet, BeanInfo beanInfo, Constructor constructor) throws SQLException { 163 | var instance = Utils.newInstance(constructor); 164 | for(var property: beanInfo.getPropertyDescriptors()) { 165 | var propertyName = property.getName(); 166 | if (propertyName.equals("class")) { 167 | continue; 168 | } 169 | var value = resultSet.getObject(propertyName); 170 | Utils.invokeMethod(instance, property.getWriteMethod(), value); 171 | } 172 | return instance; 173 | } 174 | 175 | // package private for tests 176 | static List findAll(Connection connection, String sqlQuery, BeanInfo beanInfo, Constructor constructor, Object... args) throws SQLException { 177 | var list = new ArrayList<>(); 178 | try(var statement = connection.prepareStatement(sqlQuery)) { 179 | //System.err.println(sqlQuery); 180 | if (args != null) { // if no argument 181 | for (var i = 0; i < args.length; i++) { 182 | statement.setObject(i + 1, args[i]); 183 | } 184 | } 185 | try(var resultSet = statement.executeQuery()) { 186 | while(resultSet.next()) { 187 | var instance = toEntityClass(resultSet, beanInfo, constructor); 188 | list.add(instance); 189 | } 190 | } 191 | } 192 | return list; 193 | } 194 | 195 | // package private for tests 196 | static PropertyDescriptor findId(Class beanType, BeanInfo beanInfo) { 197 | return Arrays.stream(beanInfo.getPropertyDescriptors()) 198 | .filter(property -> property.getReadMethod().isAnnotationPresent(Id.class)) 199 | .findFirst() 200 | .orElse(null); 201 | } 202 | 203 | // package private for tests 204 | static PropertyDescriptor findProperty(Class beanType, BeanInfo beanInfo, String propertyName) { 205 | return Arrays.stream(beanInfo.getPropertyDescriptors()) 206 | .filter(property -> property.getName().equals(propertyName)) 207 | .findFirst() 208 | .orElseThrow(() -> {throw new IllegalStateException("no property " + propertyName + " found for type " + beanType.getName()); }); 209 | } 210 | 211 | @SuppressWarnings("resource") 212 | public static > R createRepository(Class type) { 213 | var beanType = findBeanTypeFromRepository(type); 214 | var beanInfo = Utils.beanInfo(beanType); 215 | var tableName = findTableName(beanType); 216 | var constructor = Utils.defaultConstructor(beanType); 217 | var idProperty = findId(beanType, beanInfo); 218 | return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type }, (proxy, method, args) -> { 219 | var connection = currentConnection(); 220 | var name = method.getName(); 221 | try { 222 | return switch(name) { 223 | case "findAll" -> { 224 | var sqlQuery = "SELECT * FROM " + tableName; 225 | yield findAll(connection, sqlQuery, beanInfo, constructor); 226 | } 227 | case "findById" -> { 228 | var sqlQuery = "SELECT * FROM " + tableName + " WHERE " + idProperty.getName() + " = ?"; 229 | yield findAll(connection, sqlQuery, beanInfo, constructor, args[0]).stream().findFirst(); 230 | } 231 | case "save" -> save(connection, tableName, beanInfo, args[0], idProperty); 232 | case "equals", "hashCode", "toString" -> throw new UnsupportedOperationException("" + method); 233 | default -> { 234 | var query = method.getAnnotation(Query.class); 235 | if (query != null) { 236 | yield findAll(connection, query.value(), beanInfo, constructor, args); 237 | } 238 | if (name.startsWith("findBy")) { 239 | var propertyName = Introspector.decapitalize(name.substring(6)); 240 | var property = findProperty(beanType, beanInfo, propertyName); 241 | var sqlQuery = "SELECT * FROM " + tableName + " WHERE " + property.getName() + " = ?"; 242 | yield findAll(connection, sqlQuery, beanInfo, constructor, args[0]).stream().findFirst(); 243 | } 244 | throw new IllegalStateException("unknown method " + method); 245 | } 246 | }; 247 | } catch(SQLException e) { 248 | throw new UncheckedSQLException(e); 249 | } 250 | })); 251 | } 252 | 253 | // package private for tests 254 | static String createSaveQuery(String tableName, BeanInfo beanInfo) { 255 | var values = new StringJoiner(", ", "(", ")"); 256 | var columns = new StringJoiner(", ", "(", ")"); 257 | for(var property: beanInfo.getPropertyDescriptors()) { 258 | var propertyName = property.getName(); 259 | if (propertyName.equals("class")) { 260 | continue; 261 | } 262 | values.add("?"); 263 | columns.add(propertyName); 264 | } 265 | return "MERGE INTO " + tableName + " " + columns + " VALUES " + values + ";"; 266 | } 267 | 268 | static Object save(Connection connection, String tableName, BeanInfo beanInfo, Object bean, PropertyDescriptor idProperty) throws SQLException { 269 | String sqlQuery = createSaveQuery(tableName, beanInfo); 270 | //System.err.println(sqlQuery); 271 | 272 | try(var statement = connection.prepareStatement(sqlQuery, Statement.RETURN_GENERATED_KEYS)) { 273 | var index = 1; 274 | for(var property: beanInfo.getPropertyDescriptors()) { 275 | if (property.getName().equals("class")) { 276 | continue; 277 | } 278 | statement.setObject(index++, Utils.invokeMethod(bean, property.getReadMethod())); 279 | } 280 | statement.executeUpdate(); 281 | if (idProperty != null) { 282 | try(var resultSet = statement.getGeneratedKeys()) { 283 | if (resultSet.next()) { 284 | var key = resultSet.getObject( 1); 285 | Utils.invokeMethod(bean, idProperty.getWriteMethod(), key); 286 | } 287 | } 288 | } 289 | } 290 | return bean; 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /mapper/README2.md: -------------------------------------------------------------------------------- 1 | # Reading objects from JSON 2 | 3 | The aim of class `JSONReader` is to transform JSON objects in textual form into Java Beans, 4 | or records and JSON array into `java.util.List`. 5 | 6 | First, we register `TypeMatcher`s with `addTypeMatcher(typeMatcher)`, that recognize types like records or list and 7 | provide a specific `Collector` which is able to create objects populated with the values from the JSON text. 8 | Then we call `parseJSON(text, type)` with a JSON text, and a type that will be used to decode the JSON text. 9 | 10 | Here is an example using a record 11 | ```java 12 | record Person(String name, int age) {} 13 | 14 | var reader = new JSONReader(); 15 | reader.addTypeMatcher(type -> Optional.of(Utils.erase(type)) 16 | .filter(Class::isRecord) 17 | .map(ObjectBuilder::record)); 18 | var person = reader.parseJSON(""" 19 | { 20 | "name": "Ana", "age": 24 21 | } 22 | """, Person.class); 23 | assertEquals(new Person("Ana", 24), person); 24 | ``` 25 | 26 | In the code above, the `TypeMatcher` recognizes the type that can be erased as records and provides a 27 | specific `ObjectBuilder` named `ObjectBuilder.record(recordClass)` which decodes records. 28 | 29 | 30 | ### Class vs Type 31 | 32 | To decode a JSON array, we want to decode types like `List`. 33 | This type is not representable as a `java.lang.Class` (the corresponding class is only `List` 34 | which is missing the type of the JSON element). So we will use `java.lang.reflect.Type` instead. 35 | See [](../COMPANION.md#java-compiler-generics-attributes) for mode info. 36 | 37 | 38 | ### ToyJSONParser 39 | 40 | Instead or using a real JSON parser like [Jackson](https://github.com/FasterXML/jackson-core), 41 | we are using a toy one. 42 | `ToyJSONParser` is a simple parser that doesn't really implement the JSON format but that's enough 43 | for what we want. The method `ToyJSONParser.parseJSON(text, visitor)` takes a JSON text and calls 44 | the methods of the visitor during the parsing. 45 | 46 | The interface `ToyJSONParser.JSONVisitor` is defined like this 47 | ```java 48 | public interface JSONVisitor { 49 | /** 50 | * Called during the parsing or the content of an object or an array. 51 | * 52 | * @param key the key of the value if inside an object, {@code null} otherwise. 53 | * @param value the value 54 | */ 55 | void value(String key, Object value); 56 | 57 | /** 58 | * Called during the parsing at the beginning of an object. 59 | * @param key the key of the value if inside an object, {@code null} otherwise. 60 | * 61 | * @see #endObject(String) 62 | */ 63 | void startObject(String key); 64 | 65 | /** 66 | * Called during the parsing at the end of an object. 67 | * @param key the key of the value if inside an object, {@code null} otherwise. 68 | * 69 | * @see #startObject(String) 70 | */ 71 | void endObject(String key); 72 | 73 | /** 74 | * Called during the parsing at the beginning of an array. 75 | * @param key the key of the value if inside an object, {@code null} otherwise. 76 | * 77 | * @see #endArray(String) 78 | */ 79 | void startArray(String key); 80 | 81 | /** 82 | * Called during the parsing at the end of an array. 83 | * @param key the key of the value if inside an object, {@code null} otherwise. 84 | * 85 | * @see #startArray(String) 86 | */ 87 | void endArray(String key); 88 | } 89 | ``` 90 | 91 | JSON values are either inside an object or an array, if they are inside an object, the label (the `key`) 92 | is provided. If the values are inside an array, the `key` is `null`. 93 | Given that an object or an array can itself be in an object, the methods `startObject`/`startArray` and 94 | `endObject`/`endArray`also takes a `key` as parameter. 95 | 96 | For example, for the JSON object `{ "kevin": true, "data": [1, 2] }`, the visitor will call `startObject(null)`, 97 | `value("kevin", true)`, `startArray("data")`, `value(null, 1)`, `value(null, 2)`, `endArray("data")` 98 | and `endObject(null)`. 99 | 100 | 101 | ### ObjectBuilder 102 | 103 | A `JSONReader` needs a way to create an empty object/list, to populate it with the values and then return it. 104 | To represent those operations, we are using an abstract type named `ObjectBuiler`. 105 | Unlike a mutable builder that would store the intermediary object inside itself, here we are using 106 | a pure functional representation similar to the Collector class of Java. 107 | 108 | An object builder abstract the way to create an object/list using a supplier. 109 | The value are inserted into the object using a populater. 110 | At the end, the object is transformed to another one (maybe non-mutable) using a finisher. 111 | In order to propagate the type, it also has a qualifier, a function that return the type of a `key` 112 | 113 | An object builder is composed of 4 functions 114 | - a *typeProvider* that returns the `Type` of a key `(key) -> Type` 115 | - a *supplier* that creates a data `() -> data` 116 | - a *populater* that inserts the key / value into the data `(data, key, value) -> void` 117 | - a *finisher* that transform the data into an object `(data) -> Object` 118 | 119 | ### TypeMatcher 120 | 121 | Now that we have an object builder, we need to answer to the question, which object builder to associate to 122 | a peculiar `Type`. This is the role of the `TypeMatcher`. It indicates for a type if it knows 123 | an object builder encapsulated as an Optional or if it does not know this kind of type and returns 124 | `Optional.empty()`. 125 | 126 | ```java 127 | @FunctionalInterface 128 | public interface TypeMatcher { 129 | Optional> match(Type type); 130 | } 131 | ``` 132 | 133 | 134 | ## Let's implement it 135 | 136 | The unit tests are in [JSONReaderTest.java](src/test/java/com/github/forax/framework/mapper/JSONReaderTest.java) 137 | 138 | 1. Let's start small, in the class `JSONReader` write a method `parseJSON(text, class)` that takes 139 | a JSON text, and the class of a Java Beans and returns an instance of the class with 140 | initialized by calling the setters corresponding to the JSON keys. 141 | For now, let's pretend that the JSON can not itself store another JSON Object and 142 | that there is no JSON Array. 143 | 144 | For example, with the JSON object `{ "foo": 3, "bar": 4 }`, the method `parseJSON(text, class)` 145 | creates an instance of the class using the default constructor, then for the key "foo" calls 146 | the setter `setFoo(3)` and for the key "bar" calls the setter `setBar(4)` and returns 147 | the initialized instance. 148 | Then check that the tests in the nested class "Q1" all pass. 149 | 150 | Note: you can use a type variable 'T' to indicate that the type of the class and the return type 151 | of `parseJSON` are the same. 152 | Note2: to avoid to find the setter in an array of properties each time there is a value, 153 | precompute a `Map` that associate a property name to the property. 154 | We store this map is a record `BeanData` and cache it in a `ClassValue`. 155 | 156 | 2. We now want to support a JSON object defined inside a JSON object. 157 | For that, we need a stack that stores a pair of `BeanData` and bean instance 158 | (otherwise we will not know which setter to call on which bean when we come back in a `endObject()`). 159 | We call this pair, `Context` defined using a record 160 | ```java 161 | private record Context(BeanData beanData, Object result) { } 162 | ``` 163 | Change the code of `parseJSON(text, class)` to handle the recursive definition of JSON objects 164 | and check that the tests in the nested class "Q2" all pass. 165 | 166 | Note: in Java, the class 167 | [ArrayDeque](https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/util/ArrayDeque.html) 168 | can act as a stack using the methods `peek()`, `push()` and `pop()` 169 | 170 | 171 | 3. We now want to abstract the code to support other model than the Java bean model. 172 | For that we introduce a record `ObjectBuilder` that let users define how to retrieve the type from a key 173 | (`typeProvider`), how to create a temporary object (`supplier`), how to store value into the temporary object 174 | (`populater`) and how to create an immutable version of the temporary objet (`finisher`). 175 | ```java 176 | public record ObjectBuilder(Function typeProvider, 177 | Supplier supplier, 178 | Populater populater, 179 | Function finisher) { 180 | public interface Populater { 181 | void populate(T instance, String key, Object value); 182 | } 183 | } 184 | ``` 185 | Before changing the code of `parseJSON(text, class)` to use an object builder, let's first create an `ObjectBuilder` 186 | for the Java Beans. For that, we will create a static method in `ObjectBuilder` named `bean(beanClass)` that takes 187 | a class of a bean as parameter and return a `ObjectBuilder` able to create a Java bean instance 188 | and populate it (a bean is inherently mutable, thus the finisher will be the identity function). 189 | 190 | On the method `ObjectBuilder.bean(beanClass)` is created, we can rewrite the code of `parseJSON(text, class)` to use 191 | an `ObjectBuilder` instead of a `BeanData`. So the record `Context` is now defined as 192 | ```java 193 | private record Context(ObjectBuilder objectBuilder, Object result) {} 194 | ``` 195 | 196 | And then checks that the tests in the nested class "Q3" all pass. 197 | 198 | 199 | 4. You can notice that an `ObjectBuilder` works on `Type` and not on `Class`, 200 | so we can add an overload to the method `parseJSON()` that takes a `Type` as second parameter 201 | instead of a `Class` so the code will work for all `Type`s. 202 | Fortunately, given that a `Class` is a `Type`, you do not have to duplicate the code between 203 | the two overloads. 204 | 205 | Modify the code to have the two methods `parseJSON(text, class)` and `parseJSON(text, type)`, 206 | and checks that the tests in the nested class "Q4" all pass. 207 | 208 | 209 | 5. We now want to add a new `ObjectBuilder` tailored for supporting JSON array as `java.util.List`, 210 | for that add a method static `list(elementType)` in `ObjectBuilder` that takes the type of 211 | the element as parameter and returns the object builder typed as `ObjectBuilder>`. 212 | Write the method `list(elementType)` in `ObjectBuilder`. 213 | 214 | We also want to users to be able to add their own object builders, exactly, their own `TypeMatcher`. 215 | ```java 216 | @FunctionalInterface 217 | public interface TypeMatcher { 218 | Optional> match(Type type); 219 | } 220 | ``` 221 | A `TypeMatcher` which specify for a type a collector to use (by returning a collector 222 | wrapped into an `Optional`) or say that the type is not supported by the `TypeMatcher` 223 | (and return `Optional.empty()`). 224 | 225 | To add a `TypeMatcher`, we introduce a method `addTypeMatcher(typeMatcher)`. 226 | The `TypeMatcher`s should be called the reverse order of the insertion order. 227 | If no `TypeMatcher` answer for a `Type`, use the `ObjectBuilder.bean()`. 228 | 229 | We now have two different object builders, so the `Context` need to be parametrized 230 | by the type used by the object builder 231 | ```java 232 | private record Context(ObjectBuilder objectBuilder, T result) 233 | ``` 234 | 235 | Modify the code of `parseJSON(text, expectedType)` accordingly and 236 | checks that the tests in the nested class "Q5" all pass. 237 | 238 | Note: there is a method `List.reversed()` that reverse a List without actually moving the elements. 239 | 240 | 241 | 6. Creating a `Type` is not something easy for a user because all the implementations 242 | are not visible, the indirect way is to ask for a `Type` of 243 | a field, a method or a class/interface by reflection. 244 | Jackson uses a type named `TypeReference` to help people to specify a `Type`. 245 | 246 | The idea is that if you provide an implementation of `TypeRefrence` as an anonymous class, 247 | the compiler inserts in the anonymous class an attribute indicating the generic interfaces, 248 | and you can write a code that extract the type argument of a generic interfaces. 249 | 250 | For example with, 251 | ```java 252 | var typeReference = new TypeRefrence() {}; 253 | ``` 254 | finding `Foo` is equivalent to getting the class of `typeReference`, asking for the first 255 | generic interface (with `getGenericInterfaces()`), seeing it as a `ParameterizedType` and 256 | extracting the first actual type argument (with `getActualTypeArguments()`). 257 | 258 | Implement a new method `parseJSON(text, typeRefrence)` that extract the type argument from 259 | the anonymous class and calls `parseJSON(text, type)` with the type argument. 260 | Checks that the tests in the nested class "Q6" all pass. 261 | 262 | 263 | 7. We now want to support records. 264 | Add a static method `record(recordClass)` in `ObjectBuilder` so the example at the beginning 265 | of this page works. 266 | Checks that the tests in the nested class "Q7" all pass. 267 | 268 | Note: to create a record, you first need to create an array to store all the component values, 269 | in the order of the component, then call the canonical constructor (see `Utils.canonicalConstructor`). 270 | -------------------------------------------------------------------------------- /orm/README.md: -------------------------------------------------------------------------------- 1 | # ORM 2 | 3 | An ORM or Object Relational Mapping is a library that tries to bridge object relational data and 4 | object instances. 5 | 6 | ### Two kinds of ORM 7 | 8 | There are two kinds of object relational mapping libraries, depending on if the object world 9 | is the source of true of the SQL world is the source of true 10 | - Libraries that maps SQL requests to Java objects, 11 | a library like [JDBI](https://jdbi.org/) creates one instance for each row of a query expressed in SQL. 12 | So two different queries on the same table may use two different classes. 13 | - Libraries that maps Java objects to database table. 14 | a library like [Hibernate](https://hibernate.org/) creates multiple instances for each row one by table 15 | from a query expressed in SQL. 16 | 17 | In this exercice, we will implement the first kind, because it's far easier. 18 | 19 | ### DataSource and in memory database 20 | 21 | In Java, a [DataSource](../JDBC.md#datasource-connection-and-transaction) 22 | is an object used to define how to access to a database (login, password, IP adress, port, etc) 23 | and create connections from it. Note, the connection are not direct TCP connections, there are abstract connection 24 | that reuses real TCP connection. So creating a connection / closing a connection is fast. 25 | 26 | Most embedded databases, databases that run inside the JVM not externally as another process or on another server, 27 | have a special test mode that creates the table of the database in memory so tables only exist for one 28 | connection and are cleaned once the connection ends. 29 | 30 | With H2, setting the URL to "jdbc:h2:mem:test" asks for this specific mode. 31 | 32 | ```java 33 | var dataSource = new org.h2.jdbcx.JdbcDataSource(); 34 | dataSource.setURL("jdbc:h2:mem:test"); 35 | ``` 36 | 37 | ### A simple ORM 38 | 39 | This ORM sees each row of a table as an instance of a Java bean. 40 | It can 41 | - create a database table from the bean definition of a Java class 42 | - insert and update a row using the values of a bean instance 43 | - execute a SQL query and returns a list of bean instances 44 | 45 | First we need a way to represent a SQL transaction, that will commit all the modifications at the end 46 | or rollback the transaction is an exception occurs. The method `ORM.transaction` takes a lambda 47 | and run it inside a transaction. 48 | 49 | ```java 50 | var dataSource = new org.h2.jdbcx.JdbcDataSource(); 51 | dataSource.setURL("jdbc:h2:mem:test"); 52 | ORM.transaction(dataSource, () -> { 53 | // start of the transaction 54 | ... 55 | // end of a transaction 56 | }); 57 | ``` 58 | 59 | Then, we need a Java bean, here a `Country` with a field `id` and a field `name`. 60 | The getter of the property `id` is annotated with `@Id` meaning it's the primary key and 61 | `@GeneratedValue`meaning that the database will provide a value if one is not provided. 62 | 63 | ```java 64 | class Country { 65 | private Long id; 66 | private String name; 67 | 68 | public Country() {} 69 | public Country(String name) { 70 | this.name = name; 71 | } 72 | 73 | @Id 74 | @GeneratedValue 75 | public Long getId() { 76 | return id; 77 | } 78 | public void setId(Long id) { 79 | this.id = id; 80 | } 81 | 82 | public String getName() { 83 | return name; 84 | } 85 | public void setName(String name) { 86 | this.name = name; 87 | } 88 | 89 | @Override 90 | public boolean equals(Object o) { 91 | return o instanceof Country country && 92 | Objects.equals(id, country.id) && 93 | Objects.equals(name, country.name); 94 | } 95 | @Override 96 | public int hashCode() { 97 | return Objects.hash(id, name); 98 | } 99 | 100 | @Override 101 | public String toString() { 102 | return "Country { id=" + id + ", name='" + name + "'}"; 103 | } 104 | } 105 | ``` 106 | 107 | We also need to define a repository of `Country`, a repository is an interface with methods allowing to emit SQL queries 108 | specialized for a specific bean. This is how to declare a repository of `Country` with a primary key of type `Long`. 109 | ```java 110 | interface CountryRepository extends Repository { 111 | @Query("SELECT * FROM COUNTRY WHERE NAME LIKE ?") 112 | List findAllWithNameLike(String name); 113 | 114 | Optional findByName(String name); 115 | } 116 | ``` 117 | 118 | This interface contains the user-defined methods 119 | - The method `findAllWithNameLike` is annotated by `@Query` with the SQL query to execute. 120 | - The method `findByName()` has no query attached but by convention if the method name starts with "finBy", 121 | the next word is the property used to find the instance, here this is equivalent to define the 122 | query "SELECT * FROM COUNTRY WHERE name = ?". 123 | 124 | The interface `Repository` is declared like this 125 | ```java 126 | public interface Repository { 127 | List findAll(); // find all instances 128 | Optional findById(ID id); // find a specific instance by its primary key 129 | T save(T entity); // insert/update a specific instance 130 | } 131 | ``` 132 | 133 | By default, a repository provides the methods 134 | - the method `findAll` is equivalent to a "SELECT * FROM COUNTRY" 135 | - the method `findById` is equivalent to a "SELECT * FROM COUNTRY WHERE id = ?" 136 | - the method `save` is equivalent to either an INSERT INTO or an UPDATE. In case of an INSERT INTO, 137 | the setter of the primary key is called if the value of the primary key was not defined. 138 | 139 | The method `ORM.createRepository` returns a [dynamic proxy](../COMPANION.md#dynamic-proxy) that implements 140 | all the methods of the interface `Repository` and all the user defined repository. 141 | 142 | The method `ORM.createTable` creates a new table from the class definition. 143 | 144 | ```java 145 | 146 | 147 | var repository = ORM.createRepository(PersonRepository.class); 148 | ORM.transaction(dataSource, () -> { 149 | createTable(Country.class); 150 | repository.save(new Country("France")); 151 | repository.save(new Country("Spain")); 152 | repository.save(new Country("Australia")); 153 | repository.save(new Country("Austria")); 154 | ... 155 | ``` 156 | 157 | 158 | ## Let's implement it 159 | 160 | 1. We want to be able to create a transaction and be able to retrieve the underlying SQL `Connection` instance 161 | if we are inside the transaction, such as the following code works. 162 | ```java 163 | var dataSource = ... 164 | transaction(dataSource, () -> { 165 | var connection = ORM.currentConnection(); 166 | ... 167 | }); 168 | ``` 169 | Add a method `transaction(dataSource, block)` that takes a DataSource and a lambda, 170 | create a connection from the DataSource, runs the lambda block and close the connection. 171 | The lambda block is a kind of Runnable that can throw a SQLException, thus is defined by the following code 172 | ```java 173 | @FunctionalInterface 174 | public interface TransactionBlock { 175 | void run() throws SQLException; 176 | } 177 | ``` 178 | We also want a non-public method `currentConnection()` that returns the current Connection when called 179 | inside the transaction block or throw an exception otherwise. 180 | In order to store the connection associated to the current thread, you can use the class 181 | [ThreadLocal](../COMPANION.md#threadlocal). 182 | Check that the tests in the nested class "Q1" all pass. 183 | 184 | 185 | 2. Modify the code of `transaction` to implement real SQL transactions. 186 | At the beginning of a transaction, the auto-commit should be set to false. 187 | At the end of a transaction, the method `commit()` should be called on the transaction. 188 | In case of an exception, the method `rollback()` should be called. If the method `rollback()` itself 189 | throw an exception, the initial exception should be rethrown. 190 | Check that the tests in the nested class "Q2" all pass. 191 | 192 | 193 | 3. We now want to implement the method `createTable(beanClass)` that take a class of a Java bean, 194 | find all its properties and uses the current connection to create a [SQL table](../JDBC.md#create-a-table) 195 | with one column per property. 196 | The name of the table is the name of the class apart if the class is annotated with `@Table`, 197 | in that case, it's the value of the annotation. 198 | The name of each column is the name of the property apart if the getter of the property is annotated 199 | with `@Column`, in that case, it's the value of the annotation. 200 | First create a non-public method `findTableName(beanClass)` that takes the class as argument and returns 201 | the name of the table. Then create a non-public method `findColumnName(property)` that takes a PropertyDescriptor 202 | and returns the name of a column. 203 | Then implement the method `createTable(beanClass)` that uses the current connection to create a table 204 | knowing that for now all columns are of type `VARCHAR(255)`. 205 | Check that the tests in the nested class "Q3" all pass. 206 | 207 | 208 | 4. We now want to find for the type of a property (a PropertyDescriptor) the corresponding SQL type. 209 | For that, we have a predefined Map that associate the common Java type with their SQL equivalent. 210 | ```java 211 | private static final Map, String> TYPE_MAPPING = Map.of( 212 | int.class, "INTEGER", 213 | Integer.class, "INTEGER", 214 | long.class, "BIGINT", 215 | Long.class, "BIGINT", 216 | String.class, "VARCHAR(255)" 217 | ); 218 | ``` 219 | We also want that the column that correspond to a primitive type to be declared NOT NULL. 220 | Modify the method `createTable(beanClass)` to declare the column with the right type. 221 | Check that the tests in the nested class "Q4" all pass. 222 | 223 | 224 | 5. We now want to support the annotation `@Id` and `@GeneratedValue`. 225 | `@Ìd`adds after the column, the text "PRIMARY KEY (foo)" if foo is the primary key and 226 | `@GeneratedValue` adds "AUTO_INCREMENT" at the end of a column declaration. 227 | For example, with the class `Country` declared above, the SQL to create the table should be 228 | ```sql 229 | CREATE TABLE COUNTRY( 230 | ID BIGINT AUTO_INCREMENT, 231 | PRIMARY KEY (id), 232 | NAME VARCHAR(255) 233 | ); 234 | ``` 235 | Modify the method `createTable(beanClass)` to add the support of primary key and generated value. 236 | Check that the tests in the nested class "Q5" all pass. 237 | 238 | 239 | 6. In order to implement `createRepository(repositoryType)` we need to create a dynamic proxy 240 | that will switch over the methods of the interface to implement them after having 241 | verified that the call to those methods is done inside a transaction. 242 | For now, we will implement only the method `findAll()` that returns an empty list 243 | For the methods `equals`, `hashCode` and `toString`, we will throw an UnsupportedOperationException 244 | For all other methods, we will throw a IllegalStateException. 245 | Check that the tests in the nested class "Q5" all pass. 246 | 247 | 248 | 7. In order to finish the implementation `repository.findAll()`, we need several helper methods. 249 | - `findBeanTypeFromRepository(repositoryType)`, extract the bean class from 250 | the declaration of a user-defined repository. 251 | For example with a `CountryRepository` defined like this 252 | ```java 253 | interface CountryRepository extends Repository { } 254 | ``` 255 | `findBeanTypeFromRepository(CountryRepository.class)` returns `Country.class`. 256 | This method is already implemented. 257 | - `toEntityClass(resultSet, beanInfo, constructor)` takes the result of a SQL query, 258 | creates a bean instance using the constructor and for each column value, calls the corresponding 259 | property setter and returns the instance. 260 | - `findAll(connection, sqlQuery, beanInfo, constructor)` execute a SQL query as a 261 | [prepared statement](../JDBC.md#statement-and-preparedstatement) 262 | and uses `toEntityClass` to returns a list of instances. 263 | Once those methods are implemented, modify the implementation of `createRepository(repositoryType)` 264 | so `Repository.findAll()` fully work. 265 | Note: if a SQL exception occurs while executing the query, given that the method of the repository do 266 | not declare to throw a SQLException, the exception has to be wrapped into a runtime exception 267 | (you can use `UncheckedSQLException` for that) 268 | Check that the tests in the nested class "Q7" all pass. 269 | 270 | 271 | 8. We now want to implement the method `repository.save(bean)` that take an instance of a bean as parameter 272 | and insert its values into the corresponding table. 273 | For that, we will first implement two helper methods 274 | - `createSaveQuery(tableName, beanInfo)` that generate a [SQL insert](../JDBC.md#insert-data) to add 275 | all the values of as a prepared statement. 276 | - `save(connection, tableName, beanInfo, bean, idProperty)` that generate the SQL insert query using 277 | `createSaveQuery` and execute it with the values of the `bean`. For now, the parameter idProperty 278 | is useless and will always be null. 279 | Note: in SQL, the first column is the column 1 not 0. 280 | Once those methods are implemented, modify the implementation of `createRepository(repositoryType)` 281 | so `Repository.save(bean)` works. 282 | Check that the tests in the nested class "Q8" all pass. 283 | 284 | 285 | 9. We want to improve the method `repository.save(bean)` so a [generated primary key](../JDBC.md#generated-primary-key) 286 | computed when inserting a row is updated in the bean. 287 | For that, we first need a method `findId(beanType, beanInfo)` that returns the property of the primary key 288 | (the one annotated with `@Id`) or null otherwise. 289 | Then we can change the code of `save(connection, tableName, beanInfo, bean, idProperty)` to call the setter 290 | of the `idProperty` when the values are inserted if the property is not null 291 | Modify the implementation of `createRepository(repositoryType)` so `Repository.save(bean)` fully works. 292 | Check that the tests in the nested class "Q9" all pass. 293 | 294 | 295 | 10. We now want to update the value of a row if it already exists in the table. 296 | There is a simple solution for that, uses the SQL request "MERGE INTO" instead of "INSERT INTO". 297 | Check that the tests in the nested class "Q10" all pass. 298 | 299 | 300 | 11. We now want to implement the method `repository.findById(id)`, instead of implementing a new method 301 | to execute a query, we will change the method 302 | `findAll(connection, sqlQuery, beanInfo, constructor, args)` to takes arguments as last parameter 303 | and pass those arguments to the prepared statement. 304 | First, modify `findAll` to takes arguments as parameter, then modify the implementation of 305 | `createRepository(repositoryType)` so `Repository.findById(id)` works. 306 | Check that the tests in the nested class "Q11" all pass. 307 | 308 | 309 | 12. We now want to implement any methods declared in the user defined repository that is annotated 310 | with the annotation `@Query`. The idea, is again to delegate the execution of the query to `findAll`. 311 | Check that the tests in the nested class "Q12" all pass. 312 | 313 | 314 | 13. To finish, we want to implement all methods that start with the prefix "findBy*" followed by the name 315 | of a property. Thos methods takes an argument and returns the first instance that has the value of the property 316 | equals to the argument as an Optional or Optional.empty() if there is no result. 317 | Yet again, here, we can delegate most of the work to `findAll()`. 318 | Note: there is a method `Introspector.decapitalize(name)` to transform a name that starts with 319 | an uppercase letter to a property name). 320 | Check that the tests in the nested class "Q13" all pass. 321 | -------------------------------------------------------------------------------- /mapper/CODE_COMMENTS2.md: -------------------------------------------------------------------------------- 1 | # Code comments 2 | 3 | ### Q1 4 | 5 | ```java 6 | private record BeanData(Constructor constructor, Map propertyMap) { 7 | PropertyDescriptor findProperty(String key) { 8 | var property = propertyMap.get(key); 9 | if (property == null) { 10 | throw new IllegalStateException("unknown key " + key + " for bean " + constructor.getDeclaringClass().getName()); 11 | } 12 | return property; 13 | } 14 | } 15 | 16 | private static final ClassValue BEAN_DATA_CLASS_VALUE = new ClassValue<>() { 17 | @Override 18 | protected BeanData computeValue(Class type) { 19 | var beanInfo = Utils.beanInfo(type); 20 | var constructor = Utils.defaultConstructor(type); 21 | var map = Arrays.stream(beanInfo.getPropertyDescriptors()) 22 | .filter(property -> !property.getName().equals("class")) 23 | .collect(Collectors.toMap(PropertyDescriptor::getName, Function.identity())); 24 | return new BeanData(constructor, map); 25 | } 26 | }; 27 | 28 | public T parseJSON(String text, Class beanClass) { 29 | Objects.requireNonNull(text); 30 | Objects.requireNonNull(beanClass); 31 | var visitor = new ToyJSONParser.JSONVisitor() { 32 | private BeanData beanData; 33 | private Object result; 34 | 35 | @Override 36 | public void value(String key, Object value) { 37 | var property = beanData.findProperty(key); 38 | Utils.invokeMethod(result, property.getWriteMethod(), value); 39 | } 40 | 41 | @Override 42 | public void startObject(String key) { 43 | beanData = BEAN_DATA_CLASS_VALUE.get(beanClass); 44 | result = Utils.newInstance(beanData.constructor); 45 | } 46 | 47 | @Override 48 | public void endObject(String key) { 49 | // do nothing 50 | } 51 | 52 | @Override 53 | public void startArray(String key) { 54 | throw new UnsupportedOperationException("Implemented later"); 55 | } 56 | 57 | @Override 58 | public void endArray(String key) { 59 | throw new UnsupportedOperationException("Implemented later"); 60 | } 61 | }; 62 | ToyJSONParser.parse(text, visitor); 63 | return beanClass.cast(visitor.result); 64 | } 65 | ``` 66 | 67 | ### Q2 68 | 69 | ```java 70 | private record Context(BeanData beanData, Object result) { } 71 | 72 | public T parseJSON(String text, Class beanClass) { 73 | Objects.requireNonNull(text); 74 | Objects.requireNonNull(beanClass); 75 | var stack = new ArrayDeque(); 76 | var visitor = new ToyJSONParser.JSONVisitor() { 77 | private Object result; 78 | 79 | @Override 80 | public void value(String key, Object value) { 81 | var context = stack.peek(); 82 | var property = context.beanData.findProperty(key); 83 | Utils.invokeMethod(context.result, property.getWriteMethod(), value); 84 | } 85 | 86 | @Override 87 | public void startObject(String key) { 88 | var context = stack.peek(); 89 | var type = context == null ? beanClass : context.beanData.findProperty(key).getPropertyType(); 90 | var beanData = BEAN_DATA_CLASS_VALUE.get(type); 91 | var instance = Utils.newInstance(beanData.constructor); 92 | stack.push(new Context(beanData, instance)); 93 | } 94 | 95 | @Override 96 | public void endObject(String key) { 97 | var instance = stack.pop().result; 98 | if (stack.isEmpty()) { 99 | result = instance; 100 | return; 101 | } 102 | var context = stack.peek(); 103 | var property = context.beanData.findProperty(key); 104 | Utils.invokeMethod(context.result, property.getWriteMethod(), instance); 105 | } 106 | 107 | @Override 108 | public void startArray(String key) { 109 | throw new UnsupportedOperationException("Implemented later"); 110 | } 111 | 112 | @Override 113 | public void endArray(String key) { 114 | throw new UnsupportedOperationException("Implemented later"); 115 | } 116 | }; 117 | ToyJSONParser.parse(text, visitor); 118 | return beanClass.cast(visitor.result); 119 | } 120 | ``` 121 | 122 | ### Q3 123 | 124 | ```java 125 | public record ObjectBuilder(Function> typeProvider, 126 | Supplier supplier, 127 | Populater populater, 128 | Function finisher) { 129 | public interface Populater { 130 | void populate(T instance, String key, Object value); 131 | } 132 | 133 | public ObjectBuilder { 134 | Objects.requireNonNull(typeProvider); 135 | Objects.requireNonNull(supplier); 136 | Objects.requireNonNull(populater); 137 | Objects.requireNonNull(finisher); 138 | } 139 | 140 | public static ObjectBuilder bean(Class beanClass) { 141 | Objects.requireNonNull(beanClass); 142 | var beanData = BEAN_DATA_CLASS_VALUE.get(beanClass); 143 | return new ObjectBuilder<>( 144 | key -> beanData.findProperty(key).getPropertyType(), 145 | () -> Utils.newInstance(beanData.constructor), 146 | (instance, key, value) -> { 147 | var property = beanData.findProperty(key); 148 | Utils.invokeMethod(instance, property.getWriteMethod(), value); 149 | }, 150 | Function.identity() 151 | ); 152 | } 153 | } 154 | 155 | private record Context(ObjectBuilder objectBuilder, Object result) {} 156 | 157 | public T parseJSON(String text, Class beanClass) { 158 | Objects.requireNonNull(text); 159 | Objects.requireNonNull(beanClass); 160 | var stack = new ArrayDeque(); 161 | var visitor = new ToyJSONParser.JSONVisitor() { 162 | private Object result; 163 | 164 | @Override 165 | public void value(String key, Object value) { 166 | var context = stack.peek(); 167 | context.objectBuilder.populater.populate(context.result, key, value); 168 | } 169 | 170 | @Override 171 | public void startObject(String key) { 172 | var context = stack.peek(); 173 | var type = context == null ? beanClass : context.objectBuilder.typeProvider.apply(key); 174 | var objectbuilder = ObjectBuilder.bean(type); 175 | stack.push(new Context(objectbuilder, objectbuilder.supplier.get())); 176 | } 177 | 178 | @Override 179 | public void endObject(String key) { 180 | var instance = stack.pop().result; 181 | if (stack.isEmpty()) { 182 | result = instance; 183 | return; 184 | } 185 | var context = stack.peek(); 186 | context.objectBuilder.populater.populate(context.result, key, instance); 187 | } 188 | 189 | @Override 190 | public void startArray(String key) { 191 | throw new UnsupportedOperationException("Implemented later"); 192 | } 193 | 194 | @Override 195 | public void endArray(String key) { 196 | throw new UnsupportedOperationException("Implemented later"); 197 | } 198 | }; 199 | ToyJSONParser.parse(text, visitor); 200 | return beanClass.cast(visitor.result); 201 | } 202 | ``` 203 | 204 | ### Q4 205 | 206 | ```java 207 | public record ObjectBuilder(Function typeProvider, 208 | Supplier supplier, 209 | Populater populater, 210 | Function finisher) { 211 | public interface Populater { 212 | void populate(T instance, String key, Object value); 213 | } 214 | 215 | public ObjectBuilder { 216 | Objects.requireNonNull(typeProvider); 217 | Objects.requireNonNull(supplier); 218 | Objects.requireNonNull(populater); 219 | Objects.requireNonNull(finisher); 220 | } 221 | 222 | public static ObjectBuilder bean(Class beanClass) { 223 | Objects.requireNonNull(beanClass); 224 | var beanData = BEAN_DATA_CLASS_VALUE.get(beanClass); 225 | return new ObjectBuilder<>( 226 | key -> beanData.findProperty(key).getWriteMethod().getGenericParameterTypes()[0], 227 | () -> Utils.newInstance(beanData.constructor), 228 | (instance, key, value) -> { 229 | var property = beanData.findProperty(key); 230 | Utils.invokeMethod(instance, property.getWriteMethod(), value); 231 | }, 232 | Function.identity() 233 | ); 234 | } 235 | } 236 | 237 | private record Context(ObjectBuilder objectBuilder, Object result) {} 238 | 239 | public T parseJSON(String text, Class expectedClass) { 240 | return expectedClass.cast(parseJSON(text, (Type) expectedClass)); 241 | } 242 | 243 | public Object parseJSON(String text, Type expectedType) { 244 | Objects.requireNonNull(text); 245 | Objects.requireNonNull(expectedType); 246 | var stack = new ArrayDeque(); 247 | var visitor = new ToyJSONParser.JSONVisitor() { 248 | private Object result; 249 | 250 | @Override 251 | public void value(String key, Object value) { 252 | var context = stack.peek(); 253 | context.objectBuilder.populater.populate(context.result, key, value); 254 | } 255 | 256 | @Override 257 | public void startObject(String key) { 258 | var context = stack.peek(); 259 | var type = context == null ? expectedType : context.objectBuilder.typeProvider.apply(key); 260 | var objectbuilder = ObjectBuilder.bean(Utils.erase(type)); 261 | stack.push(new Context(objectbuilder, objectbuilder.supplier.get())); 262 | } 263 | 264 | @Override 265 | public void endObject(String key) { 266 | var instance = stack.pop().result; 267 | if (stack.isEmpty()) { 268 | result = instance; 269 | return; 270 | } 271 | var context = stack.peek(); 272 | context.objectBuilder.populater.populate(context.result, key, instance); 273 | } 274 | 275 | @Override 276 | public void startArray(String key) { 277 | throw new UnsupportedOperationException("Implemented later"); 278 | } 279 | 280 | @Override 281 | public void endArray(String key) { 282 | throw new UnsupportedOperationException("Implemented later"); 283 | } 284 | }; 285 | ToyJSONParser.parse(text, visitor); 286 | return visitor.result; 287 | } 288 | ``` 289 | 290 | ### Q5 291 | 292 | We then define the list collector 293 | 294 | ```java 295 | ... 296 | public static ObjectBuilder> list(Type elementType) { 297 | Objects.requireNonNull(elementType); 298 | return new ObjectBuilder<>( 299 | key -> elementType, 300 | ArrayList::new, 301 | (list, key, value) -> list.add(value), 302 | List::copyOf 303 | ); 304 | } 305 | } 306 | ``` 307 | 308 | We add the support of `TypeMatcher`s, the method `addTypeMatcher(typeMatcher)` and `findCollector(type)`. 309 | 310 | ```java 311 | @FunctionalInterface 312 | public interface TypeMatcher { 313 | Optional> match(Type type); 314 | } 315 | 316 | private final ArrayList typeMatchers = new ArrayList<>(); 317 | 318 | public void addTypeMatcher(TypeMatcher typeMatcher) { 319 | Objects.requireNonNull(typeMatcher); 320 | typeMatchers.add(typeMatcher); 321 | } 322 | 323 | ObjectBuilder findObjectBuilder(Type type) { 324 | return typeMatchers.reversed().stream() 325 | .flatMap(typeMatcher -> typeMatcher.match(type).stream()) 326 | .findFirst() 327 | .orElseGet(() -> ObjectBuilder.bean(Utils.erase(type))); 328 | } 329 | ``` 330 | 331 | And we add another `parseJSON(text, type)` overload with a Type instead of a Class. 332 | Inside `start(key)`, we call `findObjectBuilder(type)`. 333 | 334 | ```java 335 | private record Context(ObjectBuilder objectBuilder, T result) { 336 | private static Context create(ObjectBuilder objectBuilder) { 337 | return new Context<>(objectBuilder, objectBuilder.supplier.get()); 338 | } 339 | 340 | private void populate(String key, Object value) { 341 | objectBuilder.populater.populate(result, key, value); 342 | } 343 | 344 | private Object finish() { 345 | return objectBuilder.finisher.apply(result); 346 | } 347 | } 348 | 349 | public T parseJSON(String text, Class expectedClass) { 350 | return expectedClass.cast(parseJSON(text, (Type) expectedClass)); 351 | } 352 | 353 | public Object parseJSON(String text, Type expectedType) { 354 | Objects.requireNonNull(text); 355 | Objects.requireNonNull(expectedType); 356 | var stack = new ArrayDeque>(); 357 | var visitor = new ToyJSONParser.JSONVisitor() { 358 | private Object result; 359 | 360 | @Override 361 | public void value(String key, Object value) { 362 | var context = stack.peek(); 363 | context.populate(key, value); 364 | } 365 | 366 | @Override 367 | public void startObject(String key) { 368 | var context = stack.peek(); 369 | var type = context == null ? expectedType : context.objectBuilder.typeProvider.apply(key); 370 | var objectbuilder = findObjectBuilder(type); 371 | stack.push(Context.create(objectbuilder)); 372 | } 373 | 374 | @Override 375 | public void endObject(String key) { 376 | var instance = stack.pop().finish(); 377 | if (stack.isEmpty()) { 378 | result = instance; 379 | return; 380 | } 381 | var context = stack.peek(); 382 | context.populate(key, instance); 383 | } 384 | 385 | @Override 386 | public void startArray(String key) { 387 | startObject(key); 388 | } 389 | 390 | @Override 391 | public void endArray(String key) { 392 | endObject(key); 393 | } 394 | }; 395 | ToyJSONParser.parse(text, visitor); 396 | return visitor.result; 397 | } 398 | ``` 399 | 400 | ### Q6 401 | 402 | ```java 403 | public interface TypeReference { } 404 | 405 | private static Type findElemntType(TypeReference typeReference) { 406 | var typeReferenceType = Arrays.stream(typeReference.getClass().getGenericInterfaces()) 407 | .flatMap(t -> t instanceof ParameterizedType parameterizedType? Stream.of(parameterizedType): null) 408 | .filter(t -> t.getRawType() == TypeReference.class) 409 | .findFirst() 410 | .orElseThrow(() -> new IllegalArgumentException("invalid TypeReference " + typeReference)); 411 | return typeReferenceType.getActualTypeArguments()[0]; 412 | } 413 | 414 | public T parseJSON(String text, TypeReference typeReference) { 415 | var elementType = findElemntType(typeReference); 416 | @SuppressWarnings("unchecked") 417 | var result = (T) parseJSON(text, elementType); 418 | return result; 419 | } 420 | ``` 421 | 422 | ### Q7 423 | 424 | ```java 425 | public record ObjectBuilder(Function typeProvider, 426 | Supplier supplier, 427 | Populater populater, 428 | Function finisher) { 429 | public interface Populater { 430 | void populate(T instance, String key, Object value); 431 | } 432 | 433 | ... 434 | 435 | public static ObjectBuilder record(Class recordClass) { 436 | Objects.requireNonNull(recordClass); 437 | var components = recordClass.getRecordComponents(); 438 | var map = IntStream.range(0, components.length) 439 | .boxed() 440 | .collect(Collectors.toMap(i -> components[i].getName(), Function.identity())); 441 | var constructor = Utils.canonicalConstructor(recordClass, components); 442 | return new ObjectBuilder<>( 443 | key -> components[map.get(key)].getGenericType(), 444 | () -> new Object[components.length], 445 | (array, key, value) -> array[map.get(key)] = value, 446 | array -> Utils.newInstance(constructor, array) 447 | ); 448 | } 449 | } 450 | ``` 451 | --------------------------------------------------------------------------------