diff --git a/3-0-spring-framework/bean-post-processor/README.MD b/3-0-spring-framework/bean-post-processor/README.MD
new file mode 100644
index 0000000..25a9dbc
--- /dev/null
+++ b/3-0-spring-framework/bean-post-processor/README.MD
@@ -0,0 +1,38 @@
+#
Bean Post Processor exercise
+Improve your *Spring* advanced skills đź’Ş
+### Task
+Implement automatic String trimming functionality. So if I pass " Hello " as an argument to my bean’s method, or return it from the bean’s method, it gets automatically trimmed to "Hello".
+
+- create annotation @Trimmed that you can put on class
+
+- create annotation @EnableStringTrimming that will enable automatic String trimming for all beans that are annotated with @Trimmed
+
+- create TrimmedAnnotationBeanPostProcessor that will check for beans that are marked with @Trimmed,
+
+- create a proxy of those classes, and override methods. Proxy methods should:
+
+ - trim all String arguments
+
+ - trim all String return values
+
+- extract TrimmedAnnotationBeanPostProcessor into a separate StringTrimmingConfiguration that is imported by @EnableStringTrimming
+
+To verify your configuration, run `BeanPostProcessorTest.java`
+
+
+### Pre-conditions âť—
+You're supposed to be familiar with *Spring*
+
+### How to start âť“
+* Just clone the repository and start implementing the **todo** section, verify your changes by running tests
+* If you don't have enough knowledge about this domain, check out the [links below](#Related-materials-ℹ)
+* Don't worry if you got stuck, checkout the **exercise/completed** branch and see the final implementation
+
+### Related materials ℹ
+ todo
+
+---
+#### 🆕 First time here? – [See Introduction](https://github.com/bobocode-projects/java-fundamentals-course/tree/main/0-0-intro#introduction)
+
+##
+

\ No newline at end of file
diff --git a/3-0-spring-framework/bean-post-processor/pom.xml b/3-0-spring-framework/bean-post-processor/pom.xml
new file mode 100644
index 0000000..dddb1d2
--- /dev/null
+++ b/3-0-spring-framework/bean-post-processor/pom.xml
@@ -0,0 +1,27 @@
+
+
+
+ 3-0-spring-framework
+ com.bobocode
+ 1.0-SNAPSHOT
+
+ 4.0.0
+
+ bean-post-processor
+
+
+ 11
+ 11
+
+
+
+
+ cglib
+ cglib
+ 3.3.0
+
+
+
+
\ No newline at end of file
diff --git a/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/annotation/EnableStringTrimming.java b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/annotation/EnableStringTrimming.java
new file mode 100644
index 0000000..4ae5384
--- /dev/null
+++ b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/annotation/EnableStringTrimming.java
@@ -0,0 +1,14 @@
+package com.bobocode.annotation;
+
+import com.bobocode.util.StringTrimmingConfiguration;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Configuration
+@Import(StringTrimmingConfiguration.class)
+public @interface EnableStringTrimming {
+}
diff --git a/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/annotation/Trimmed.java b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/annotation/Trimmed.java
new file mode 100644
index 0000000..ff1c423
--- /dev/null
+++ b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/annotation/Trimmed.java
@@ -0,0 +1,8 @@
+package com.bobocode.annotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Trimmed {
+}
diff --git a/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/config/ApplicationConfig.java b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/config/ApplicationConfig.java
new file mode 100644
index 0000000..ead5c5b
--- /dev/null
+++ b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/config/ApplicationConfig.java
@@ -0,0 +1,16 @@
+package com.bobocode.config;
+
+import com.bobocode.annotation.EnableStringTrimming;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Configure the application for using @Trimmed annotation and corresponding logic.
+ */
+
+@Configuration
+@ComponentScan(basePackages = "com.bobocode.service")
+@EnableStringTrimming
+public class ApplicationConfig {
+
+}
diff --git a/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/service/TextService.java b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/service/TextService.java
new file mode 100644
index 0000000..549e43d
--- /dev/null
+++ b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/service/TextService.java
@@ -0,0 +1,23 @@
+package com.bobocode.service;
+
+import com.bobocode.annotation.Trimmed;
+import org.springframework.stereotype.Service;
+
+/**
+ * Configure the service to provide trimming process.
+ */
+
+@Service
+@Trimmed
+public class TextService {
+ public String savedText;
+ private final static String AVAILABLE_TEXT = " Who cares about tabbing? ";
+
+ public void saveText(String text){
+ savedText = text;
+ }
+
+ public String getAvailableText(){
+ return AVAILABLE_TEXT;
+ }
+}
diff --git a/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/util/StringTrimmingConfiguration.java b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/util/StringTrimmingConfiguration.java
new file mode 100644
index 0000000..f566114
--- /dev/null
+++ b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/util/StringTrimmingConfiguration.java
@@ -0,0 +1,14 @@
+package com.bobocode.util;
+
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class StringTrimmingConfiguration {
+
+ @Bean
+ public BeanPostProcessor trimmedAnnotationBeanPostProcessor(){
+ return new TrimmedAnnotationBeanPostProcessor();
+ }
+}
diff --git a/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/util/TrimmedAnnotationBeanPostProcessor.java b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/util/TrimmedAnnotationBeanPostProcessor.java
new file mode 100644
index 0000000..22c143d
--- /dev/null
+++ b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/util/TrimmedAnnotationBeanPostProcessor.java
@@ -0,0 +1,42 @@
+package com.bobocode.util;
+
+import com.bobocode.annotation.Trimmed;
+import net.sf.cglib.proxy.Enhancer;
+import net.sf.cglib.proxy.MethodInterceptor;
+import net.sf.cglib.proxy.MethodProxy;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+
+public class TrimmedAnnotationBeanPostProcessor implements BeanPostProcessor {
+ @Override
+ public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+ Class> beanClass = bean.getClass();
+ if (beanClass.isAnnotationPresent(Trimmed.class)) {
+ return createTrimmedProxy(beanClass);
+ }
+ return bean;
+ }
+
+ private Object createTrimmedProxy(Class> beanClass) {
+ var enhancer = new Enhancer();
+ enhancer.setSuperclass(beanClass);
+ enhancer.setInterfaces(beanClass.getInterfaces());
+ MethodInterceptor interceptor = (Object obj, Method method, Object[] args, MethodProxy proxy) -> {
+ Object[] arguments = Arrays.stream(args)
+ .filter(arg -> arg instanceof String)
+ .map(arg -> ((String) arg).trim())
+ .toArray();
+
+ if (method.getReturnType().equals(String.class)) {
+ String invokeSuper = (String) proxy.invokeSuper(obj, arguments);
+ return (String) invokeSuper.trim();
+ }
+ return proxy.invokeSuper(obj, arguments);
+ };
+ enhancer.setCallback(interceptor);
+ return beanClass.cast(enhancer.create());
+ }
+}
diff --git a/3-0-spring-framework/bean-post-processor/src/test/java/com/bobocode/BeanPostProcessorTest.java b/3-0-spring-framework/bean-post-processor/src/test/java/com/bobocode/BeanPostProcessorTest.java
new file mode 100644
index 0000000..80993d3
--- /dev/null
+++ b/3-0-spring-framework/bean-post-processor/src/test/java/com/bobocode/BeanPostProcessorTest.java
@@ -0,0 +1,289 @@
+package com.bobocode;
+
+import com.bobocode.config.ApplicationConfig;
+import com.bobocode.service.TextService;
+import com.bobocode.testService.NotTrimmedTextService;
+import lombok.SneakyThrows;
+import org.junit.jupiter.api.*;
+import org.reflections.Reflections;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.stereotype.Component;
+import org.springframework.stereotype.Service;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@SpringJUnitConfig(BeanPostProcessorTest.TestConfig.class)
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class BeanPostProcessorTest {
+ @Configuration
+ @ComponentScan(basePackages = "com.bobocode")
+ static class TestConfig {
+ }
+
+ @Autowired
+ TextService textService;
+
+ @Autowired
+ NotTrimmedTextService notTrimmedTextService;
+
+ private static final String ANNOTATIONS_PACKAGE = "com.bobocode.annotation";
+ private static final String UTIL_PACKAGE = "com.bobocode.util";
+ private static final String TRIMMED = "Trimmed";
+ private static final String ENABLE_STRING_TRIMMING = "EnableStringTrimming";
+ private static final String TRIMMED_ANNOTATION_BEAN_POST_PROCESSOR = "TrimmedAnnotationBeanPostProcessor";
+ private static final String STRING_TRIMMING_CONFIGURATION = "StringTrimmingConfiguration";
+
+ private Reflections annotationReflections;
+ private Reflections configurationReflections;
+
+ @BeforeEach
+ @SneakyThrows
+ public void init() {
+ annotationReflections = new Reflections(ANNOTATIONS_PACKAGE);
+ configurationReflections = new Reflections(UTIL_PACKAGE);
+ }
+
+ @Test
+ @Order(1)
+ @DisplayName("@Trimmed annotation exists")
+ void trimmedAnnotationExists() throws ClassNotFoundException {
+ Class.forName(ANNOTATIONS_PACKAGE + "." + TRIMMED);
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("Trimmed annotation class is marked by @Retention with \"Runtime\" policy")
+ void trimmedAnnotationClassIsMarkedByRetentionRuntimePolicy() {
+ Optional> annotationClass = getAnnotationClassMarketByRetentionRuntimePolicy(TRIMMED);
+
+ assertThat(annotationClass).isPresent();
+ }
+
+ @Test
+ @Order(3)
+ @DisplayName("@EnableStringTrimming annotation exists")
+ void enableStringTrimmingAnnotationExists() throws ClassNotFoundException {
+ Class.forName(ANNOTATIONS_PACKAGE + "." + ENABLE_STRING_TRIMMING);
+ }
+
+ @Test
+ @Order(4)
+ @DisplayName("EnableStringTrimming annotation class is marked by @Retention with \"Runtime\" policy")
+ void enableStringTrimmingAnnotationClassIsMarkedByRetentionRuntimePolicy() {
+ Optional> annotationClass = getAnnotationClassMarketByRetentionRuntimePolicy(ENABLE_STRING_TRIMMING);
+
+ assertThat(annotationClass).isPresent();
+ }
+
+ @Test
+ @Order(5)
+ @DisplayName("ApplicationConfig class is marked by @Configuration annotation")
+ void applicationConfigurationClassExists() {
+ Configuration annotation = ApplicationConfig.class.getAnnotation(Configuration.class);
+
+ assertThat(annotation).isNotNull();
+ }
+
+ @Test
+ @Order(6)
+ @DisplayName("Package scanning is configured for service package")
+ void applicationComponentScanIsConfiguredForService() {
+ ComponentScan annotation = ApplicationConfig.class.getAnnotation(ComponentScan.class);
+
+ Optional scannedPackage = Arrays.stream(annotation.basePackages())
+ .filter(p -> p.equals("com.bobocode.service"))
+ .findAny();
+ assertThat(annotation).isNotNull();
+ assertThat(scannedPackage).isPresent();
+ }
+
+ @Test
+ @Order(7)
+ @DisplayName("TextService is marked by @Service annotation")
+ void textServiceIcMarkedAsComponent() {
+ Service annotation = TextService.class.getAnnotation(Service.class);
+
+ assertThat(annotation).isNotNull();
+ }
+
+ @Test
+ @Order(8)
+ @DisplayName("TextService is marked by @Trimmed annotation")
+ void textServiceIcMarkedAsTrimmed() {
+ Annotation[] annotations = TextService.class.getAnnotations();
+
+ Optional anyTrimmedAnnotation = Arrays.stream(annotations)
+ .map(a -> a.annotationType().getSimpleName())
+ .filter(n -> n.equals(TRIMMED))
+ .findAny();
+
+ assertThat(anyTrimmedAnnotation).isPresent();
+ }
+
+ @Test
+ @Order(9)
+ @DisplayName("TrimmedAnnotationBeanPostProcessor class exists in util package")
+ void trimmedAnnotationBeanPostProcessorClassExists() throws ClassNotFoundException {
+ Class.forName(UTIL_PACKAGE + "." + TRIMMED_ANNOTATION_BEAN_POST_PROCESSOR);
+ }
+
+ @Test
+ @Order(10)
+ @DisplayName("TrimmedAnnotationBeanPostProcessor class implements BeanPostProcessor interface")
+ void trimmedAnnotationBeanPostProcessorClassImplementsBeanPostProcessor() {
+ Set> beanPostProcessorsClasses = configurationReflections
+ .getSubTypesOf(BeanPostProcessor.class);
+
+ Optional anyTrimmedBeanPostProcessor = beanPostProcessorsClasses.stream()
+ .map(Class::getSimpleName)
+ .filter(n -> n.equals(TRIMMED_ANNOTATION_BEAN_POST_PROCESSOR))
+ .findAny();
+
+ assertThat(anyTrimmedBeanPostProcessor).isPresent();
+ }
+
+ @Test
+ @Order(11)
+ @DisplayName("TrimmedAnnotationBeanPostProcessor class is not marked as Spring bean")
+ void trimmedAnnotationBeanPostProcessorClassIsNotMarkedAsBean() {
+ Set> beanPostProcessorsClasses = configurationReflections
+ .getSubTypesOf(BeanPostProcessor.class);
+
+ Optional> anyTrimmedBeanPostProcessor = beanPostProcessorsClasses.stream()
+ .filter(c -> c.getSimpleName().equals(TRIMMED_ANNOTATION_BEAN_POST_PROCESSOR))
+ .findAny();
+
+ boolean componentAnnotationIsPresent = anyTrimmedBeanPostProcessor
+ .orElseThrow().isAnnotationPresent(Component.class);
+ boolean serviceAnnotationIsPresent = anyTrimmedBeanPostProcessor
+ .orElseThrow().isAnnotationPresent(Service.class);
+
+ assertFalse(componentAnnotationIsPresent);
+ assertFalse(serviceAnnotationIsPresent);
+ }
+
+ @Test
+ @Order(12)
+ @DisplayName("StringTrimmedConfiguration class exists in \"util\" package")
+ void stringTrimmedConfigurationClassExists() throws ClassNotFoundException {
+ Class.forName(UTIL_PACKAGE + "." + STRING_TRIMMING_CONFIGURATION);
+ }
+
+ @Test
+ @Order(13)
+ @DisplayName("StringTrimmedConfiguration class is marked by @Configuration annotation")
+ void stringTrimmedConfigurationClassIsMarkedByConfiguration() {
+ Optional> anyMarkedClass = configurationReflections.getTypesAnnotatedWith(Configuration.class).stream()
+ .filter(c -> c.getSimpleName().equals(STRING_TRIMMING_CONFIGURATION))
+ .findAny();
+
+ assertThat(anyMarkedClass).isPresent();
+ }
+
+ @Test
+ @Order(14)
+ @DisplayName("StringTrimmedConfiguration class creates TrimmedAnnotationBeanPostProcessor bean using method")
+ void stringTrimmedConfigurationClassCreatesTrimmedBeanPostProcessor() throws ClassNotFoundException {
+ Class> configurationClass = Class.forName(UTIL_PACKAGE + "." + STRING_TRIMMING_CONFIGURATION);
+
+ Optional optionalMethod = Arrays
+ .stream(configurationClass.getDeclaredMethods())
+ .filter(method -> method.isAnnotationPresent(Bean.class))
+ .filter(method -> method.getReturnType().equals(BeanPostProcessor.class))
+ .findAny();
+
+ assertThat(optionalMethod).isPresent();
+ }
+
+ @Test
+ @Order(15)
+ @DisplayName("EnableStringAnnotation class is marked with @Configuration")
+ void enableStringTrimmingAnnotationClassIsMarkedAsConfiguration() {
+ Optional> anyAnnotationClass = annotationReflections
+ .getTypesAnnotatedWith(Configuration.class).stream()
+ .filter(c -> c.getSimpleName().equals(ENABLE_STRING_TRIMMING))
+ .findAny();
+
+ assertThat(anyAnnotationClass).isPresent();
+ }
+
+ @Test
+ @Order(16)
+ @DisplayName("EnableStringAnnotation class is marked with @Configuration")
+ void enableStringTrimmingAnnotationClassIsMarkedWithConfiguredImport() {
+ Optional> anyAnnotationClass = annotationReflections
+ .getTypesAnnotatedWith(Import.class).stream()
+ .filter(c -> c.getSimpleName().equals(ENABLE_STRING_TRIMMING))
+ .filter(c -> c.getAnnotation(Import.class).value()[0].getSimpleName().equals(STRING_TRIMMING_CONFIGURATION))
+ .findAny();
+
+ assertThat(anyAnnotationClass).isPresent();
+ }
+
+ @Test
+ @Order(17)
+ @DisplayName("TextService bean is CGLIB proxy")
+ void textServiceHasProxy() {
+ String className = textService.getClass().getSimpleName();
+
+ boolean isCGLibCreature = className.contains("CGLIB");
+
+ assertThat(className).isNotEqualTo("TextService");
+ assertTrue(isCGLibCreature);
+ }
+
+ @Test
+ @Order(18)
+ @DisplayName("Bean is not proxy when @Trimmed annotation is missed")
+ void beanIsNotProxyWhenTrimmedAnnotationIsNotUsed() {
+ String name = notTrimmedTextService.getClass().getSimpleName();
+
+ assertThat(name).isEqualTo("NotTrimmedTextService");
+ }
+
+ @Test
+ @Order(19)
+ @DisplayName("TextService receives trimmed String method arguments")
+ void textServiceReceivesTrimmedStringMethodArguments() {
+ String text = " Need more space ";
+ textService.saveText(text);
+
+ String result = textService.savedText;
+
+ assertThat(result).isEqualTo("Need more space");
+ }
+
+ @Test
+ @Order(20)
+ @DisplayName("TextService method returns trimmed String value")
+ void textServiceMethodReturnsTrimmedStringValue() {
+ String result = textService.getAvailableText();
+
+ assertThat(result).isEqualTo("Who cares about tabbing?");
+ }
+
+ private Optional> getAnnotationClassMarketByRetentionRuntimePolicy(String className) {
+ Set> annotations =
+ annotationReflections.getTypesAnnotatedWith(Retention.class);
+ return annotations.stream()
+ .filter(c -> c.getSimpleName().equals(className))
+ .filter(c -> c.getAnnotation(Retention.class).value().equals(RetentionPolicy.RUNTIME))
+ .findAny();
+ }
+}
diff --git a/3-0-spring-framework/bean-post-processor/src/test/java/com/bobocode/testService/NotTrimmedTextService.java b/3-0-spring-framework/bean-post-processor/src/test/java/com/bobocode/testService/NotTrimmedTextService.java
new file mode 100644
index 0000000..3bc848a
--- /dev/null
+++ b/3-0-spring-framework/bean-post-processor/src/test/java/com/bobocode/testService/NotTrimmedTextService.java
@@ -0,0 +1,7 @@
+package com.bobocode.testService;
+
+import org.springframework.stereotype.Service;
+
+@Service
+public class NotTrimmedTextService {
+}
diff --git a/3-0-spring-framework/pom.xml b/3-0-spring-framework/pom.xml
index 6beb492..3a73db4 100644
--- a/3-0-spring-framework/pom.xml
+++ b/3-0-spring-framework/pom.xml
@@ -18,6 +18,7 @@
3-1-1-dispatcher-servlet-initializer
3-0-2-view-resolver
3-2-1-account-rest-api
+ bean-post-processor
diff --git a/pom.xml b/pom.xml
index a006708..5884de2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -57,7 +57,7 @@
org.reflections
reflections
- 0.9.12
+ 0.10.2
org.hamcrest