getAll() {
+ return super.getAll();
+ }
+
+ @Override
+ public User get(int id) {
+ return super.get(id);
+ }
+
+ @Override
+ public User create(User user) {
+ return super.create(user);
+ }
+
+ @Override
+ public void delete(int id) {
+ super.delete(id);
+ }
+
+ @Override
+ public void update(User user, int id) {
+ super.update(user, id);
+ }
+
+ @Override
+ public User getByMail(String email) {
+ return super.getByMail(email);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java
new file mode 100644
index 000000000..7d3702c31
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java
@@ -0,0 +1,22 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.springframework.stereotype.Controller;
+import ru.javawebinar.topjava.model.User;
+
+import static ru.javawebinar.topjava.web.SecurityUtil.authUserId;
+
+@Controller
+public class ProfileRestController extends AbstractUserController {
+
+ public User get() {
+ return super.get(authUserId());
+ }
+
+ public void delete() {
+ super.delete(authUserId());
+ }
+
+ public void update(User user) {
+ super.update(user, authUserId());
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/cache/ehcache.xml b/src/main/resources/cache/ehcache.xml
new file mode 100644
index 000000000..05589f71f
--- /dev/null
+++ b/src/main/resources/cache/ehcache.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+ 5
+
+ 5000
+
+
+
+
+
+
+ 1
+
+
+
+
diff --git a/src/main/resources/db/hsqldb.properties b/src/main/resources/db/hsqldb.properties
new file mode 100644
index 000000000..17c03ef4e
--- /dev/null
+++ b/src/main/resources/db/hsqldb.properties
@@ -0,0 +1,11 @@
+#database.url=jdbc:hsqldb:file:D:/temp/topjava
+
+database.url=jdbc:hsqldb:mem:topjava
+database.username=sa
+database.password=
+
+database.init=true
+jdbc.initLocation=classpath:db/initDB_hsql.sql
+jpa.showSql=true
+hibernate.format_sql=true
+hibernate.use_sql_comments=true
\ No newline at end of file
diff --git a/src/main/resources/db/initDB.sql b/src/main/resources/db/initDB.sql
new file mode 100644
index 000000000..4bf3d8446
--- /dev/null
+++ b/src/main/resources/db/initDB.sql
@@ -0,0 +1,37 @@
+DROP TABLE IF EXISTS user_role;
+DROP TABLE IF EXISTS meal;
+DROP TABLE IF EXISTS users;
+DROP SEQUENCE IF EXISTS global_seq;
+
+CREATE SEQUENCE global_seq START WITH 100000;
+
+CREATE TABLE users
+(
+ id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'),
+ name VARCHAR NOT NULL,
+ email VARCHAR NOT NULL,
+ password VARCHAR NOT NULL,
+ registered TIMESTAMP DEFAULT now() NOT NULL,
+ enabled BOOL DEFAULT TRUE NOT NULL,
+ calories_per_day INTEGER DEFAULT 2000 NOT NULL
+);
+CREATE UNIQUE INDEX users_unique_email_idx ON users (email);
+
+CREATE TABLE user_role
+(
+ user_id INTEGER NOT NULL,
+ role VARCHAR NOT NULL,
+ CONSTRAINT user_roles_idx UNIQUE (user_id, role),
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
+);
+
+CREATE TABLE meal
+(
+ id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'),
+ user_id INTEGER NOT NULL,
+ date_time TIMESTAMP NOT NULL,
+ description TEXT NOT NULL,
+ calories INT NOT NULL,
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
+);
+CREATE UNIQUE INDEX meal_unique_user_datetime_idx ON meal (user_id, date_time);
\ No newline at end of file
diff --git a/src/main/resources/db/initDB_hsql.sql b/src/main/resources/db/initDB_hsql.sql
new file mode 100644
index 000000000..9e0e195e6
--- /dev/null
+++ b/src/main/resources/db/initDB_hsql.sql
@@ -0,0 +1,39 @@
+DROP TABLE user_role IF EXISTS;
+DROP TABLE meal IF EXISTS;
+DROP TABLE users IF EXISTS;
+DROP SEQUENCE global_seq IF EXISTS;
+
+CREATE SEQUENCE GLOBAL_SEQ AS INTEGER START WITH 100000;
+
+CREATE TABLE users
+(
+ id INTEGER GENERATED BY DEFAULT AS SEQUENCE GLOBAL_SEQ PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ email VARCHAR(255) NOT NULL,
+ password VARCHAR(255) NOT NULL,
+ registered TIMESTAMP DEFAULT now() NOT NULL,
+ enabled BOOLEAN DEFAULT TRUE NOT NULL,
+ calories_per_day INTEGER DEFAULT 2000 NOT NULL
+);
+CREATE UNIQUE INDEX users_unique_email_idx
+ ON USERS (email);
+
+CREATE TABLE user_role
+(
+ user_id INTEGER NOT NULL,
+ role VARCHAR(255) NOT NULL,
+ CONSTRAINT user_roles_idx UNIQUE (user_id, role),
+ FOREIGN KEY (user_id) REFERENCES USERS (id) ON DELETE CASCADE
+);
+
+CREATE TABLE meal
+(
+ id INTEGER GENERATED BY DEFAULT AS SEQUENCE GLOBAL_SEQ PRIMARY KEY,
+ date_time TIMESTAMP NOT NULL,
+ description VARCHAR(255) NOT NULL,
+ calories INT NOT NULL,
+ user_id INTEGER NOT NULL,
+ FOREIGN KEY (user_id) REFERENCES USERS (id) ON DELETE CASCADE
+);
+CREATE UNIQUE INDEX meal_unique_user_datetime_idx
+ ON meal (user_id, date_time)
\ No newline at end of file
diff --git a/src/main/resources/db/populateDB.sql b/src/main/resources/db/populateDB.sql
new file mode 100644
index 000000000..5021dc6f1
--- /dev/null
+++ b/src/main/resources/db/populateDB.sql
@@ -0,0 +1,24 @@
+DELETE FROM user_role;
+DELETE FROM meal;
+DELETE FROM users;
+ALTER SEQUENCE global_seq RESTART WITH 100000;
+
+INSERT INTO users (name, email, password)
+VALUES ('User', 'user@yandex.ru', 'password'),
+ ('Admin', 'admin@gmail.com', 'admin'),
+ ('Guest', 'guest@gmail.com', 'guest');
+
+INSERT INTO user_role (role, user_id)
+VALUES ('USER', 100000),
+ ('ADMIN', 100001);
+
+INSERT INTO meal (date_time, description, calories, user_id)
+VALUES ('2020-01-30 10:00:00', 'Завтрак', 500, 100000),
+ ('2020-01-30 13:00:00', 'Обед', 1000, 100000),
+ ('2020-01-30 20:00:00', 'Ужин', 500, 100000),
+ ('2020-01-31 0:00:00', 'Еда на граничное значение', 100, 100000),
+ ('2020-01-31 10:00:00', 'Завтрак', 500, 100000),
+ ('2020-01-31 13:00:00', 'Обед', 1000, 100000),
+ ('2020-01-31 20:00:00', 'Ужин', 510, 100000),
+ ('2020-01-31 14:00:00', 'Админ ланч', 510, 100001),
+ ('2020-01-31 21:00:00', 'Админ ужин', 1500, 100001);
diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties
new file mode 100644
index 000000000..75d7d6873
--- /dev/null
+++ b/src/main/resources/db/postgres.properties
@@ -0,0 +1,9 @@
+database.url=jdbc:postgresql://localhost:5432/topjava
+database.username=user
+database.password=password
+
+database.init=true
+jdbc.initLocation=classpath:db/initDB.sql
+jpa.showSql=true
+hibernate.format_sql=true
+hibernate.use_sql_comments=true
\ No newline at end of file
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
new file mode 100644
index 000000000..d5fbdd95d
--- /dev/null
+++ b/src/main/resources/logback.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+ ${TOPJAVA_ROOT}/log/topjava.log
+
+
+ UTF-8
+ %date %-5level %logger{50}.%M:%L - %msg%n
+
+
+
+
+
+ UTF-8
+ %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%class{50}.%M:%L) - %msg%n
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml
new file mode 100644
index 000000000..d6c643e97
--- /dev/null
+++ b/src/main/resources/spring/spring-app.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/spring/spring-cache.xml b/src/main/resources/spring/spring-cache.xml
new file mode 100644
index 000000000..73325fee0
--- /dev/null
+++ b/src/main/resources/spring/spring-cache.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml
new file mode 100644
index 000000000..4106dbf1f
--- /dev/null
+++ b/src/main/resources/spring/spring-db.xml
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/tld/functions.tld b/src/main/webapp/WEB-INF/tld/functions.tld
new file mode 100644
index 000000000..d138fecdb
--- /dev/null
+++ b/src/main/webapp/WEB-INF/tld/functions.tld
@@ -0,0 +1,16 @@
+
+
+
+ 1.0
+ functions
+ http://topjava.javawebinar.ru/functions
+
+
+ formatDateTime
+ ru.javawebinar.topjava.util.DateTimeUtil
+ java.lang.String toString(java.time.LocalDateTime)
+
+
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..0ac5a7610
--- /dev/null
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,28 @@
+
+
+ TopJava
+
+
+ userServlet
+ ru.javawebinar.topjava.web.UserServlet
+ 0
+
+
+ userServlet
+ /users
+
+
+
+ mealServlet
+ ru.javawebinar.topjava.web.MealServlet
+ 0
+
+
+ mealServlet
+ /meals
+
+
diff --git a/src/main/webapp/css/style.css b/src/main/webapp/css/style.css
new file mode 100644
index 000000000..cfffdcb53
--- /dev/null
+++ b/src/main/webapp/css/style.css
@@ -0,0 +1,24 @@
+dl {
+ background: none repeat scroll 0 0 #FAFAFA;
+ margin: 8px 0;
+ padding: 0;
+}
+
+dt {
+ display: inline-block;
+ width: 170px;
+}
+
+dd {
+ display: inline-block;
+ margin-left: 8px;
+ vertical-align: top;
+}
+
+tr[data-meal-excess="false"] {
+ color: green;
+}
+
+tr[data-meal-excess="true"] {
+ color: red;
+}
diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html
new file mode 100644
index 000000000..57d710a81
--- /dev/null
+++ b/src/main/webapp/index.html
@@ -0,0 +1,18 @@
+
+
+
+ Java Enterprise (Topjava)
+
+
+
+
+
+
+
diff --git a/src/main/webapp/mealForm.jsp b/src/main/webapp/mealForm.jsp
new file mode 100644
index 000000000..98a6f4873
--- /dev/null
+++ b/src/main/webapp/mealForm.jsp
@@ -0,0 +1,34 @@
+<%@ page contentType="text/html;charset=UTF-8" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+
+
+
+ Meal
+
+
+
+
+
+
+ ${param.action == 'create' ? 'Create meal' : 'Edit meal'}
+
+
+
+
+
diff --git a/src/main/webapp/meals.jsp b/src/main/webapp/meals.jsp
new file mode 100644
index 000000000..7d9bf3e42
--- /dev/null
+++ b/src/main/webapp/meals.jsp
@@ -0,0 +1,66 @@
+<%@ page contentType="text/html;charset=UTF-8" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
+<%@ taglib prefix="fn" uri="http://topjava.javawebinar.ru/functions" %>
+
+
+ Meals
+
+
+
+
+
+
+ Meals
+
+
+ Add Meal
+
+
+
+
+ | Date |
+ Description |
+ Calories |
+ |
+ |
+
+
+
+
+
+ |
+ <%--${meal.dateTime.toLocalDate()} ${meal.dateTime.toLocalTime()}--%>
+ <%--<%=TimeUtil.toString(meal.getDateTime())%>--%>
+ <%--${fn:replace(meal.dateTime, 'T', ' ')}--%>
+ ${fn:formatDateTime(meal.dateTime)}
+ |
+ ${meal.description} |
+ ${meal.calories} |
+ Update |
+ Delete |
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/users.jsp b/src/main/webapp/users.jsp
new file mode 100644
index 000000000..650c8dda4
--- /dev/null
+++ b/src/main/webapp/users.jsp
@@ -0,0 +1,11 @@
+<%@ page contentType="text/html;charset=UTF-8" %>
+
+
+ Users
+
+
+
+
+Users
+
+
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java b/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java
new file mode 100644
index 000000000..43f143cc7
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java
@@ -0,0 +1,19 @@
+package ru.javawebinar.topjava;
+
+import org.springframework.lang.NonNull;
+import org.springframework.test.context.support.DefaultActiveProfilesResolver;
+
+import java.util.Arrays;
+
+//http://stackoverflow.com/questions/23871255/spring-profiles-simple-example-of-activeprofilesresolver
+public class ActiveDbProfileResolver extends DefaultActiveProfilesResolver {
+ @Override
+ public @NonNull
+ String[] resolve(@NonNull Class> aClass) {
+ // https://stackoverflow.com/a/52438829/548473
+ String[] activeProfiles = super.resolve(aClass);
+ String[] activeProfilesWithDb = Arrays.copyOf(activeProfiles, activeProfiles.length + 1);
+ activeProfilesWithDb[activeProfiles.length] = Profiles.getActiveDbProfile();
+ return activeProfilesWithDb;
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/MatcherFactory.java b/src/test/java/ru/javawebinar/topjava/MatcherFactory.java
new file mode 100644
index 000000000..c5f04086a
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/MatcherFactory.java
@@ -0,0 +1,37 @@
+package ru.javawebinar.topjava;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Factory for creating test matchers.
+ *
+ * Comparing actual and expected objects via AssertJ
+ */
+public class MatcherFactory {
+ public static Matcher usingIgnoringFieldsComparator(String... fieldsToIgnore) {
+ return new Matcher<>(fieldsToIgnore);
+ }
+
+ public static class Matcher {
+ private final String[] fieldsToIgnore;
+
+ private Matcher(String... fieldsToIgnore) {
+ this.fieldsToIgnore = fieldsToIgnore;
+ }
+
+ public void assertMatch(T actual, T expected) {
+ assertThat(actual).usingRecursiveComparison().ignoringFields(fieldsToIgnore).isEqualTo(expected);
+ }
+
+ @SafeVarargs
+ public final void assertMatch(Iterable actual, T... expected) {
+ assertMatch(actual, List.of(expected));
+ }
+
+ public void assertMatch(Iterable actual, Iterable expected) {
+ assertThat(actual).usingRecursiveFieldByFieldElementComparatorIgnoringFields(fieldsToIgnore).isEqualTo(expected);
+ }
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/MealTestData.java b/src/test/java/ru/javawebinar/topjava/MealTestData.java
new file mode 100644
index 000000000..d044e3f90
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java
@@ -0,0 +1,38 @@
+package ru.javawebinar.topjava;
+
+import ru.javawebinar.topjava.model.Meal;
+
+import java.time.Month;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+
+import static java.time.LocalDateTime.of;
+import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ;
+
+public class MealTestData {
+ public static final MatcherFactory.Matcher MEAL_MATCHER = MatcherFactory.usingIgnoringFieldsComparator("user");
+
+ public static final int NOT_FOUND = 10;
+ public static final int MEAL1_ID = START_SEQ + 3;
+ public static final int ADMIN_MEAL_ID = START_SEQ + 10;
+
+ public static final Meal meal1 = new Meal(MEAL1_ID, of(2020, Month.JANUARY, 30, 10, 0), "Завтрак", 500);
+ public static final Meal meal2 = new Meal(MEAL1_ID + 1, of(2020, Month.JANUARY, 30, 13, 0), "Обед", 1000);
+ public static final Meal meal3 = new Meal(MEAL1_ID + 2, of(2020, Month.JANUARY, 30, 20, 0), "Ужин", 500);
+ public static final Meal meal4 = new Meal(MEAL1_ID + 3, of(2020, Month.JANUARY, 31, 0, 0), "Еда на граничное значение", 100);
+ public static final Meal meal5 = new Meal(MEAL1_ID + 4, of(2020, Month.JANUARY, 31, 10, 0), "Завтрак", 500);
+ public static final Meal meal6 = new Meal(MEAL1_ID + 5, of(2020, Month.JANUARY, 31, 13, 0), "Обед", 1000);
+ public static final Meal meal7 = new Meal(MEAL1_ID + 6, of(2020, Month.JANUARY, 31, 20, 0), "Ужин", 510);
+ public static final Meal adminMeal1 = new Meal(ADMIN_MEAL_ID, of(2020, Month.JANUARY, 31, 14, 0), "Админ ланч", 510);
+ public static final Meal adminMeal2 = new Meal(ADMIN_MEAL_ID + 1, of(2020, Month.JANUARY, 31, 21, 0), "Админ ужин", 1500);
+
+ public static final List meals = List.of(meal7, meal6, meal5, meal4, meal3, meal2, meal1);
+
+ public static Meal getNew() {
+ return new Meal(null, of(2020, Month.FEBRUARY, 1, 18, 0), "Созданный ужин", 300);
+ }
+
+ public static Meal getUpdated() {
+ return new Meal(MEAL1_ID, meal1.getDateTime().plus(2, ChronoUnit.MINUTES), "Обновленный завтрак", 200);
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/SpringMain.java b/src/test/java/ru/javawebinar/topjava/SpringMain.java
new file mode 100644
index 000000000..e41f1ae11
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/SpringMain.java
@@ -0,0 +1,36 @@
+package ru.javawebinar.topjava;
+
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.to.MealTo;
+import ru.javawebinar.topjava.web.meal.MealRestController;
+import ru.javawebinar.topjava.web.user.AdminRestController;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.Month;
+import java.util.Arrays;
+import java.util.List;
+
+public class SpringMain {
+ public static void main(String[] args) {
+ // java 7 automatic resource management (ARM)
+ try (ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml", "spring/inmemory.xml")) {
+ System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames()));
+ AdminRestController adminUserController = appCtx.getBean(AdminRestController.class);
+ adminUserController.create(new User(null, "userName", "email@mail.ru", "password", Role.ADMIN));
+ System.out.println();
+
+ MealRestController mealController = appCtx.getBean(MealRestController.class);
+ List filteredMealsWithExcess =
+ mealController.getBetween(
+ LocalDate.of(2020, Month.JANUARY, 30), LocalTime.of(7, 0),
+ LocalDate.of(2020, Month.JANUARY, 31), LocalTime.of(11, 0));
+ filteredMealsWithExcess.forEach(System.out::println);
+ System.out.println();
+ System.out.println(mealController.getBetween(null, null, null, null));
+ }
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java
new file mode 100644
index 000000000..bfaba979d
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java
@@ -0,0 +1,37 @@
+package ru.javawebinar.topjava;
+
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.model.User;
+
+import java.util.Collections;
+import java.util.Date;
+
+import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ;
+
+public class UserTestData {
+ public static final MatcherFactory.Matcher USER_MATCHER = MatcherFactory.usingIgnoringFieldsComparator("registered", "roles");
+
+ public static final int USER_ID = START_SEQ;
+ public static final int ADMIN_ID = START_SEQ + 1;
+ public static final int GUEST_ID = START_SEQ + 2;
+ public static final int NOT_FOUND = 10;
+
+ public static final User user = new User(USER_ID, "User", "user@yandex.ru", "password", Role.USER);
+ public static final User admin = new User(ADMIN_ID, "Admin", "admin@gmail.com", "admin", Role.ADMIN);
+ public static final User guest = new User(GUEST_ID, "Guest", "guest@gmail.com", "guest");
+
+ public static User getNew() {
+ return new User(null, "New", "new@gmail.com", "newPass", 1555, false, new Date(), Collections.singleton(Role.USER));
+ }
+
+ public static User getUpdated() {
+ User updated = new User(user);
+ updated.setEmail("update@gmail.com");
+ updated.setName("UpdatedName");
+ updated.setCaloriesPerDay(330);
+ updated.setPassword("newPass");
+ updated.setEnabled(false);
+ updated.setRoles(Collections.singletonList(Role.ADMIN));
+ return updated;
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java
new file mode 100644
index 000000000..03770da21
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java
@@ -0,0 +1,45 @@
+package ru.javawebinar.topjava.repository.inmemory;
+
+import ru.javawebinar.topjava.model.AbstractBaseEntity;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ;
+
+public class InMemoryBaseRepository {
+
+ static final AtomicInteger counter = new AtomicInteger(START_SEQ);
+
+ final Map map = new ConcurrentHashMap<>();
+
+ public T save(T entity) {
+ Objects.requireNonNull(entity, "Entity must not be null");
+ if (entity.isNew()) {
+ entity.setId(counter.incrementAndGet());
+ map.put(entity.getId(), entity);
+ return entity;
+ }
+ return map.computeIfPresent(entity.getId(), (id, oldT) -> entity);
+ }
+
+ public boolean delete(int id) {
+ return map.remove(id) != null;
+ }
+
+ public T get(int id) {
+ return map.get(id);
+ }
+
+ Collection getCollection() {
+ return map.values();
+ }
+
+ void put(T entity) {
+ Objects.requireNonNull(entity, "Entity must not be null");
+ map.put(entity.id(), entity);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java
new file mode 100644
index 000000000..5c65ced86
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java
@@ -0,0 +1,80 @@
+package ru.javawebinar.topjava.repository.inmemory;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Repository;
+import ru.javawebinar.topjava.MealTestData;
+import ru.javawebinar.topjava.UserTestData;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.repository.MealRepository;
+import ru.javawebinar.topjava.util.Util;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Predicate;
+
+@Repository
+public class InMemoryMealRepository implements MealRepository {
+ private static final Logger log = LoggerFactory.getLogger(InMemoryMealRepository.class);
+
+ // Map userId -> mealRepository
+ private final Map> usersMealsMap = new ConcurrentHashMap<>();
+
+ {
+ var userMeals = new InMemoryBaseRepository();
+ MealTestData.meals.forEach(userMeals::put);
+ usersMealsMap.put(UserTestData.USER_ID, userMeals);
+ }
+
+
+ @Override
+ public Meal save(Meal meal, int userId) {
+ Objects.requireNonNull(meal, "meal must not be null");
+ var meals = usersMealsMap.computeIfAbsent(userId, uId -> new InMemoryBaseRepository<>());
+ return meals.save(meal);
+ }
+
+ @PostConstruct
+ public void postConstruct() {
+ log.info("+++ PostConstruct");
+ }
+
+ @PreDestroy
+ public void preDestroy() {
+ log.info("+++ PreDestroy");
+ }
+
+ @Override
+ public boolean delete(int id, int userId) {
+ var meals = usersMealsMap.get(userId);
+ return meals != null && meals.delete(id);
+ }
+
+ @Override
+ public Meal get(int id, int userId) {
+ var meals = usersMealsMap.get(userId);
+ return meals == null ? null : meals.get(id);
+ }
+
+ @Override
+ public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) {
+ return filterByPredicate(userId, meal -> Util.isBetweenHalfOpen(meal.getDateTime(), startDateTime, endDateTime));
+ }
+
+ @Override
+ public List getAll(int userId) {
+ return filterByPredicate(userId, meal -> true);
+ }
+
+ private List filterByPredicate(int userId, Predicate filter) {
+ var meals = usersMealsMap.get(userId);
+ return meals == null ? Collections.emptyList() :
+ meals.getCollection().stream()
+ .filter(filter)
+ .sorted(Comparator.comparing(Meal::getDateTime).reversed())
+ .toList();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java
new file mode 100644
index 000000000..f3585dfff
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java
@@ -0,0 +1,40 @@
+package ru.javawebinar.topjava.repository.inmemory;
+
+import org.springframework.stereotype.Repository;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.UserRepository;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+
+import static ru.javawebinar.topjava.UserTestData.*;
+
+
+@Repository
+public class InMemoryUserRepository extends InMemoryBaseRepository implements UserRepository {
+
+ public void init() {
+ map.clear();
+ put(user);
+ put(admin);
+ put(guest);
+ counter.getAndSet(GUEST_ID + 1);
+ }
+
+ @Override
+ public List getAll() {
+ return getCollection().stream()
+ .sorted(Comparator.comparing(User::getName).thenComparing(User::getEmail))
+ .toList();
+ }
+
+ @Override
+ public User getByEmail(String email) {
+ Objects.requireNonNull(email, "email must not be null");
+ return getCollection().stream()
+ .filter(u -> email.equals(u.getEmail()))
+ .findFirst()
+ .orElse(null);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java
new file mode 100644
index 000000000..4c78a2cb6
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java
@@ -0,0 +1,144 @@
+package ru.javawebinar.topjava.service;
+
+import org.junit.*;
+import org.junit.rules.Stopwatch;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DataAccessException;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.jdbc.SqlConfig;
+import org.springframework.test.context.junit4.SpringRunner;
+import ru.javawebinar.topjava.ActiveDbProfileResolver;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import java.time.LocalDate;
+import java.time.Month;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertThrows;
+import static org.slf4j.LoggerFactory.getLogger;
+import static ru.javawebinar.topjava.MealTestData.*;
+import static ru.javawebinar.topjava.UserTestData.ADMIN_ID;
+import static ru.javawebinar.topjava.UserTestData.USER_ID;
+
+@ContextConfiguration({
+ "classpath:spring/spring-app.xml",
+ "classpath:spring/spring-db.xml"
+})
+@RunWith(SpringRunner.class)
+@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8"))
+@ActiveProfiles(resolver = ActiveDbProfileResolver.class)
+@Ignore
+public class MealServiceTest {
+ private static final Logger log = getLogger("result");
+
+ private static final StringBuilder results = new StringBuilder();
+
+ @Rule
+ // http://stackoverflow.com/questions/14892125/what-is-the-best-practice-to-determine-the-execution-time-of-the-bussiness-relev
+ public final Stopwatch stopwatch = new Stopwatch() {
+ @Override
+ protected void finished(long nanos, Description description) {
+ String result = String.format("\n%-25s %7d", description.getMethodName(), TimeUnit.NANOSECONDS.toMillis(nanos));
+ results.append(result);
+ log.info(result + " ms\n");
+ }
+ };
+
+ @Autowired
+ private MealService service;
+
+ @AfterClass
+ public static void printResult() {
+ log.info("\n---------------------------------" +
+ "\nTest Duration, ms" +
+ "\n---------------------------------" +
+ results +
+ "\n---------------------------------");
+ }
+
+ @Test
+ public void delete() {
+ service.delete(MEAL1_ID, USER_ID);
+ assertThrows(NotFoundException.class, () -> service.get(MEAL1_ID, USER_ID));
+ }
+
+ @Test
+ public void deleteNotFound() {
+ assertThrows(NotFoundException.class, () -> service.delete(NOT_FOUND, USER_ID));
+ }
+
+ @Test
+ public void deleteNotOwn() {
+ assertThrows(NotFoundException.class, () -> service.delete(MEAL1_ID, ADMIN_ID));
+ }
+
+ @Test
+ public void create() {
+ Meal created = service.create(getNew(), USER_ID);
+ int newId = created.id();
+ Meal newMeal = getNew();
+ newMeal.setId(newId);
+ MEAL_MATCHER.assertMatch(created, newMeal);
+ MEAL_MATCHER.assertMatch(service.get(newId, USER_ID), newMeal);
+ }
+
+ @Test
+ public void duplicateDateTimeCreate() {
+ assertThrows(DataAccessException.class, () ->
+ service.create(new Meal(null, meal1.getDateTime(), "duplicate", 100), USER_ID));
+ }
+
+ @Test
+ public void get() {
+ Meal actual = service.get(ADMIN_MEAL_ID, ADMIN_ID);
+ MEAL_MATCHER.assertMatch(actual, adminMeal1);
+ }
+
+ @Test
+ public void getNotFound() {
+ assertThrows(NotFoundException.class, () -> service.get(NOT_FOUND, USER_ID));
+ }
+
+ @Test
+ public void getNotOwn() {
+ assertThrows(NotFoundException.class, () -> service.get(MEAL1_ID, ADMIN_ID));
+ }
+
+ @Test
+ public void update() {
+ Meal updated = getUpdated();
+ service.update(updated, USER_ID);
+ MEAL_MATCHER.assertMatch(service.get(MEAL1_ID, USER_ID), getUpdated());
+ }
+
+ @Test
+ public void updateNotOwn() {
+ NotFoundException exception = assertThrows(NotFoundException.class, () -> service.update(getUpdated(), ADMIN_ID));
+ Assert.assertEquals("Not found entity with id=" + MEAL1_ID, exception.getMessage());
+ MEAL_MATCHER.assertMatch(service.get(MEAL1_ID, USER_ID), meal1);
+ }
+
+ @Test
+ public void getAll() {
+ MEAL_MATCHER.assertMatch(service.getAll(USER_ID), meals);
+ }
+
+ @Test
+ public void getBetweenInclusive() {
+ MEAL_MATCHER.assertMatch(service.getBetweenInclusive(
+ LocalDate.of(2020, Month.JANUARY, 30),
+ LocalDate.of(2020, Month.JANUARY, 30), USER_ID),
+ meal3, meal2, meal1);
+ }
+
+ @Test
+ public void getBetweenWithNullDates() {
+ MEAL_MATCHER.assertMatch(service.getBetweenInclusive(null, null, USER_ID), meals);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java
new file mode 100644
index 000000000..edc3aed7b
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java
@@ -0,0 +1,101 @@
+package ru.javawebinar.topjava.service;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.CacheManager;
+import org.springframework.dao.DataAccessException;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.jdbc.SqlConfig;
+import org.springframework.test.context.junit4.SpringRunner;
+import ru.javawebinar.topjava.ActiveDbProfileResolver;
+import ru.javawebinar.topjava.UserTestData;
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import java.util.List;
+
+import static org.junit.Assert.assertThrows;
+import static ru.javawebinar.topjava.UserTestData.*;
+
+@ContextConfiguration({
+ "classpath:spring/spring-app.xml",
+ "classpath:spring/spring-db.xml"
+})
+@RunWith(SpringRunner.class)
+@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8"))
+@ActiveProfiles(resolver = ActiveDbProfileResolver.class)
+public class UserServiceTest {
+
+ @Autowired
+ private UserService service;
+
+ @Autowired
+ private CacheManager cacheManager;
+
+ @Before
+ public void setup() {
+ cacheManager.getCache("users").clear();
+ }
+
+ @Test
+ public void create() {
+ User created = service.create(getNew());
+ int newId = created.id();
+ User newUser = getNew();
+ newUser.setId(newId);
+ USER_MATCHER.assertMatch(created, newUser);
+ USER_MATCHER.assertMatch(service.get(newId), newUser);
+ }
+
+ @Test
+ public void duplicateMailCreate() {
+ assertThrows(DataAccessException.class, () ->
+ service.create(new User(null, "Duplicate", "user@yandex.ru", "newPass", Role.USER)));
+ }
+
+ @Test
+ public void delete() {
+ service.delete(USER_ID);
+ assertThrows(NotFoundException.class, () -> service.get(USER_ID));
+ }
+
+ @Test
+ public void deletedNotFound() {
+ assertThrows(NotFoundException.class, () -> service.delete(NOT_FOUND));
+ }
+
+ @Test
+ public void get() {
+ User user = service.get(USER_ID);
+ USER_MATCHER.assertMatch(user, UserTestData.user);
+ }
+
+ @Test
+ public void getNotFound() {
+ assertThrows(NotFoundException.class, () -> service.get(NOT_FOUND));
+ }
+
+ @Test
+ public void getByEmail() {
+ User user = service.getByEmail("admin@gmail.com");
+ USER_MATCHER.assertMatch(user, admin);
+ }
+
+ @Test
+ public void update() {
+ User updated = getUpdated();
+ service.update(updated);
+ USER_MATCHER.assertMatch(service.get(USER_ID), getUpdated());
+ }
+
+ @Test
+ public void getAll() {
+ List all = service.getAll();
+ USER_MATCHER.assertMatch(all, admin, guest, user);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java
new file mode 100644
index 000000000..3e26c2e61
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java
@@ -0,0 +1,42 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+import ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import static ru.javawebinar.topjava.UserTestData.NOT_FOUND;
+import static ru.javawebinar.topjava.UserTestData.USER_ID;
+
+@ContextConfiguration({"classpath:spring/spring-app.xml", "classpath:spring/inmemory.xml"})
+@RunWith(SpringRunner.class)
+public class InMemoryAdminRestControllerSpringTest {
+
+ @Autowired
+ private AdminRestController controller;
+
+ @Autowired
+ private InMemoryUserRepository repository;
+
+ @Before
+ public void setup() {
+ repository.init();
+ }
+
+ @Test
+ public void delete() {
+ controller.delete(USER_ID);
+ Assert.assertNull(repository.get(USER_ID));
+ }
+
+ @Test
+ public void deleteNotFound() {
+ Assert.assertThrows(NotFoundException.class, () -> controller.delete(NOT_FOUND));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java
new file mode 100644
index 000000000..14c60d960
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java
@@ -0,0 +1,52 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.junit.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+import ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import java.util.Arrays;
+
+import static ru.javawebinar.topjava.UserTestData.NOT_FOUND;
+import static ru.javawebinar.topjava.UserTestData.USER_ID;
+
+public class InMemoryAdminRestControllerTest {
+ private static final Logger log = LoggerFactory.getLogger(InMemoryAdminRestControllerTest.class);
+
+ private static ConfigurableApplicationContext appCtx;
+ private static AdminRestController controller;
+ private static InMemoryUserRepository repository;
+
+ @BeforeClass
+ public static void beforeClass() {
+ appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml", "spring/inmemory.xml");
+ log.info("\n{}\n", Arrays.toString(appCtx.getBeanDefinitionNames()));
+ controller = appCtx.getBean(AdminRestController.class);
+ repository = appCtx.getBean(InMemoryUserRepository.class);
+ }
+
+ @AfterClass
+ public static void afterClass() {
+ appCtx.close();
+ }
+
+ @Before
+ public void setup() {
+ // re-initialize
+ repository.init();
+ }
+
+ @Test
+ public void delete() {
+ controller.delete(USER_ID);
+ Assert.assertNull(repository.get(USER_ID));
+ }
+
+ @Test
+ public void deleteNotFound() {
+ Assert.assertThrows(NotFoundException.class, () -> controller.delete(NOT_FOUND));
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml
new file mode 100644
index 000000000..803655475
--- /dev/null
+++ b/src/test/resources/logback-test.xml
@@ -0,0 +1,32 @@
+
+
+
+ true
+
+
+
+
+ UTF-8
+ %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%class{50}.%M:%L) - %msg%n
+
+
+
+
+
+ UTF-8
+ %magenta(%msg%n)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/resources/spring/inmemory.xml b/src/test/resources/spring/inmemory.xml
new file mode 100644
index 000000000..c6a2710cb
--- /dev/null
+++ b/src/test/resources/spring/inmemory.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file