diff --git a/3-0-spring-framework/3-0-0-hello-spring-framework/README.MD b/3-0-spring-framework/3-0-0-hello-spring-framework/README.MD
new file mode 100644
index 0000000..ff8f127
--- /dev/null
+++ b/3-0-spring-framework/3-0-0-hello-spring-framework/README.MD
@@ -0,0 +1,21 @@
+#
Hello ApplicationContext exercise :muscle:
+Improve your *Spring ApplicationContext* Java configuration skills
+### Task
+The task is to **configure `ApplicationContext`** that contains `AccountService` bean, `AccountDao` bean
+and `TestDataGenerator` bean. Your job is to follow the instructions in the *todo* section and **implement
+a proper configuration.**
+
+To verify your configuration, run `AppConfigTest.java`
+
+
+### Pre-conditions :heavy_exclamation_mark:
+You're supposed to be familiar with *Spring IoC* and *Dependency injection*
+
+### How to start :question:
+* 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-information_source)
+* Don't worry if you got stuck, checkout the **exercise/completed** branch and see the final implementation
+
+### Related materials :information_source:
+ * [Spring IoC basics tutorial](https://github.com/boy4uck/spring-framework-tutorial/tree/master/spring-framework-ioc-basics)
+
diff --git a/3-0-spring-framework/3-0-0-hello-spring-framework/pom.xml b/3-0-spring-framework/3-0-0-hello-spring-framework/pom.xml
new file mode 100644
index 0000000..0dd36ae
--- /dev/null
+++ b/3-0-spring-framework/3-0-0-hello-spring-framework/pom.xml
@@ -0,0 +1,49 @@
+
+
+
+ 3-0-spring-framework
+ com.bobocode
+ 1.0-SNAPSHOT
+
+ 4.0.0
+
+ 3-0-0-hello-spring-framework
+
+
+
+ org.springframework
+ spring-context
+ 5.2.12.RELEASE
+
+
+ org.springframework
+ spring-test
+ 5.2.12.RELEASE
+
+
+ com.bobocode
+ spring-framework-exercises-util
+ 1.0-SNAPSHOT
+
+
+ org.hamcrest
+ hamcrest-all
+ 1.3
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ 1.7.24
+
+
+ com.bobocode
+ spring-framework-exercises-model
+ 1.0-SNAPSHOT
+ compile
+
+
+
+
\ No newline at end of file
diff --git a/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/config/ApplicationConfig.java b/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/config/ApplicationConfig.java
new file mode 100644
index 0000000..91df5b3
--- /dev/null
+++ b/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/config/ApplicationConfig.java
@@ -0,0 +1,30 @@
+package com.bobocode.config;
+
+import com.bobocode.TestDataGenerator;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.ImportResource;
+import org.springframework.stereotype.Component;
+
+/**
+ * This class specify application context configuration. Basically, it's all about which instances of which classes
+ * should be created and registered in the context. An instance that is registered in the context is called 'bean'.
+ *
+ * To tell the container, which bean should be created, you could either specify packages to scan using @{@link ComponentScan},
+ * or declare your own beans using @{@link Bean}. When you use @{@link ComponentScan} the container will discover
+ * specified package and its sub-folders, to find all classes marked @{@link Component}.
+ *
+ * If you want to import other configs from Java class or XML file, you could use @{@link Import}
+ * and @{@link ImportResource} accordingly
+ *
+ * todo 1: make this class a Spring configuration class
+ * todo 2: enable component scanning for dao and service packages
+ * todo 3: provide explicit configuration for a bean of type {@link TestDataGenerator} with name "dataGenerator" in this class.
+ * hint: use method creation approach with @Bean annotation;
+ * todo 4: Don't specify bean name "dataGenerator" explicitly
+ */
+
+public class ApplicationConfig {
+
+}
diff --git a/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/dao/AccountDao.java b/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/dao/AccountDao.java
new file mode 100644
index 0000000..cbc888b
--- /dev/null
+++ b/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/dao/AccountDao.java
@@ -0,0 +1,12 @@
+package com.bobocode.dao;
+
+import com.bobocode.model.Account;
+
+import java.util.List;
+
+/**
+ * Defines an API for {@link Account} data access object (DAO).
+ */
+public interface AccountDao {
+ List findAll();
+}
diff --git a/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/dao/FakeAccountDao.java b/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/dao/FakeAccountDao.java
new file mode 100644
index 0000000..9543845
--- /dev/null
+++ b/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/dao/FakeAccountDao.java
@@ -0,0 +1,34 @@
+package com.bobocode.dao;
+
+import com.bobocode.TestDataGenerator;
+import com.bobocode.model.Account;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import static java.util.stream.Collectors.toList;
+
+/**
+ * This class should be marked with @{@link Component}, thus Spring container will create an instance
+ * of {@link FakeAccountDao} class, and will register it the context.
+ *
+ * todo: configure this class as Spring component with bean name "accountDao"
+ * todo: use explicit (with {@link Autowired} annotation) constructor-based dependency injection for specific bean
+ */
+
+public class FakeAccountDao implements AccountDao {
+ private List accounts;
+
+ public FakeAccountDao(TestDataGenerator testDataGenerator) {
+ this.accounts = Stream.generate(testDataGenerator::generateAccount)
+ .limit(20)
+ .collect(toList());
+ }
+
+ @Override
+ public List findAll() {
+ return accounts;
+ }
+}
diff --git a/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/service/AccountService.java b/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/service/AccountService.java
new file mode 100644
index 0000000..3bc0b66
--- /dev/null
+++ b/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/service/AccountService.java
@@ -0,0 +1,29 @@
+package com.bobocode.service;
+
+import com.bobocode.dao.AccountDao;
+import com.bobocode.model.Account;
+
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Provides service API for {@link Account}.
+ *
+ * todo: configure {@link AccountService} bean implicitly using special annotation for service classes
+ * todo: use implicit constructor-based dependency injection (don't use {@link org.springframework.beans.factory.annotation.Autowired})
+ */
+
+public class AccountService {
+ private final AccountDao accountDao;
+
+ public AccountService(AccountDao accountDao) {
+ this.accountDao = accountDao;
+ }
+
+ public Account findRichestAccount() {
+ List accounts = accountDao.findAll();
+ return accounts.stream()
+ .max(Comparator.comparing(Account::getBalance))
+ .get();
+ }
+}
diff --git a/3-0-spring-framework/3-0-0-hello-spring-framework/src/test/java/com/bobocode/ApplicationConfigTest.java b/3-0-spring-framework/3-0-0-hello-spring-framework/src/test/java/com/bobocode/ApplicationConfigTest.java
new file mode 100644
index 0000000..2c7a211
--- /dev/null
+++ b/3-0-spring-framework/3-0-0-hello-spring-framework/src/test/java/com/bobocode/ApplicationConfigTest.java
@@ -0,0 +1,120 @@
+package com.bobocode;
+
+import com.bobocode.config.ApplicationConfig;
+import com.bobocode.dao.AccountDao;
+import com.bobocode.dao.FakeAccountDao;
+import com.bobocode.service.AccountService;
+import org.junit.jupiter.api.*;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.stereotype.Component;
+import org.springframework.stereotype.Service;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class ApplicationConfigTest {
+
+ @Test
+ @Order(1)
+ @DisplayName("Config class is marked as @Configuration")
+ void configClassIsMarkedAsConfiguration() {
+ Configuration configuration = ApplicationConfig.class.getAnnotation(Configuration.class);
+
+ assertNotNull(configuration);
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("@ComponentScan is enabled")
+ void componentScanIsEnabled() {
+ ComponentScan componentScan = ApplicationConfig.class.getAnnotation(ComponentScan.class);
+
+ assertNotNull(componentScan);
+ }
+
+ @Test
+ @Order(3)
+ @DisplayName("@ComponentScan configured for \"dao\" and \"service\" packages")
+ void componentScanPackagesAreSpecified() {
+ ComponentScan componentScan = ApplicationConfig.class.getAnnotation(ComponentScan.class);
+ String[] packages = componentScan.basePackages();
+ if (packages.length == 0) {
+ packages = componentScan.value();
+ }
+ assertThat(packages).containsExactlyInAnyOrder("com.bobocode.dao", "com.bobocode.service");
+ }
+
+ @Test
+ @Order(4)
+ @DisplayName("DataGenerator bean is configured in method marked with @Bean")
+ void dataGeneratorBeanIsConfiguredExplicitly() {
+ Method[] methods = ApplicationConfig.class.getMethods();
+ Method testDataGeneratorBeanMethod = findTestDataGeneratorBeanMethod(methods);
+
+ assertNotNull(testDataGeneratorBeanMethod);
+ }
+
+ @Test
+ @Order(5)
+ @DisplayName("DataGenerator bean name is not specified explicitly")
+ void dataGeneratorBeanNameIsNotSpecifiedExplicitly() {
+ Method[] methods = ApplicationConfig.class.getMethods();
+ Method testDataGeneratorBeanMethod = findTestDataGeneratorBeanMethod(methods);
+ Bean bean = testDataGeneratorBeanMethod.getDeclaredAnnotation(Bean.class);
+
+ assertThat(bean.name().length).isEqualTo(0);
+ assertThat(bean.value().length).isEqualTo(0);
+ }
+
+ @Test
+ @Order(6)
+ @DisplayName("FakeAccountDao is configured as @Component")
+ void fakeAccountDaoIsConfiguredAsComponent() {
+ Component component = FakeAccountDao.class.getAnnotation(Component.class);
+
+ assertNotNull(component);
+ }
+
+ @Test
+ @Order(7)
+ @DisplayName("AccountService is configured as @Service")
+ void accountServiceIsConfiguredAsService() {
+ Service service = AccountService.class.getAnnotation(Service.class);
+
+ assertNotNull(service);
+ }
+
+ @Test
+ @Order(8)
+ @DisplayName("AccountService bean name is not specified explicitly")
+ void accountServiceBeanNameIsNotSpecifiedExplicitly() {
+ Service service = AccountService.class.getAnnotation(Service.class);
+
+ assertThat(service.value()).isEqualTo("");
+ }
+
+ @Test
+ @Order(9)
+ @DisplayName("AccountService doesn't use @Autowired")
+ void accountServiceDoesNotUseAutowired() throws NoSuchMethodException {
+ Annotation[] annotations = AccountService.class.getConstructor(AccountDao.class).getDeclaredAnnotations();
+
+ assertThat(annotations.length).isEqualTo(0);
+ }
+
+ private Method findTestDataGeneratorBeanMethod(Method[] methods) {
+ for (Method method : methods) {
+ if (method.getReturnType().equals(TestDataGenerator.class)
+ && method.getDeclaredAnnotation(Bean.class) != null) {
+ return method;
+ }
+ }
+ return null;
+ }
+}
diff --git a/3-0-spring-framework/3-0-0-hello-spring-framework/src/test/java/com/bobocode/ApplicationContextTest.java b/3-0-spring-framework/3-0-0-hello-spring-framework/src/test/java/com/bobocode/ApplicationContextTest.java
new file mode 100644
index 0000000..6955a14
--- /dev/null
+++ b/3-0-spring-framework/3-0-0-hello-spring-framework/src/test/java/com/bobocode/ApplicationContextTest.java
@@ -0,0 +1,97 @@
+package com.bobocode;
+
+import com.bobocode.dao.AccountDao;
+import com.bobocode.dao.FakeAccountDao;
+import com.bobocode.service.AccountService;
+import org.junit.jupiter.api.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@SpringJUnitConfig
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+class ApplicationContextTest {
+ @Configuration
+ @ComponentScan(basePackages = "com.bobocode")
+ static class TestConfig {
+ }
+
+ @Autowired
+ private ApplicationContext applicationContext;
+
+ @Autowired
+ private AccountService accountService;
+
+ @Autowired
+ private AccountDao accountDao;
+
+ @Test
+ @Order(1)
+ @DisplayName("DataGenerator has only one bean")
+ void dataGeneratorHasOnlyOneBean() {
+ Map testDataGeneratorMap = applicationContext.getBeansOfType(TestDataGenerator.class);
+
+ assertThat(testDataGeneratorMap.size()).isEqualTo(1);
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("DataGenerator bean has proper name")
+ void testDataGeneratorBeanName() {
+ Map dataGeneratorBeanMap = applicationContext.getBeansOfType(TestDataGenerator.class);
+
+ assertThat(dataGeneratorBeanMap.keySet()).contains("dataGenerator");
+ }
+
+ @Test
+ @Order(3)
+ @DisplayName("AccountDao has only one bean")
+ void accountDaoHasOnlyOneBean() {
+ Map accountDaoBeanMap = applicationContext.getBeansOfType(AccountDao.class);
+
+ assertThat(accountDaoBeanMap.size()).isEqualTo(1);
+ }
+
+ @Test
+ @Order(4)
+ @DisplayName("AccountDao bean has proper name")
+ void accountDaoBeanName() {
+ Map accountDaoBeanMap = applicationContext.getBeansOfType(AccountDao.class);
+
+ assertThat(accountDaoBeanMap.keySet()).contains("accountDao");
+ }
+
+ @Test
+ @Order(5)
+ @DisplayName("AccountDao constructor is marked with @Autowired")
+ void accountDaoConstructorIsMarkedWithAutowired() throws NoSuchMethodException {
+ Autowired autowired = FakeAccountDao.class.getConstructor(TestDataGenerator.class).getAnnotation(Autowired.class);
+
+ assertNotNull(autowired);
+ }
+
+ @Test
+ @Order(6)
+ @DisplayName("AccountService has only one bean")
+ void accountServiceHasOnlyOneBean() {
+ Map accountServiceMap = applicationContext.getBeansOfType(AccountService.class);
+
+ assertThat(accountServiceMap.size()).isEqualTo(1);
+ }
+
+ @Test
+ @Order(7)
+ @DisplayName("AccountService has proper name")
+ void accountServiceBeanName() {
+ Map accountServiceMap = applicationContext.getBeansOfType(AccountService.class);
+
+ assertThat(accountServiceMap.keySet()).contains("accountService");
+ }
+}
diff --git a/3-0-spring-framework/pom.xml b/3-0-spring-framework/pom.xml
index 209ddd8..f018246 100644
--- a/3-0-spring-framework/pom.xml
+++ b/3-0-spring-framework/pom.xml
@@ -13,6 +13,7 @@
pom
+ 3-0-0-hello-spring-framework
3-0-1-hello-spring-mvc
3-1-1-dispatcher-servlet-initializer
diff --git a/java-web-course-util/pom.xml b/java-web-course-util/pom.xml
index eb5e5a8..1990107 100644
--- a/java-web-course-util/pom.xml
+++ b/java-web-course-util/pom.xml
@@ -13,6 +13,8 @@
java-web-course-util
+ spring-framework-exercises-model
+ spring-framework-exercises-util
java-web-util
diff --git a/java-web-course-util/spring-framework-exercises-model/pom.xml b/java-web-course-util/spring-framework-exercises-model/pom.xml
new file mode 100644
index 0000000..9edb14f
--- /dev/null
+++ b/java-web-course-util/spring-framework-exercises-model/pom.xml
@@ -0,0 +1,14 @@
+
+
+
+ java-web-course-util
+ com.bobocode
+ 1.0-SNAPSHOT
+
+ 4.0.0
+
+ spring-framework-exercises-model
+
+
\ No newline at end of file
diff --git a/java-web-course-util/spring-framework-exercises-model/src/main/java/com/bobocode/model/Account.java b/java-web-course-util/spring-framework-exercises-model/src/main/java/com/bobocode/model/Account.java
new file mode 100644
index 0000000..1a0bb14
--- /dev/null
+++ b/java-web-course-util/spring-framework-exercises-model/src/main/java/com/bobocode/model/Account.java
@@ -0,0 +1,23 @@
+package com.bobocode.model;
+
+import lombok.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@NoArgsConstructor
+@Getter
+@Setter
+@ToString
+@EqualsAndHashCode(of = "email")
+public class Account {
+ private Long id;
+ private String firstName;
+ private String lastName;
+ private String email;
+ private LocalDate birthday;
+ private Gender gender;
+ private LocalDateTime creationTime;
+ private BigDecimal balance = BigDecimal.ZERO;
+}
diff --git a/java-web-course-util/spring-framework-exercises-model/src/main/java/com/bobocode/model/Gender.java b/java-web-course-util/spring-framework-exercises-model/src/main/java/com/bobocode/model/Gender.java
new file mode 100644
index 0000000..8a5b149
--- /dev/null
+++ b/java-web-course-util/spring-framework-exercises-model/src/main/java/com/bobocode/model/Gender.java
@@ -0,0 +1,6 @@
+package com.bobocode.model;
+
+public enum Gender {
+ MALE,
+ FEMALE
+}
diff --git a/java-web-course-util/spring-framework-exercises-util/pom.xml b/java-web-course-util/spring-framework-exercises-util/pom.xml
new file mode 100644
index 0000000..ee655e5
--- /dev/null
+++ b/java-web-course-util/spring-framework-exercises-util/pom.xml
@@ -0,0 +1,27 @@
+
+
+
+ java-web-course-util
+ com.bobocode
+ 1.0-SNAPSHOT
+
+ 4.0.0
+
+ spring-framework-exercises-util
+
+
+
+
+ com.bobocode
+ spring-framework-exercises-model
+ 1.0-SNAPSHOT
+
+
+ io.codearte.jfairy
+ jfairy
+ 0.5.7
+
+
+
\ No newline at end of file
diff --git a/java-web-course-util/spring-framework-exercises-util/src/main/java/com/bobocode/TestDataGenerator.java b/java-web-course-util/spring-framework-exercises-util/src/main/java/com/bobocode/TestDataGenerator.java
new file mode 100644
index 0000000..50cea04
--- /dev/null
+++ b/java-web-course-util/spring-framework-exercises-util/src/main/java/com/bobocode/TestDataGenerator.java
@@ -0,0 +1,33 @@
+package com.bobocode;
+
+import com.bobocode.model.Account;
+import com.bobocode.model.Gender;
+import io.codearte.jfairy.Fairy;
+import io.codearte.jfairy.producer.person.Person;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.Random;
+
+public class TestDataGenerator {
+ public Account generateAccount() {
+ Fairy fairy = Fairy.create();
+ Person person = fairy.person();
+ Random random = new Random();
+
+ Account fakeAccount = new Account();
+ fakeAccount.setFirstName(person.getFirstName());
+ fakeAccount.setLastName(person.getLastName());
+ fakeAccount.setEmail(person.getEmail());
+ fakeAccount.setBirthday(LocalDate.of(
+ person.getDateOfBirth().getYear(),
+ person.getDateOfBirth().getMonthOfYear(),
+ person.getDateOfBirth().getDayOfMonth()));
+ fakeAccount.setGender(Gender.valueOf(person.getSex().name()));
+ fakeAccount.setBalance(BigDecimal.valueOf(random.nextInt(200_000)));
+ fakeAccount.setCreationTime(LocalDateTime.now());
+
+ return fakeAccount;
+ }
+}