From cfbd3851dc9cac2009381cd68b142b9139483fac Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Wed, 18 Jan 2023 17:39:32 +0700 Subject: [PATCH 001/125] pathc applied --- .../javawebinar/topjava/model/UserMeal.java | 29 ++++++++++++++ .../topjava/model/UserMealWithExcess.java | 30 ++++++++++++++ .../ru/javawebinar/topjava/util/TimeUtil.java | 9 +++++ .../topjava/util/UserMealsUtil.java | 39 +++++++++++++++++++ 4 files changed, 107 insertions(+) create mode 100644 src/main/java/ru/javawebinar/topjava/model/UserMeal.java create mode 100644 src/main/java/ru/javawebinar/topjava/model/UserMealWithExcess.java create mode 100644 src/main/java/ru/javawebinar/topjava/util/TimeUtil.java create mode 100644 src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java diff --git a/src/main/java/ru/javawebinar/topjava/model/UserMeal.java b/src/main/java/ru/javawebinar/topjava/model/UserMeal.java new file mode 100644 index 000000000000..d8f91b127f6a --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/UserMeal.java @@ -0,0 +1,29 @@ +package ru.javawebinar.topjava.model; + +import java.time.LocalDateTime; + +public class UserMeal { + private final LocalDateTime dateTime; + + private final String description; + + private final int calories; + + public UserMeal(LocalDateTime dateTime, String description, int calories) { + this.dateTime = dateTime; + this.description = description; + this.calories = calories; + } + + public LocalDateTime getDateTime() { + return dateTime; + } + + public String getDescription() { + return description; + } + + public int getCalories() { + return calories; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/model/UserMealWithExcess.java b/src/main/java/ru/javawebinar/topjava/model/UserMealWithExcess.java new file mode 100644 index 000000000000..d0aa431a35d9 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/UserMealWithExcess.java @@ -0,0 +1,30 @@ +package ru.javawebinar.topjava.model; + +import java.time.LocalDateTime; + +public class UserMealWithExcess { + private final LocalDateTime dateTime; + + private final String description; + + private final int calories; + + private final boolean excess; + + public UserMealWithExcess(LocalDateTime dateTime, String description, int calories, boolean excess) { + this.dateTime = dateTime; + this.description = description; + this.calories = calories; + this.excess = excess; + } + + @Override + public String toString() { + return "UserMealWithExcess{" + + "dateTime=" + dateTime + + ", description='" + description + '\'' + + ", calories=" + calories + + ", excess=" + excess + + '}'; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java new file mode 100644 index 000000000000..0ebfdb5fcdcb --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java @@ -0,0 +1,9 @@ +package ru.javawebinar.topjava.util; + +import java.time.LocalTime; + +public class TimeUtil { + public static boolean isBetweenHalfOpen(LocalTime lt, LocalTime startTime, LocalTime endTime) { + return lt.compareTo(startTime) >= 0 && lt.compareTo(endTime) < 0; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java new file mode 100644 index 000000000000..3c171b4a5972 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java @@ -0,0 +1,39 @@ +package ru.javawebinar.topjava.util; + +import ru.javawebinar.topjava.model.UserMeal; +import ru.javawebinar.topjava.model.UserMealWithExcess; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.util.Arrays; +import java.util.List; + +public class UserMealsUtil { + public static void main(String[] args) { + List meals = Arrays.asList( + new UserMeal(LocalDateTime.of(2020, Month.JANUARY, 30, 10, 0), "Завтрак", 500), + new UserMeal(LocalDateTime.of(2020, Month.JANUARY, 30, 13, 0), "Обед", 1000), + new UserMeal(LocalDateTime.of(2020, Month.JANUARY, 30, 20, 0), "Ужин", 500), + new UserMeal(LocalDateTime.of(2020, Month.JANUARY, 31, 0, 0), "Еда на граничное значение", 100), + new UserMeal(LocalDateTime.of(2020, Month.JANUARY, 31, 10, 0), "Завтрак", 1000), + new UserMeal(LocalDateTime.of(2020, Month.JANUARY, 31, 13, 0), "Обед", 500), + new UserMeal(LocalDateTime.of(2020, Month.JANUARY, 31, 20, 0), "Ужин", 410) + ); + + List mealsTo = filteredByCycles(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000); + mealsTo.forEach(System.out::println); + +// System.out.println(filteredByStreams(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000)); + } + + public static List filteredByCycles(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + // TODO return filtered list with excess. Implement by cycles + return null; + } + + public static List filteredByStreams(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + // TODO Implement by streams + return null; + } +} From e2972e7a377fd283481f1e2a7bc6a4a14aa60e3f Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 4 Feb 2023 20:22:46 +0700 Subject: [PATCH 002/125] all patches for 1 home work --- .DS_Store | Bin 0 -> 6148 bytes pom.xml | 33 +++++++++++- .../model/{UserMeal.java => Meal.java} | 14 +++++- .../{UserMealWithExcess.java => MealTo.java} | 6 +-- .../javawebinar/topjava/util/MealsUtil.java | 47 ++++++++++++++++++ .../topjava/util/UserMealsUtil.java | 39 --------------- .../javawebinar/topjava/web/UserServlet.java | 23 +++++++++ src/main/resources/logback.xml | 29 +++++++++++ src/main/webapp/WEB-INF/web.xml | 18 +++++++ src/main/webapp/index.html | 13 +++++ src/main/webapp/users.jsp | 11 ++++ 11 files changed, 187 insertions(+), 46 deletions(-) create mode 100644 .DS_Store rename src/main/java/ru/javawebinar/topjava/model/{UserMeal.java => Meal.java} (62%) rename src/main/java/ru/javawebinar/topjava/model/{UserMealWithExcess.java => MealTo.java} (77%) create mode 100644 src/main/java/ru/javawebinar/topjava/util/MealsUtil.java delete mode 100644 src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/UserServlet.java create mode 100644 src/main/resources/logback.xml create mode 100644 src/main/webapp/WEB-INF/web.xml create mode 100644 src/main/webapp/index.html create mode 100644 src/main/webapp/users.jsp diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..733d6852e422ebb768d8877b36182c4ff3773288 GIT binary patch literal 6148 zcmeHKO>fgM7=GQ4HDwHX0BIMbNL;JZeP|QfrIhZ#0S+sI1E7*LQ5TWLRg zFF;8A1+M%O{tG8~UfYwhw4j|30>6^|IQIM4@#8mkOhlqP4R(kcMC2ebR&F3$W8BVZ z#Y(2;CQzt8f+w*Y`myv^d~JnQz$)Mh&V+tZa)e zkjDjOzQZHGzI8gGW2}0Qo&wiIM2Y*&Gj4+sQjFvwE<$>S(J~byYuGBDKIc}V49=t& z6U32|_u4>@9{fh=H!IA5<0tQnM{Czqz(ka@c};)oW)-js{67lt{@@}pb_^~xs;vW= zx&iru.javawebinar topjava - jar + war 1.0-SNAPSHOT @@ -15,12 +15,20 @@ 1.8 UTF-8 UTF-8 + + 1.2.11 + 1.7.36 topjava - install + package + + org.apache.maven.plugins + maven-war-plugin + 3.3.2 + org.apache.maven.plugins maven-compiler-plugin @@ -34,6 +42,27 @@ + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + ch.qos.logback + logback-classic + ${logback.version} + runtime + + + + + javax.servlet + javax.servlet-api + 4.0.1 + provided + diff --git a/src/main/java/ru/javawebinar/topjava/model/UserMeal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java similarity index 62% rename from src/main/java/ru/javawebinar/topjava/model/UserMeal.java rename to src/main/java/ru/javawebinar/topjava/model/Meal.java index d8f91b127f6a..943ff5cd59fa 100644 --- a/src/main/java/ru/javawebinar/topjava/model/UserMeal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -1,15 +1,17 @@ package ru.javawebinar.topjava.model; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; -public class UserMeal { +public class Meal { private final LocalDateTime dateTime; private final String description; private final int calories; - public UserMeal(LocalDateTime dateTime, String description, int calories) { + public Meal(LocalDateTime dateTime, String description, int calories) { this.dateTime = dateTime; this.description = description; this.calories = calories; @@ -26,4 +28,12 @@ public String getDescription() { public int getCalories() { return calories; } + + public LocalDate getDate() { + return dateTime.toLocalDate(); + } + + public LocalTime getTime() { + return dateTime.toLocalTime(); + } } diff --git a/src/main/java/ru/javawebinar/topjava/model/UserMealWithExcess.java b/src/main/java/ru/javawebinar/topjava/model/MealTo.java similarity index 77% rename from src/main/java/ru/javawebinar/topjava/model/UserMealWithExcess.java rename to src/main/java/ru/javawebinar/topjava/model/MealTo.java index d0aa431a35d9..07f04f8dbb9f 100644 --- a/src/main/java/ru/javawebinar/topjava/model/UserMealWithExcess.java +++ b/src/main/java/ru/javawebinar/topjava/model/MealTo.java @@ -2,7 +2,7 @@ import java.time.LocalDateTime; -public class UserMealWithExcess { +public class MealTo { private final LocalDateTime dateTime; private final String description; @@ -11,7 +11,7 @@ public class UserMealWithExcess { private final boolean excess; - public UserMealWithExcess(LocalDateTime dateTime, String description, int calories, boolean excess) { + public MealTo(LocalDateTime dateTime, String description, int calories, boolean excess) { this.dateTime = dateTime; this.description = description; this.calories = calories; @@ -20,7 +20,7 @@ public UserMealWithExcess(LocalDateTime dateTime, String description, int calori @Override public String toString() { - return "UserMealWithExcess{" + + return "MealTo{" + "dateTime=" + dateTime + ", description='" + description + '\'' + ", calories=" + calories + diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java new file mode 100644 index 000000000000..c29e1fbbb077 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -0,0 +1,47 @@ +package ru.javawebinar.topjava.util; + +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.model.MealTo; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class MealsUtil { + public static void main(String[] args) { + List meals = Arrays.asList( + new Meal(LocalDateTime.of(2020, Month.JANUARY, 30, 10, 0), "Завтрак", 500), + new Meal(LocalDateTime.of(2020, Month.JANUARY, 30, 13, 0), "Обед", 1000), + new Meal(LocalDateTime.of(2020, Month.JANUARY, 30, 20, 0), "Ужин", 500), + new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 0, 0), "Еда на граничное значение", 100), + new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 10, 0), "Завтрак", 1000), + new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 13, 0), "Обед", 500), + new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 20, 0), "Ужин", 410) + ); + + List mealsTo = filteredByStreams(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000); + mealsTo.forEach(System.out::println); + } + + public static List filteredByStreams(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + Map caloriesSumByDate = meals.stream() + .collect( + Collectors.groupingBy(Meal::getDate, Collectors.summingInt(Meal::getCalories)) +// Collectors.toMap(Meal::getDate, Meal::getCalories, Integer::sum) + ); + + return meals.stream() + .filter(meal -> TimeUtil.isBetweenHalfOpen(meal.getTime(), startTime, endTime)) + .map(meal -> createTo(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)) + .collect(Collectors.toList()); + } + + private static MealTo createTo(Meal meal, boolean excess) { + return new MealTo(meal.getDateTime(), meal.getDescription(), meal.getCalories(), excess); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java deleted file mode 100644 index 3c171b4a5972..000000000000 --- a/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java +++ /dev/null @@ -1,39 +0,0 @@ -package ru.javawebinar.topjava.util; - -import ru.javawebinar.topjava.model.UserMeal; -import ru.javawebinar.topjava.model.UserMealWithExcess; - -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.Month; -import java.util.Arrays; -import java.util.List; - -public class UserMealsUtil { - public static void main(String[] args) { - List meals = Arrays.asList( - new UserMeal(LocalDateTime.of(2020, Month.JANUARY, 30, 10, 0), "Завтрак", 500), - new UserMeal(LocalDateTime.of(2020, Month.JANUARY, 30, 13, 0), "Обед", 1000), - new UserMeal(LocalDateTime.of(2020, Month.JANUARY, 30, 20, 0), "Ужин", 500), - new UserMeal(LocalDateTime.of(2020, Month.JANUARY, 31, 0, 0), "Еда на граничное значение", 100), - new UserMeal(LocalDateTime.of(2020, Month.JANUARY, 31, 10, 0), "Завтрак", 1000), - new UserMeal(LocalDateTime.of(2020, Month.JANUARY, 31, 13, 0), "Обед", 500), - new UserMeal(LocalDateTime.of(2020, Month.JANUARY, 31, 20, 0), "Ужин", 410) - ); - - List mealsTo = filteredByCycles(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000); - mealsTo.forEach(System.out::println); - -// System.out.println(filteredByStreams(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000)); - } - - public static List filteredByCycles(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { - // TODO return filtered list with excess. Implement by cycles - return null; - } - - public static List filteredByStreams(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { - // TODO Implement by streams - return null; - } -} diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java new file mode 100644 index 000000000000..ef52d67576c0 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java @@ -0,0 +1,23 @@ +package ru.javawebinar.topjava.web; + +import org.slf4j.Logger; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.slf4j.LoggerFactory.getLogger; + +public class UserServlet extends HttpServlet { + private static final Logger log = getLogger(UserServlet.class); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + log.debug("redirect to users"); + +// request.getRequestDispatcher("/users.jsp").forward(request, response); + response.sendRedirect("users.jsp"); + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 000000000000..bdf004d037fb --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,29 @@ + + + + + + + + ${TOPJAVA_ROOT}/log/topjava.log + + + UTF-8 + %date %-5level %logger{0} [%file:%line] %msg%n + + + + + + UTF-8 + %-5level %logger{0} [%file:%line] %msg%n + + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000000..e576c30a3642 --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,18 @@ + + + TopJava + + + userServlet + ru.javawebinar.topjava.web.UserServlet + 0 + + + userServlet + /users + + diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html new file mode 100644 index 000000000000..921d3b47281d --- /dev/null +++ b/src/main/webapp/index.html @@ -0,0 +1,13 @@ + + + + Java Enterprise (Topjava) + + +

Проект Java Enterprise (Topjava)

+
+ + + diff --git a/src/main/webapp/users.jsp b/src/main/webapp/users.jsp new file mode 100644 index 000000000000..650c8dda479c --- /dev/null +++ b/src/main/webapp/users.jsp @@ -0,0 +1,11 @@ +<%@ page contentType="text/html;charset=UTF-8" %> + + + Users + + +

Home

+
+

Users

+ + \ No newline at end of file From 1cb55cfc9910c202eb2d15a1c8279e388b791e42 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 9 Feb 2023 22:58:49 +0700 Subject: [PATCH 003/125] apply 2_1-HW1.patch --- .DS_Store | Bin 6148 -> 8196 bytes pom.xml | 6 +++ .../java/ru/javawebinar/topjava/Main.java | 11 ---- .../ru/javawebinar/topjava/model/MealTo.java | 16 ++++++ .../topjava/util/DateTimeUtil.java | 18 +++++++ .../javawebinar/topjava/util/MealsUtil.java | 36 +++++++------ .../ru/javawebinar/topjava/util/TimeUtil.java | 9 ---- .../javawebinar/topjava/web/MealServlet.java | 22 ++++++++ .../javawebinar/topjava/web/UserServlet.java | 6 +-- src/main/webapp/WEB-INF/tld/functions.tld | 16 ++++++ src/main/webapp/WEB-INF/web.xml | 10 ++++ src/main/webapp/index.html | 1 + src/main/webapp/meals.jsp | 48 ++++++++++++++++++ 13 files changed, 160 insertions(+), 39 deletions(-) delete mode 100644 src/main/java/ru/javawebinar/topjava/Main.java create mode 100644 src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java delete mode 100644 src/main/java/ru/javawebinar/topjava/util/TimeUtil.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/MealServlet.java create mode 100644 src/main/webapp/WEB-INF/tld/functions.tld create mode 100644 src/main/webapp/meals.jsp diff --git a/.DS_Store b/.DS_Store index 733d6852e422ebb768d8877b36182c4ff3773288..88b81404868cd1caad2d082c03633080e453d480 100644 GIT binary patch delta 512 zcmZoMXmOBWU|?W$DortDU;r^WfEYvza8E20o2aMAD7Z0TH}hr%jz7$c**Q2SHn1=X zPUc}bDb37~!;sIA?wOOHoRpKF1k%F5z?2T8=S@DvqRRRj$XYo03(NOz~1v$PmPv#f#ob1oT P!NCZLVusD}Jad=f#Ac*s=|7cmkaOTSgg;un4N<|kQpco1OnVZ!WCr7#=`H+llf&lL1r>A PLCgnP$*?(|XAUy}!`vBD diff --git a/pom.xml b/pom.xml index 3898218dda19..7278f5ad42b8 100644 --- a/pom.xml +++ b/pom.xml @@ -63,6 +63,12 @@ 4.0.1 provided + + + javax.servlet + jstl + 1.2 + diff --git a/src/main/java/ru/javawebinar/topjava/Main.java b/src/main/java/ru/javawebinar/topjava/Main.java deleted file mode 100644 index c2f9cc618f7c..000000000000 --- a/src/main/java/ru/javawebinar/topjava/Main.java +++ /dev/null @@ -1,11 +0,0 @@ -package ru.javawebinar.topjava; - -/** - * @see Demo application - * @see Initial project - */ -public class Main { - public static void main(String[] args) { - System.out.format("Hello TopJava Enterprise!"); - } -} diff --git a/src/main/java/ru/javawebinar/topjava/model/MealTo.java b/src/main/java/ru/javawebinar/topjava/model/MealTo.java index 07f04f8dbb9f..800b1fcbb5c3 100644 --- a/src/main/java/ru/javawebinar/topjava/model/MealTo.java +++ b/src/main/java/ru/javawebinar/topjava/model/MealTo.java @@ -18,6 +18,22 @@ public MealTo(LocalDateTime dateTime, String description, int calories, boolean this.excess = excess; } + public LocalDateTime getDateTime() { + return dateTime; + } + + public String getDescription() { + return description; + } + + public int getCalories() { + return calories; + } + + public boolean isExcess() { + return excess; + } + @Override public String toString() { return "MealTo{" + diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java new file mode 100644 index 000000000000..3f23f83fde65 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -0,0 +1,18 @@ +package ru.javawebinar.topjava.util; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +public class DateTimeUtil { + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + public static boolean isBetweenHalfOpen(LocalTime lt, LocalTime startTime, LocalTime endTime) { + return lt.compareTo(startTime) >= 0 && lt.compareTo(endTime) < 0; + } + + public static String toString(LocalDateTime ldt) { + return ldt == null ? "" : ldt.format(DATE_TIME_FORMATTER); + } +} + diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index c29e1fbbb077..ac9c815959c5 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -10,25 +10,31 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.function.Predicate; import java.util.stream.Collectors; public class MealsUtil { - public static void main(String[] args) { - List meals = Arrays.asList( - new Meal(LocalDateTime.of(2020, Month.JANUARY, 30, 10, 0), "Завтрак", 500), - new Meal(LocalDateTime.of(2020, Month.JANUARY, 30, 13, 0), "Обед", 1000), - new Meal(LocalDateTime.of(2020, Month.JANUARY, 30, 20, 0), "Ужин", 500), - new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 0, 0), "Еда на граничное значение", 100), - new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 10, 0), "Завтрак", 1000), - new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 13, 0), "Обед", 500), - new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 20, 0), "Ужин", 410) - ); - - List mealsTo = filteredByStreams(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000); - mealsTo.forEach(System.out::println); + public static final int DEFAULT_CALORIES_PER_DAY = 2000; + + public static final List meals = Arrays.asList( + new Meal(LocalDateTime.of(2020, Month.JANUARY, 30, 10, 0), "Завтрак", 500), + new Meal(LocalDateTime.of(2020, Month.JANUARY, 30, 13, 0), "Обед", 1000), + new Meal(LocalDateTime.of(2020, Month.JANUARY, 30, 20, 0), "Ужин", 500), + new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 0, 0), "Еда на граничное значение", 100), + new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 10, 0), "Завтрак", 1000), + new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 13, 0), "Обед", 500), + new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 20, 0), "Ужин", 410) + ); + + public static List getTos(List meals, int caloriesPerDay) { + return filterByPredicate(meals, caloriesPerDay, meal -> true); + } + + public static List getFilteredTos(List meals, int caloriesPerDay, LocalTime startTime, LocalTime endTime) { + return filterByPredicate(meals, caloriesPerDay, meal -> DateTimeUtil.isBetweenHalfOpen(meal.getTime(), startTime, endTime)); } - public static List filteredByStreams(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + private static List filterByPredicate(List meals, int caloriesPerDay, Predicate filter) { Map caloriesSumByDate = meals.stream() .collect( Collectors.groupingBy(Meal::getDate, Collectors.summingInt(Meal::getCalories)) @@ -36,7 +42,7 @@ public static List filteredByStreams(List meals, LocalTime startTi ); return meals.stream() - .filter(meal -> TimeUtil.isBetweenHalfOpen(meal.getTime(), startTime, endTime)) + .filter(filter) .map(meal -> createTo(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)) .collect(Collectors.toList()); } diff --git a/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java deleted file mode 100644 index 0ebfdb5fcdcb..000000000000 --- a/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java +++ /dev/null @@ -1,9 +0,0 @@ -package ru.javawebinar.topjava.util; - -import java.time.LocalTime; - -public class TimeUtil { - public static boolean isBetweenHalfOpen(LocalTime lt, LocalTime startTime, LocalTime endTime) { - return lt.compareTo(startTime) >= 0 && lt.compareTo(endTime) < 0; - } -} diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java new file mode 100644 index 000000000000..8b47dd35450e --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -0,0 +1,22 @@ +package ru.javawebinar.topjava.web; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.javawebinar.topjava.util.MealsUtil; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class MealServlet extends HttpServlet { + private static final Logger log = LoggerFactory.getLogger(MealServlet.class); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + log.info("forward to meals"); + request.setAttribute("meals", MealsUtil.getTos(MealsUtil.meals, MealsUtil.DEFAULT_CALORIES_PER_DAY)); + request.getRequestDispatcher("/meals.jsp").forward(request, response); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java index ef52d67576c0..f6cf12e69976 100644 --- a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java @@ -15,9 +15,7 @@ public class UserServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - log.debug("redirect to users"); - -// request.getRequestDispatcher("/users.jsp").forward(request, response); - response.sendRedirect("users.jsp"); + log.debug("forward to users"); + request.getRequestDispatcher("/users.jsp").forward(request, response); } } 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 000000000000..d138fecdbfb5 --- /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 index e576c30a3642..0ac5a7610ed4 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -15,4 +15,14 @@ userServlet /users + + + mealServlet + ru.javawebinar.topjava.web.MealServlet + 0 + + + mealServlet + /meals + diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index 921d3b47281d..714683bd3856 100644 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -8,6 +8,7 @@

Проект
  • Users
  • +
  • Meals
  • diff --git a/src/main/webapp/meals.jsp b/src/main/webapp/meals.jsp new file mode 100644 index 000000000000..adc5a2c94ce4 --- /dev/null +++ b/src/main/webapp/meals.jsp @@ -0,0 +1,48 @@ +<%@ 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" %> +<%--<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>--%> + + + Meal list + + + +
    +

    Home

    +
    +

    Meals

    + + + + + + + + + + + + + + + + +
    DateDescriptionCalories
    + <%--${meal.dateTime.toLocalDate()} ${meal.dateTime.toLocalTime()}--%> + <%--<%=TimeUtil.toString(meal.getDateTime())%>--%> + <%--${fn:replace(meal.dateTime, 'T', ' ')}--%> + ${fn:formatDateTime(meal.dateTime)} + ${meal.description}${meal.calories}
    +
    + + \ No newline at end of file From e3735f2aeb05784b453b35f05902471b811e1f1c Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 9 Feb 2023 22:59:39 +0700 Subject: [PATCH 004/125] apply 2_1-HW1_optional.patch --- .../ru/javawebinar/topjava/model/Meal.java | 29 +++++++++ .../ru/javawebinar/topjava/model/MealTo.java | 12 +++- .../repository/InMemoryMealRepository.java | 45 ++++++++++++++ .../topjava/repository/MealRepository.java | 18 ++++++ .../javawebinar/topjava/util/MealsUtil.java | 9 +-- .../javawebinar/topjava/web/MealServlet.java | 61 ++++++++++++++++++- src/main/webapp/mealForm.jsp | 51 ++++++++++++++++ src/main/webapp/meals.jsp | 6 ++ 8 files changed, 222 insertions(+), 9 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepository.java create mode 100644 src/main/java/ru/javawebinar/topjava/repository/MealRepository.java create mode 100644 src/main/webapp/mealForm.jsp diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index 943ff5cd59fa..3abbee42511e 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -5,6 +5,8 @@ import java.time.LocalTime; public class Meal { + private Integer id; + private final LocalDateTime dateTime; private final String description; @@ -12,11 +14,24 @@ public class Meal { private final int calories; public Meal(LocalDateTime dateTime, String description, int calories) { + this(null, dateTime, description, calories); + } + + public Meal(Integer id, LocalDateTime dateTime, String description, int calories) { + this.id = id; this.dateTime = dateTime; this.description = description; this.calories = calories; } + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + public LocalDateTime getDateTime() { return dateTime; } @@ -36,4 +51,18 @@ public LocalDate getDate() { public LocalTime getTime() { return dateTime.toLocalTime(); } + + public boolean isNew() { + return id == null; + } + + @Override + public String toString() { + return "Meal{" + + "id=" + id + + ", dateTime=" + dateTime + + ", description='" + description + '\'' + + ", calories=" + calories + + '}'; + } } diff --git a/src/main/java/ru/javawebinar/topjava/model/MealTo.java b/src/main/java/ru/javawebinar/topjava/model/MealTo.java index 800b1fcbb5c3..01b3a5fdaad1 100644 --- a/src/main/java/ru/javawebinar/topjava/model/MealTo.java +++ b/src/main/java/ru/javawebinar/topjava/model/MealTo.java @@ -3,6 +3,8 @@ import java.time.LocalDateTime; public class MealTo { + private final Integer id; + private final LocalDateTime dateTime; private final String description; @@ -11,13 +13,18 @@ public class MealTo { private final boolean excess; - public MealTo(LocalDateTime dateTime, String description, int calories, boolean excess) { + public MealTo(Integer id, LocalDateTime dateTime, String description, int calories, boolean excess) { + this.id = id; this.dateTime = dateTime; this.description = description; this.calories = calories; this.excess = excess; } + public Integer getId() { + return id; + } + public LocalDateTime getDateTime() { return dateTime; } @@ -37,7 +44,8 @@ public boolean isExcess() { @Override public String toString() { return "MealTo{" + - "dateTime=" + dateTime + + "id=" + id + + ", dateTime=" + dateTime + ", description='" + description + '\'' + ", calories=" + calories + ", excess=" + excess + diff --git a/src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepository.java new file mode 100644 index 000000000000..ad6669056a4b --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepository.java @@ -0,0 +1,45 @@ +package ru.javawebinar.topjava.repository; + +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.util.MealsUtil; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +public class InMemoryMealRepository implements MealRepository { + private final Map repository = new ConcurrentHashMap<>(); + private final AtomicInteger counter = new AtomicInteger(0); + + { + MealsUtil.meals.forEach(this::save); + } + + @Override + public Meal save(Meal meal) { + if (meal.isNew()) { + meal.setId(counter.incrementAndGet()); + repository.put(meal.getId(), meal); + return meal; + } + // handle case: update, but not present in storage + return repository.computeIfPresent(meal.getId(), (id, oldMeal) -> meal); + } + + @Override + public boolean delete(int id) { + return repository.remove(id) != null; + } + + @Override + public Meal get(int id) { + return repository.get(id); + } + + @Override + public Collection getAll() { + return repository.values(); + } +} + diff --git a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java new file mode 100644 index 000000000000..2cb2aef8ba8d --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java @@ -0,0 +1,18 @@ +package ru.javawebinar.topjava.repository; + +import ru.javawebinar.topjava.model.Meal; + +import java.util.Collection; + +public interface MealRepository { + // null if not found, when updated + Meal save(Meal meal); + + // false if not found + boolean delete(int id); + + // null if not found + Meal get(int id); + + Collection getAll(); +} diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index ac9c815959c5..8d940a63efa1 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -8,6 +8,7 @@ import java.time.LocalTime; import java.time.Month; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.function.Predicate; @@ -26,15 +27,15 @@ public class MealsUtil { new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 20, 0), "Ужин", 410) ); - public static List getTos(List meals, int caloriesPerDay) { + public static List getTos(Collection meals, int caloriesPerDay) { return filterByPredicate(meals, caloriesPerDay, meal -> true); } - public static List getFilteredTos(List meals, int caloriesPerDay, LocalTime startTime, LocalTime endTime) { + public static List getFilteredTos(Collection meals, int caloriesPerDay, LocalTime startTime, LocalTime endTime) { return filterByPredicate(meals, caloriesPerDay, meal -> DateTimeUtil.isBetweenHalfOpen(meal.getTime(), startTime, endTime)); } - private static List filterByPredicate(List meals, int caloriesPerDay, Predicate filter) { + private static List filterByPredicate(Collection meals, int caloriesPerDay, Predicate filter) { Map caloriesSumByDate = meals.stream() .collect( Collectors.groupingBy(Meal::getDate, Collectors.summingInt(Meal::getCalories)) @@ -48,6 +49,6 @@ private static List filterByPredicate(List meals, int caloriesPerD } private static MealTo createTo(Meal meal, boolean excess) { - return new MealTo(meal.getDateTime(), meal.getDescription(), meal.getCalories(), excess); + return new MealTo(meal.getId(), meal.getDateTime(), meal.getDescription(), meal.getCalories(), excess); } } diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java index 8b47dd35450e..78d73c710e57 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -2,6 +2,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.InMemoryMealRepository; +import ru.javawebinar.topjava.repository.MealRepository; import ru.javawebinar.topjava.util.MealsUtil; import javax.servlet.ServletException; @@ -9,14 +12,66 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Objects; public class MealServlet extends HttpServlet { private static final Logger log = LoggerFactory.getLogger(MealServlet.class); + private MealRepository repository; + + @Override + public void init() { + repository = new InMemoryMealRepository(); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { + request.setCharacterEncoding("UTF-8"); + String id = request.getParameter("id"); + + Meal meal = new Meal(id.isEmpty() ? null : Integer.valueOf(id), + LocalDateTime.parse(request.getParameter("dateTime")), + request.getParameter("description"), + Integer.parseInt(request.getParameter("calories"))); + + log.info(meal.isNew() ? "Create {}" : "Update {}", meal); + repository.save(meal); + response.sendRedirect("meals"); + } + @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - log.info("forward to meals"); - request.setAttribute("meals", MealsUtil.getTos(MealsUtil.meals, MealsUtil.DEFAULT_CALORIES_PER_DAY)); - request.getRequestDispatcher("/meals.jsp").forward(request, response); + String action = request.getParameter("action"); + + switch (action == null ? "all" : action) { + case "delete": + int id = getId(request); + log.info("Delete id={}", id); + repository.delete(id); + response.sendRedirect("meals"); + break; + case "create": + case "update": + final Meal meal = "create".equals(action) ? + new Meal(LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), "", 1000) : + repository.get(getId(request)); + request.setAttribute("meal", meal); + request.getRequestDispatcher("/mealForm.jsp").forward(request, response); + break; + case "all": + default: + log.info("getAll"); + request.setAttribute("meals", + MealsUtil.getTos(repository.getAll(), MealsUtil.DEFAULT_CALORIES_PER_DAY)); + request.getRequestDispatcher("/meals.jsp").forward(request, response); + break; + } + } + + private int getId(HttpServletRequest request) { + String paramId = Objects.requireNonNull(request.getParameter("id")); + return Integer.parseInt(paramId); } } diff --git a/src/main/webapp/mealForm.jsp b/src/main/webapp/mealForm.jsp new file mode 100644 index 000000000000..28f140b65fce --- /dev/null +++ b/src/main/webapp/mealForm.jsp @@ -0,0 +1,51 @@ +<%@ page contentType="text/html;charset=UTF-8" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + Meal + + + +
    +

    Home

    +
    +

    ${param.action == 'create' ? 'Create meal' : 'Edit meal'}

    + +
    + +
    +
    DateTime:
    +
    +
    +
    +
    Description:
    +
    +
    +
    +
    Calories:
    +
    +
    + + +
    +
    + + diff --git a/src/main/webapp/meals.jsp b/src/main/webapp/meals.jsp index adc5a2c94ce4..224d9876135b 100644 --- a/src/main/webapp/meals.jsp +++ b/src/main/webapp/meals.jsp @@ -21,12 +21,16 @@

    Home


    Meals

    + Add Meal +

    + + @@ -40,6 +44,8 @@ + +
    Date Description Calories
    ${meal.description} ${meal.calories}UpdateDelete
    From 5836a80fbf21199521a4341fa01f389ef613d816 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 9 Feb 2023 23:00:38 +0700 Subject: [PATCH 005/125] apply 2_3_app_layers.patch --- .../topjava/model/AbstractBaseEntity.java | 26 ++++++ .../topjava/model/AbstractNamedEntity.java | 24 +++++ .../ru/javawebinar/topjava/model/Role.java | 6 ++ .../ru/javawebinar/topjava/model/User.java | 91 +++++++++++++++++++ .../topjava/repository/MealRepository.java | 8 +- .../topjava/repository/UserRepository.java | 21 +++++ .../topjava/service/MealService.java | 9 ++ .../topjava/service/UserService.java | 38 ++++++++ .../topjava/util/ValidationUtil.java | 43 +++++++++ .../util/exception/NotFoundException.java | 7 ++ .../javawebinar/topjava/web/SecurityUtil.java | 14 +++ .../topjava/web/meal/MealRestController.java | 8 ++ .../web/user/AbstractUserController.java | 49 ++++++++++ .../topjava/web/user/AdminRestController.java | 38 ++++++++ .../web/user/ProfileRestController.java | 20 ++++ 15 files changed, 399 insertions(+), 3 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java create mode 100644 src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java create mode 100644 src/main/java/ru/javawebinar/topjava/model/Role.java create mode 100644 src/main/java/ru/javawebinar/topjava/model/User.java create mode 100644 src/main/java/ru/javawebinar/topjava/repository/UserRepository.java create mode 100644 src/main/java/ru/javawebinar/topjava/service/MealService.java create mode 100644 src/main/java/ru/javawebinar/topjava/service/UserService.java create mode 100644 src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java create mode 100644 src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java new file mode 100644 index 000000000000..8f27c902e047 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -0,0 +1,26 @@ +package ru.javawebinar.topjava.model; + +public abstract class AbstractBaseEntity { + protected Integer id; + + protected AbstractBaseEntity(Integer id) { + this.id = id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public boolean isNew() { + return this.id == null; + } + + @Override + public String toString() { + return getClass().getSimpleName() + ":" + id; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java new file mode 100644 index 000000000000..2054a3d3cf9b --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java @@ -0,0 +1,24 @@ +package ru.javawebinar.topjava.model; + +public abstract class AbstractNamedEntity extends AbstractBaseEntity { + + protected String name; + + protected AbstractNamedEntity(Integer id, String name) { + super(id); + this.name = name; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + @Override + public String toString() { + return super.toString() + '(' + name + ')'; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/Role.java b/src/main/java/ru/javawebinar/topjava/model/Role.java new file mode 100644 index 000000000000..acb7a276f6dc --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/Role.java @@ -0,0 +1,6 @@ +package ru.javawebinar.topjava.model; + +public enum Role { + USER, + ADMIN +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java new file mode 100644 index 000000000000..d88e381945d8 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -0,0 +1,91 @@ +package ru.javawebinar.topjava.model; + +import java.util.Date; +import java.util.EnumSet; +import java.util.Set; + +import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY; + +public class User extends AbstractNamedEntity { + + private String email; + + private String password; + + private boolean enabled = true; + + private Date registered = new Date(); + + private Set roles; + + private int caloriesPerDay = DEFAULT_CALORIES_PER_DAY; + + public User(Integer id, String name, String email, String password, Role role, Role... roles) { + this(id, name, email, password, DEFAULT_CALORIES_PER_DAY, true, EnumSet.of(role, roles)); + } + + public User(Integer id, String name, String email, String password, int caloriesPerDay, boolean enabled, Set roles) { + super(id, name); + this.email = email; + this.password = password; + this.caloriesPerDay = caloriesPerDay; + this.enabled = enabled; + this.roles = roles; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setPassword(String password) { + this.password = password; + } + + public Date getRegistered() { + return registered; + } + + public void setRegistered(Date registered) { + this.registered = registered; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public int getCaloriesPerDay() { + return caloriesPerDay; + } + + public void setCaloriesPerDay(int caloriesPerDay) { + this.caloriesPerDay = caloriesPerDay; + } + + public boolean isEnabled() { + return enabled; + } + + public Set getRoles() { + return roles; + } + + public String getPassword() { + return password; + } + + @Override + public String toString() { + return "User (" + + "id=" + id + + ", email=" + email + + ", name=" + name + + ", enabled=" + enabled + + ", roles=" + roles + + ", caloriesPerDay=" + caloriesPerDay + + ')'; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java index 2cb2aef8ba8d..675cdbb1bd38 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java @@ -4,15 +4,17 @@ import java.util.Collection; +// TODO add userId public interface MealRepository { - // null if not found, when updated + // null if updated meal does not belong to userId Meal save(Meal meal); - // false if not found + // false if meal does not belong to userId boolean delete(int id); - // null if not found + // null if meal does not belong to userId Meal get(int id); + // ORDERED dateTime desc Collection getAll(); } diff --git a/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java new file mode 100644 index 000000000000..138369789175 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java @@ -0,0 +1,21 @@ +package ru.javawebinar.topjava.repository; + +import ru.javawebinar.topjava.model.User; + +import java.util.List; + +public interface UserRepository { + // null if not found, when updated + User save(User user); + + // false if not found + boolean delete(int id); + + // null if not found + User get(int id); + + // null if not found + User getByEmail(String email); + + List getAll(); +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/service/MealService.java b/src/main/java/ru/javawebinar/topjava/service/MealService.java new file mode 100644 index 000000000000..0dc4a43c394f --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/MealService.java @@ -0,0 +1,9 @@ +package ru.javawebinar.topjava.service; + +import ru.javawebinar.topjava.repository.MealRepository; + +public class MealService { + + private MealRepository repository; + +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/service/UserService.java b/src/main/java/ru/javawebinar/topjava/service/UserService.java new file mode 100644 index 000000000000..b02082e352c4 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -0,0 +1,38 @@ +package ru.javawebinar.topjava.service; + +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.List; + +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFound; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; + +public class UserService { + + private UserRepository repository; + + public User create(User user) { + return repository.save(user); + } + + public void delete(int id) { + checkNotFoundWithId(repository.delete(id), id); + } + + public User get(int id) { + return checkNotFoundWithId(repository.get(id), id); + } + + public User getByEmail(String email) { + return checkNotFound(repository.getByEmail(email), "email=" + email); + } + + public List getAll() { + return repository.getAll(); + } + + public void update(User user) { + checkNotFoundWithId(repository.save(user), user.getId()); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java new file mode 100644 index 000000000000..971eb9c0c72b --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java @@ -0,0 +1,43 @@ +package ru.javawebinar.topjava.util; + + +import ru.javawebinar.topjava.model.AbstractBaseEntity; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +public class ValidationUtil { + + public static T checkNotFoundWithId(T object, int id) { + checkNotFoundWithId(object != null, id); + return object; + } + + public static void checkNotFoundWithId(boolean found, int id) { + checkNotFound(found, "id=" + id); + } + + public static T checkNotFound(T object, String msg) { + checkNotFound(object != null, msg); + return object; + } + + public static void checkNotFound(boolean found, String msg) { + if (!found) { + throw new NotFoundException("Not found entity with " + msg); + } + } + + public static void checkNew(AbstractBaseEntity entity) { + if (!entity.isNew()) { + throw new IllegalArgumentException(entity + " must be new (id=null)"); + } + } + + public static void assureIdConsistent(AbstractBaseEntity entity, int id) { +// conservative when you reply, but accept liberally (http://stackoverflow.com/a/32728226/548473) + if (entity.isNew()) { + entity.setId(id); + } else if (entity.getId() != id) { + throw new IllegalArgumentException(entity + " must be with id=" + id); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java new file mode 100644 index 000000000000..f1e9b0e46376 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package ru.javawebinar.topjava.util.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java new file mode 100644 index 000000000000..e78a4b284a9a --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java @@ -0,0 +1,14 @@ +package ru.javawebinar.topjava.web; + +import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY; + +public class SecurityUtil { + + public static int authUserId() { + return 1; + } + + public static int authUserCaloriesPerDay() { + return DEFAULT_CALORIES_PER_DAY; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java new file mode 100644 index 000000000000..ab4e8ea8bb8e --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java @@ -0,0 +1,8 @@ +package ru.javawebinar.topjava.web.meal; + +import ru.javawebinar.topjava.service.MealService; + +public class MealRestController { + private MealService service; + +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java new file mode 100644 index 000000000000..16a68259ec20 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java @@ -0,0 +1,49 @@ +package ru.javawebinar.topjava.web.user; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.service.UserService; + +import java.util.List; + +import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNew; + +public abstract class AbstractUserController { + protected final Logger log = LoggerFactory.getLogger(getClass()); + + private UserService service; + + public List getAll() { + log.info("getAll"); + return service.getAll(); + } + + public User get(int id) { + log.info("get {}", id); + return service.get(id); + } + + public User create(User user) { + log.info("create {}", user); + checkNew(user); + return service.create(user); + } + + public void delete(int id) { + log.info("delete {}", id); + service.delete(id); + } + + public void update(User user, int id) { + log.info("update {} with id={}", user, id); + assureIdConsistent(user, id); + service.update(user); + } + + public User getByMail(String email) { + log.info("getByEmail {}", email); + return service.getByEmail(email); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java new file mode 100644 index 000000000000..ae3374468e41 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java @@ -0,0 +1,38 @@ +package ru.javawebinar.topjava.web.user; + +import ru.javawebinar.topjava.model.User; + +import java.util.List; + +public class AdminRestController extends AbstractUserController { + + @Override + public List 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 000000000000..b5062e20bc90 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -0,0 +1,20 @@ +package ru.javawebinar.topjava.web.user; + +import ru.javawebinar.topjava.model.User; + +import static ru.javawebinar.topjava.web.SecurityUtil.authUserId; + +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 From 1fc61b213025348165f1a53b8e9ae28709688e41 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 9 Feb 2023 23:01:30 +0700 Subject: [PATCH 006/125] apply 2_4_spring.patch --- pom.xml | 9 ++++++++ .../ru/javawebinar/topjava/model/User.java | 22 +++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index 7278f5ad42b8..96bde22954f1 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,8 @@ 1.8 UTF-8 UTF-8 + 5.3.25 + 1.2.11 1.7.36 @@ -56,6 +58,13 @@ runtime + + + org.springframework + spring-context + ${spring.version} + + javax.servlet diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index d88e381945d8..effd8364099e 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -1,8 +1,8 @@ package ru.javawebinar.topjava.model; -import java.util.Date; -import java.util.EnumSet; -import java.util.Set; +import org.springframework.util.CollectionUtils; + +import java.util.*; import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY; @@ -20,17 +20,17 @@ public class User extends AbstractNamedEntity { private int caloriesPerDay = DEFAULT_CALORIES_PER_DAY; - public User(Integer id, String name, String email, String password, Role role, Role... roles) { - this(id, name, email, password, DEFAULT_CALORIES_PER_DAY, true, EnumSet.of(role, roles)); + public User(Integer id, String name, String email, String password, Role... roles) { + this(id, name, email, password, DEFAULT_CALORIES_PER_DAY, true, Arrays.asList(roles)); } - public User(Integer id, String name, String email, String password, int caloriesPerDay, boolean enabled, Set roles) { + public User(Integer id, String name, String email, String password, int caloriesPerDay, boolean enabled, Collection roles) { super(id, name); this.email = email; this.password = password; this.caloriesPerDay = caloriesPerDay; this.enabled = enabled; - this.roles = roles; + setRoles(roles); } public String getEmail() { @@ -73,19 +73,23 @@ public Set getRoles() { return roles; } + public void setRoles(Collection roles) { + this.roles = CollectionUtils.isEmpty(roles) ? EnumSet.noneOf(Role.class) : EnumSet.copyOf(roles); + } + public String getPassword() { return password; } @Override public String toString() { - return "User (" + + return "User{" + "id=" + id + ", email=" + email + ", name=" + name + ", enabled=" + enabled + ", roles=" + roles + ", caloriesPerDay=" + caloriesPerDay + - ')'; + '}'; } } \ No newline at end of file From 6c7b978d68f972434942f56f7f4ce8f449aef380 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 9 Feb 2023 23:02:04 +0700 Subject: [PATCH 007/125] apply 2_4_spring_context.patch --- .../ru/javawebinar/topjava/SpringMain.java | 19 ++++++++ .../InMemoryMealRepository.java | 3 +- .../inmemory/InMemoryUserRepository.java | 43 +++++++++++++++++++ .../javawebinar/topjava/web/MealServlet.java | 2 +- src/main/resources/spring/spring-app.xml | 7 +++ 5 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/SpringMain.java rename src/main/java/ru/javawebinar/topjava/repository/{ => inmemory}/InMemoryMealRepository.java (91%) create mode 100644 src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java create mode 100644 src/main/resources/spring/spring-app.xml diff --git a/src/main/java/ru/javawebinar/topjava/SpringMain.java b/src/main/java/ru/javawebinar/topjava/SpringMain.java new file mode 100644 index 000000000000..e836531c305a --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/SpringMain.java @@ -0,0 +1,19 @@ +package ru.javawebinar.topjava; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.Arrays; + +public class SpringMain { + public static void main(String[] args) { + ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml"); + System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames())); + +// UserRepository userRepository = (UserRepository) appCtx.getBean("inmemoryUserRepository"); + UserRepository userRepository = appCtx.getBean(UserRepository.class); + userRepository.getAll(); + appCtx.close(); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java similarity index 91% rename from src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepository.java rename to src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java index ad6669056a4b..3c7c9ff94dd2 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -1,6 +1,7 @@ -package ru.javawebinar.topjava.repository; +package ru.javawebinar.topjava.repository.inmemory; import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.MealRepository; import ru.javawebinar.topjava.util.MealsUtil; import java.util.Collection; diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java new file mode 100644 index 000000000000..c9bcb8a4f289 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java @@ -0,0 +1,43 @@ +package ru.javawebinar.topjava.repository.inmemory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.Collections; +import java.util.List; + +public class InMemoryUserRepository implements UserRepository { + private static final Logger log = LoggerFactory.getLogger(InMemoryUserRepository.class); + + @Override + public boolean delete(int id) { + log.info("delete {}", id); + return true; + } + + @Override + public User save(User user) { + log.info("save {}", user); + return user; + } + + @Override + public User get(int id) { + log.info("get {}", id); + return null; + } + + @Override + public List getAll() { + log.info("getAll"); + return Collections.emptyList(); + } + + @Override + public User getByEmail(String email) { + log.info("getByEmail {}", email); + return null; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java index 78d73c710e57..7a0b8aed8ca7 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -3,8 +3,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.javawebinar.topjava.model.Meal; -import ru.javawebinar.topjava.repository.InMemoryMealRepository; import ru.javawebinar.topjava.repository.MealRepository; +import ru.javawebinar.topjava.repository.inmemory.InMemoryMealRepository; import ru.javawebinar.topjava.util.MealsUtil; import javax.servlet.ServletException; diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml new file mode 100644 index 000000000000..3a32c66999dc --- /dev/null +++ b/src/main/resources/spring/spring-app.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file From b8066a8d3cc95d0fddfec8b0fed81153fcac579f Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 9 Feb 2023 23:02:35 +0700 Subject: [PATCH 008/125] apply 2_4_dependency_injection.patch --- src/main/java/ru/javawebinar/topjava/SpringMain.java | 7 +++++++ .../java/ru/javawebinar/topjava/service/UserService.java | 6 +++++- src/main/resources/spring/spring-app.xml | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/javawebinar/topjava/SpringMain.java b/src/main/java/ru/javawebinar/topjava/SpringMain.java index e836531c305a..403417858f82 100644 --- a/src/main/java/ru/javawebinar/topjava/SpringMain.java +++ b/src/main/java/ru/javawebinar/topjava/SpringMain.java @@ -2,7 +2,10 @@ 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.repository.UserRepository; +import ru.javawebinar.topjava.service.UserService; import java.util.Arrays; @@ -14,6 +17,10 @@ public static void main(String[] args) { // UserRepository userRepository = (UserRepository) appCtx.getBean("inmemoryUserRepository"); UserRepository userRepository = appCtx.getBean(UserRepository.class); userRepository.getAll(); + + UserService userService = appCtx.getBean(UserService.class); + userService.create(new User(null, "userName", "email@mail.ru", "password", Role.ADMIN)); + appCtx.close(); } } diff --git a/src/main/java/ru/javawebinar/topjava/service/UserService.java b/src/main/java/ru/javawebinar/topjava/service/UserService.java index b02082e352c4..13d052611980 100644 --- a/src/main/java/ru/javawebinar/topjava/service/UserService.java +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -10,7 +10,11 @@ public class UserService { - private UserRepository repository; + private final UserRepository repository; + + public UserService(UserRepository repository) { + this.repository = repository; + } public User create(User user) { return repository.save(user); diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml index 3a32c66999dc..20771c90e37e 100644 --- a/src/main/resources/spring/spring-app.xml +++ b/src/main/resources/spring/spring-app.xml @@ -4,4 +4,7 @@ + + + \ No newline at end of file From 86440bff8dde80e11378e83da3f4c2ccb0873cd5 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 9 Feb 2023 23:03:27 +0700 Subject: [PATCH 009/125] apply 2_7_annotation_processing.patch --- .../ru/javawebinar/topjava/SpringMain.java | 20 +++++++------------ .../inmemory/InMemoryUserRepository.java | 2 ++ .../topjava/service/UserService.java | 2 ++ .../web/user/AbstractUserController.java | 2 ++ .../topjava/web/user/AdminRestController.java | 2 ++ .../web/user/ProfileRestController.java | 2 ++ src/main/resources/spring/spring-app.xml | 12 +++++++++-- 7 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/SpringMain.java b/src/main/java/ru/javawebinar/topjava/SpringMain.java index 403417858f82..85d0832e8728 100644 --- a/src/main/java/ru/javawebinar/topjava/SpringMain.java +++ b/src/main/java/ru/javawebinar/topjava/SpringMain.java @@ -4,23 +4,17 @@ import org.springframework.context.support.ClassPathXmlApplicationContext; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; -import ru.javawebinar.topjava.repository.UserRepository; -import ru.javawebinar.topjava.service.UserService; +import ru.javawebinar.topjava.web.user.AdminRestController; import java.util.Arrays; public class SpringMain { public static void main(String[] args) { - ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml"); - System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames())); - -// UserRepository userRepository = (UserRepository) appCtx.getBean("inmemoryUserRepository"); - UserRepository userRepository = appCtx.getBean(UserRepository.class); - userRepository.getAll(); - - UserService userService = appCtx.getBean(UserService.class); - userService.create(new User(null, "userName", "email@mail.ru", "password", Role.ADMIN)); - - appCtx.close(); + // java 7 automatic resource management (ARM) + try (ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext("spring/spring-app.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)); + } } } diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java index c9bcb8a4f289..e2f8a9b8b10d 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java @@ -2,12 +2,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Repository; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.UserRepository; import java.util.Collections; import java.util.List; +@Repository public class InMemoryUserRepository implements UserRepository { private static final Logger log = LoggerFactory.getLogger(InMemoryUserRepository.class); diff --git a/src/main/java/ru/javawebinar/topjava/service/UserService.java b/src/main/java/ru/javawebinar/topjava/service/UserService.java index 13d052611980..8fbe8dc0601f 100644 --- a/src/main/java/ru/javawebinar/topjava/service/UserService.java +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -1,5 +1,6 @@ package ru.javawebinar.topjava.service; +import org.springframework.stereotype.Service; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.UserRepository; @@ -8,6 +9,7 @@ import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFound; import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; +@Service public class UserService { private final UserRepository repository; diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java index 16a68259ec20..0000f1c1e02f 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java @@ -2,6 +2,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.service.UserService; @@ -13,6 +14,7 @@ public abstract class AbstractUserController { protected final Logger log = LoggerFactory.getLogger(getClass()); + @Autowired private UserService service; public List getAll() { diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java index ae3374468e41..b37a8ed6c8a5 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java @@ -1,9 +1,11 @@ package ru.javawebinar.topjava.web.user; +import org.springframework.stereotype.Controller; import ru.javawebinar.topjava.model.User; import java.util.List; +@Controller public class AdminRestController extends AbstractUserController { @Override diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java index b5062e20bc90..7d3702c31c46 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -1,9 +1,11 @@ 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() { diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml index 20771c90e37e..cac42ba13e3d 100644 --- a/src/main/resources/spring/spring-app.xml +++ b/src/main/resources/spring/spring-app.xml @@ -1,10 +1,18 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> + + + + + + + \ No newline at end of file From 00fdb98187ae6e22a38474c847c884202b3fb0a1 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Mon, 13 Feb 2023 10:46:32 +0700 Subject: [PATCH 010/125] remoe DSStore from master --- .DS_Store | Bin 8196 -> 0 bytes .gitignore | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 88b81404868cd1caad2d082c03633080e453d480..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHM-D(p-6h6~5&4yBKK_Ljrg10dyOy!8rK7m*IotbI6-KL75R&XZF`DS;|&&-!OyEz*o67^xXM3f^U2c2)g+@1#@N%C}2<2edx#J5;1}p>r zB?EkaaM2lS2B#X;tph@p0Ki3@Rt;m+0m{ZTSTi`)s7&EgR}aFN3R7YTZI0_Ms{?BW zry8|632jcoL>8t(5h^6;p|wIi_-;XW2>Ap5;$6ZrMLzuZPWxZz zLF;{N1!>Hh{dQ>ow5Iq1%@6Z6+oSN%yw&*BKJ@!xR4jfL3#rBQQpU+R%g&3&z8p28 zRx}Ljt-&)s+Lh98ACFt@Cw*_!T)lE%22raYcmrk7?|M*qzSj@Ba#WYYpc^XNhQ8or zoosV8Hy*E7?&RI_W@(al$JO;>-mR23CzGsm_2%unJIz=AK_K5^_GrS~9%)4%&m^-I zTl-OC5co1+cG%*^2`hAq)~SeIR+2}%7;WHqgDMz3g+>vd62A2}ybBUMa-=xpKlfc# z(JEB^t-h<6cS+5=s%HDE^Ui=1iJU0VYe+w_%CA^uJTtCAdw|^F-No3%9+Qv{Xb;hw zF@6gt)7Q2YF6Hf*yDTpR-_9XhuR+&=&P|HVzv zMz#!C2L3n$VsWdnRY#cPACJk1Tx+}NFVVTN-c+M9fl%c*P?h7r*&l`&yHI7Dn!%|? SoI$fc1QZQcSO)$o1K$BvJ18^& diff --git a/.gitignore b/.gitignore index 73e6d9e3dd11..c31578dce3f6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,5 @@ target *.iml log *.patch - +.DS_Store From ee5a3c6fb60158576ed4dc26796c612629399ec2 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 16 Feb 2023 06:24:00 +0700 Subject: [PATCH 011/125] apply pathc 3_0_fix --- src/main/java/ru/javawebinar/topjava/model/User.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index effd8364099e..0ec0257d923d 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -12,13 +12,13 @@ public class User extends AbstractNamedEntity { private String password; - private boolean enabled = true; + private boolean enabled; private Date registered = new Date(); private Set roles; - private int caloriesPerDay = DEFAULT_CALORIES_PER_DAY; + private int caloriesPerDay; public User(Integer id, String name, String email, String password, Role... roles) { this(id, name, email, password, DEFAULT_CALORIES_PER_DAY, true, Arrays.asList(roles)); From be5130105f9dbc336692c0976f2d873765e971df Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 16 Feb 2023 06:48:30 +0700 Subject: [PATCH 012/125] apply pathc 3_01_HW2 --- .../topjava/repository/MealRepository.java | 11 ++-- .../inmemory/InMemoryMealRepository.java | 52 +++++++++++++------ .../inmemory/InMemoryUserRepository.java | 46 ++++++++++------ .../javawebinar/topjava/web/MealServlet.java | 8 +-- 4 files changed, 75 insertions(+), 42 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java index 675cdbb1bd38..4311fcd26e44 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java @@ -2,19 +2,18 @@ import ru.javawebinar.topjava.model.Meal; -import java.util.Collection; +import java.util.List; -// TODO add userId public interface MealRepository { // null if updated meal does not belong to userId - Meal save(Meal meal); + Meal save(Meal meal, int userId); // false if meal does not belong to userId - boolean delete(int id); + boolean delete(int id, int userId); // null if meal does not belong to userId - Meal get(int id); + Meal get(int id, int userId); // ORDERED dateTime desc - Collection getAll(); + List getAll(int userId); } diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java index 3c7c9ff94dd2..6020e0975bcd 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -1,46 +1,68 @@ package ru.javawebinar.topjava.repository.inmemory; +import org.springframework.stereotype.Repository; +import org.springframework.util.CollectionUtils; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; import ru.javawebinar.topjava.util.MealsUtil; -import java.util.Collection; +import java.time.LocalDateTime; +import java.time.Month; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import static ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository.ADMIN_ID; +import static ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository.USER_ID; + +@Repository public class InMemoryMealRepository implements MealRepository { - private final Map repository = new ConcurrentHashMap<>(); + + // Map userId -> (mealId-> meal) + private final Map> usersMealsMap = new ConcurrentHashMap<>(); private final AtomicInteger counter = new AtomicInteger(0); { - MealsUtil.meals.forEach(this::save); + MealsUtil.meals.forEach(meal -> save(meal, USER_ID)); + save(new Meal(LocalDateTime.of(2015, Month.JUNE, 1, 14, 0), "Админ ланч", 510), ADMIN_ID); + save(new Meal(LocalDateTime.of(2015, Month.JUNE, 1, 21, 0), "Админ ужин", 1500), ADMIN_ID); } + @Override - public Meal save(Meal meal) { + public Meal save(Meal meal, int userId) { + // We cannot use method reference "ConcurrentHashMap::new" here. It will be equivalent wrong "new ConcurrentHashMap<>(userId)" + Map meals = usersMealsMap.computeIfAbsent(userId, uId -> new ConcurrentHashMap<>()); if (meal.isNew()) { meal.setId(counter.incrementAndGet()); - repository.put(meal.getId(), meal); + meals.put(meal.getId(), meal); return meal; } - // handle case: update, but not present in storage - return repository.computeIfPresent(meal.getId(), (id, oldMeal) -> meal); + return meals.computeIfPresent(meal.getId(), (id, oldMeal) -> meal); } @Override - public boolean delete(int id) { - return repository.remove(id) != null; + public boolean delete(int id, int userId) { + Map meals = usersMealsMap.get(userId); + return meals != null && meals.remove(id) != null; } @Override - public Meal get(int id) { - return repository.get(id); + public Meal get(int id, int userId) { + Map meals = usersMealsMap.get(userId); + return meals == null ? null : meals.get(id); } @Override - public Collection getAll() { - return repository.values(); + public List getAll(int userId) { + Map meals = usersMealsMap.get(userId); + return CollectionUtils.isEmpty(meals) ? Collections.emptyList() : + meals.values().stream() + .sorted(Comparator.comparing(Meal::getDateTime).reversed()) + .collect(Collectors.toList()); } -} - +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java index e2f8a9b8b10d..3565868cc632 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java @@ -1,45 +1,57 @@ package ru.javawebinar.topjava.repository.inmemory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.stereotype.Repository; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.UserRepository; -import java.util.Collections; +import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; @Repository public class InMemoryUserRepository implements UserRepository { - private static final Logger log = LoggerFactory.getLogger(InMemoryUserRepository.class); + + public static final int USER_ID = 1; + public static final int ADMIN_ID = 2; + + private final Map usersMap = new ConcurrentHashMap<>(); + private final AtomicInteger counter = new AtomicInteger(0); @Override - public boolean delete(int id) { - log.info("delete {}", id); - return true; + public User save(User user) { + if (user.isNew()) { + user.setId(counter.incrementAndGet()); + usersMap.put(user.getId(), user); + return user; + } + return usersMap.computeIfPresent(user.getId(), (id, oldUser) -> user); } @Override - public User save(User user) { - log.info("save {}", user); - return user; + public boolean delete(int id) { + return usersMap.remove(id) != null; } @Override public User get(int id) { - log.info("get {}", id); - return null; + return usersMap.get(id); } @Override public List getAll() { - log.info("getAll"); - return Collections.emptyList(); + return usersMap.values().stream() + .sorted(Comparator.comparing(User::getName).thenComparing(User::getEmail)) + .collect(Collectors.toList()); } @Override public User getByEmail(String email) { - log.info("getByEmail {}", email); - return null; + return usersMap.values().stream() + .filter(u -> email.equals(u.getEmail())) + .findFirst() + .orElse(null); } -} +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java index 7a0b8aed8ca7..d6c561396c6b 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -37,7 +37,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) Integer.parseInt(request.getParameter("calories"))); log.info(meal.isNew() ? "Create {}" : "Update {}", meal); - repository.save(meal); + repository.save(meal, SecurityUtil.authUserId()); response.sendRedirect("meals"); } @@ -49,14 +49,14 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t case "delete": int id = getId(request); log.info("Delete id={}", id); - repository.delete(id); + repository.delete(id, SecurityUtil.authUserId()); response.sendRedirect("meals"); break; case "create": case "update": final Meal meal = "create".equals(action) ? new Meal(LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), "", 1000) : - repository.get(getId(request)); + repository.get(getId(request), SecurityUtil.authUserId()); request.setAttribute("meal", meal); request.getRequestDispatcher("/mealForm.jsp").forward(request, response); break; @@ -64,7 +64,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t default: log.info("getAll"); request.setAttribute("meals", - MealsUtil.getTos(repository.getAll(), MealsUtil.DEFAULT_CALORIES_PER_DAY)); + MealsUtil.getTos(repository.getAll(SecurityUtil.authUserId()), MealsUtil.DEFAULT_CALORIES_PER_DAY)); request.getRequestDispatcher("/meals.jsp").forward(request, response); break; } From bec3ad96741ef58596edc2d5fd223ffb7ef0ac8e Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 16 Feb 2023 06:49:51 +0700 Subject: [PATCH 013/125] apply pathc 3_02_HW2 --- .../topjava/repository/MealRepository.java | 4 ++++ .../repository/inmemory/InMemoryMealRepository.java | 12 ++++++++++++ .../ru/javawebinar/topjava/util/DateTimeUtil.java | 6 ------ .../java/ru/javawebinar/topjava/util/MealsUtil.java | 2 +- src/main/java/ru/javawebinar/topjava/util/Util.java | 9 +++++++++ 5 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/util/Util.java diff --git a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java index 4311fcd26e44..9461d5f9f693 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java @@ -2,6 +2,7 @@ import ru.javawebinar.topjava.model.Meal; +import java.time.LocalDateTime; import java.util.List; public interface MealRepository { @@ -16,4 +17,7 @@ public interface MealRepository { // ORDERED dateTime desc List getAll(int userId); + + // ORDERED dateTime desc + List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId); } diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java index 6020e0975bcd..7a194efbb5d2 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -5,6 +5,7 @@ import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; import ru.javawebinar.topjava.util.MealsUtil; +import ru.javawebinar.topjava.util.Util; import java.time.LocalDateTime; import java.time.Month; @@ -14,6 +15,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; import java.util.stream.Collectors; import static ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository.ADMIN_ID; @@ -57,11 +59,21 @@ public Meal get(int id, int 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) { Map meals = usersMealsMap.get(userId); return CollectionUtils.isEmpty(meals) ? Collections.emptyList() : meals.values().stream() + .filter(filter) .sorted(Comparator.comparing(Meal::getDateTime).reversed()) .collect(Collectors.toList()); } diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java index 3f23f83fde65..9db7a809b13b 100644 --- a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -1,18 +1,12 @@ package ru.javawebinar.topjava.util; import java.time.LocalDateTime; -import java.time.LocalTime; import java.time.format.DateTimeFormatter; public class DateTimeUtil { private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); - public static boolean isBetweenHalfOpen(LocalTime lt, LocalTime startTime, LocalTime endTime) { - return lt.compareTo(startTime) >= 0 && lt.compareTo(endTime) < 0; - } - public static String toString(LocalDateTime ldt) { return ldt == null ? "" : ldt.format(DATE_TIME_FORMATTER); } } - diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index 8d940a63efa1..ffea126631f4 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -32,7 +32,7 @@ public static List getTos(Collection meals, int caloriesPerDay) { } public static List getFilteredTos(Collection meals, int caloriesPerDay, LocalTime startTime, LocalTime endTime) { - return filterByPredicate(meals, caloriesPerDay, meal -> DateTimeUtil.isBetweenHalfOpen(meal.getTime(), startTime, endTime)); + return filterByPredicate(meals, caloriesPerDay, meal -> Util.isBetweenHalfOpen(meal.getTime(), startTime, endTime)); } private static List filterByPredicate(Collection meals, int caloriesPerDay, Predicate filter) { diff --git a/src/main/java/ru/javawebinar/topjava/util/Util.java b/src/main/java/ru/javawebinar/topjava/util/Util.java new file mode 100644 index 000000000000..a17a6927f975 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/Util.java @@ -0,0 +1,9 @@ +package ru.javawebinar.topjava.util; + +import org.springframework.lang.Nullable; + +public class Util { + public static > boolean isBetweenHalfOpen(T value, @Nullable T start, @Nullable T end) { + return (start == null || value.compareTo(start) >= 0) && (end == null || value.compareTo(end) < 0); + } +} \ No newline at end of file From 00fa2446d7a9b501941ed0fef220f4c6ecc7c0fa Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 16 Feb 2023 07:36:15 +0700 Subject: [PATCH 014/125] apply patch 3_02_HW2_meal --- .../ru/javawebinar/topjava/SpringMain.java | 16 +++++ .../ru/javawebinar/topjava/model/Meal.java | 18 +---- .../topjava/service/MealService.java | 40 ++++++++++- .../topjava/{model => to}/MealTo.java | 2 +- .../topjava/util/DateTimeUtil.java | 13 ++++ .../javawebinar/topjava/util/MealsUtil.java | 2 +- .../topjava/web/meal/MealRestController.java | 70 ++++++++++++++++++- src/main/webapp/meals.jsp | 2 +- 8 files changed, 142 insertions(+), 21 deletions(-) rename src/main/java/ru/javawebinar/topjava/{model => to}/MealTo.java (96%) diff --git a/src/main/java/ru/javawebinar/topjava/SpringMain.java b/src/main/java/ru/javawebinar/topjava/SpringMain.java index 85d0832e8728..b869d1c6e382 100644 --- a/src/main/java/ru/javawebinar/topjava/SpringMain.java +++ b/src/main/java/ru/javawebinar/topjava/SpringMain.java @@ -4,9 +4,15 @@ 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) { @@ -15,6 +21,16 @@ public static void main(String[] args) { 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/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index 3abbee42511e..9eed15f706be 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -4,9 +4,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; -public class Meal { - private Integer id; - +public class Meal extends AbstractBaseEntity { private final LocalDateTime dateTime; private final String description; @@ -18,20 +16,12 @@ public Meal(LocalDateTime dateTime, String description, int calories) { } public Meal(Integer id, LocalDateTime dateTime, String description, int calories) { - this.id = id; + super(id); this.dateTime = dateTime; this.description = description; this.calories = calories; } - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - public LocalDateTime getDateTime() { return dateTime; } @@ -52,10 +42,6 @@ public LocalTime getTime() { return dateTime.toLocalTime(); } - public boolean isNew() { - return id == null; - } - @Override public String toString() { return "Meal{" + diff --git a/src/main/java/ru/javawebinar/topjava/service/MealService.java b/src/main/java/ru/javawebinar/topjava/service/MealService.java index 0dc4a43c394f..7957a0895d47 100644 --- a/src/main/java/ru/javawebinar/topjava/service/MealService.java +++ b/src/main/java/ru/javawebinar/topjava/service/MealService.java @@ -1,9 +1,47 @@ package ru.javawebinar.topjava.service; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; +import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; +import java.time.LocalDate; +import java.util.List; + +import static ru.javawebinar.topjava.util.DateTimeUtil.atStartOfDayOrMin; +import static ru.javawebinar.topjava.util.DateTimeUtil.atStartOfNextDayOrMax; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; + +@Service public class MealService { - private MealRepository repository; + private final MealRepository repository; + + public MealService(MealRepository repository) { + this.repository = repository; + } + + public Meal get(int id, int userId) { + return checkNotFoundWithId(repository.get(id, userId), id); + } + + public void delete(int id, int userId) { + checkNotFoundWithId(repository.delete(id, userId), id); + } + + public List getBetweenInclusive(@Nullable LocalDate startDate, @Nullable LocalDate endDate, int userId) { + return repository.getBetweenHalfOpen(atStartOfDayOrMin(startDate), atStartOfNextDayOrMax(endDate), userId); + } + + public List getAll(int userId) { + return repository.getAll(userId); + } + + public void update(Meal meal, int userId) { + checkNotFoundWithId(repository.save(meal, userId), meal.getId()); + } + public Meal create(Meal meal, int userId) { + return repository.save(meal, userId); + } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/MealTo.java b/src/main/java/ru/javawebinar/topjava/to/MealTo.java similarity index 96% rename from src/main/java/ru/javawebinar/topjava/model/MealTo.java rename to src/main/java/ru/javawebinar/topjava/to/MealTo.java index 01b3a5fdaad1..d14feae792b7 100644 --- a/src/main/java/ru/javawebinar/topjava/model/MealTo.java +++ b/src/main/java/ru/javawebinar/topjava/to/MealTo.java @@ -1,4 +1,4 @@ -package ru.javawebinar.topjava.model; +package ru.javawebinar.topjava.to; import java.time.LocalDateTime; diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java index 9db7a809b13b..d46d38a9d08f 100644 --- a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -1,11 +1,24 @@ package ru.javawebinar.topjava.util; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; public class DateTimeUtil { private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + // DB doesn't support LocalDate.MIN/MAX + private static final LocalDateTime MIN_DATE = LocalDateTime.of(1, 1, 1, 0, 0); + private static final LocalDateTime MAX_DATE = LocalDateTime.of(3000, 1, 1, 0, 0); + + public static LocalDateTime atStartOfDayOrMin(LocalDate localDate) { + return localDate != null ? localDate.atStartOfDay() : MIN_DATE; + } + + public static LocalDateTime atStartOfNextDayOrMax(LocalDate localDate) { + return localDate != null ? localDate.plusDays(1).atStartOfDay() : MAX_DATE; + } + public static String toString(LocalDateTime ldt) { return ldt == null ? "" : ldt.format(DATE_TIME_FORMATTER); } diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index ffea126631f4..d10e1e674d47 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -1,7 +1,7 @@ package ru.javawebinar.topjava.util; import ru.javawebinar.topjava.model.Meal; -import ru.javawebinar.topjava.model.MealTo; +import ru.javawebinar.topjava.to.MealTo; import java.time.LocalDate; import java.time.LocalDateTime; diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java index ab4e8ea8bb8e..bbfe35e3f0fa 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java @@ -1,8 +1,76 @@ package ru.javawebinar.topjava.web.meal; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Controller; +import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.service.MealService; +import ru.javawebinar.topjava.to.MealTo; +import ru.javawebinar.topjava.util.MealsUtil; +import ru.javawebinar.topjava.web.SecurityUtil; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNew; + +@Controller public class MealRestController { - private MealService service; + private static final Logger log = LoggerFactory.getLogger(MealRestController.class); + + private final MealService service; + + public MealRestController(MealService service) { + this.service = service; + } + + public Meal get(int id) { + int userId = SecurityUtil.authUserId(); + log.info("get meal {} for user {}", id, userId); + return service.get(id, userId); + } + + public void delete(int id) { + int userId = SecurityUtil.authUserId(); + log.info("delete meal {} for user {}", id, userId); + service.delete(id, userId); + } + + public List getAll() { + int userId = SecurityUtil.authUserId(); + log.info("getAll for user {}", userId); + return MealsUtil.getTos(service.getAll(userId), SecurityUtil.authUserCaloriesPerDay()); + } + + public Meal create(Meal meal) { + int userId = SecurityUtil.authUserId(); + checkNew(meal); + log.info("create {} for user {}", meal, userId); + return service.create(meal, userId); + } + + public void update(Meal meal, int id) { + int userId = SecurityUtil.authUserId(); + assureIdConsistent(meal, id); + log.info("update {} for user {}", meal, userId); + service.update(meal, userId); + } + + /** + *
      Filter separately + *
    1. by date
    2. + *
    3. by time for every date
    4. + *
    + */ + public List getBetween(@Nullable LocalDate startDate, @Nullable LocalTime startTime, + @Nullable LocalDate endDate, @Nullable LocalTime endTime) { + int userId = SecurityUtil.authUserId(); + log.info("getBetween dates({} - {}) time({} - {}) for user {}", startDate, endDate, startTime, endTime, userId); + List mealsDateFiltered = service.getBetweenInclusive(startDate, endDate, userId); + return MealsUtil.getFilteredTos(mealsDateFiltered, SecurityUtil.authUserCaloriesPerDay(), startTime, endTime); + } } \ No newline at end of file diff --git a/src/main/webapp/meals.jsp b/src/main/webapp/meals.jsp index 224d9876135b..10f92805fd64 100644 --- a/src/main/webapp/meals.jsp +++ b/src/main/webapp/meals.jsp @@ -34,7 +34,7 @@ - + <%--${meal.dateTime.toLocalDate()} ${meal.dateTime.toLocalTime()}--%> From 75d1a05862b8a0d405a761d511d8efb0de2763ca Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 16 Feb 2023 07:49:29 +0700 Subject: [PATCH 015/125] apply patch 3_04_refactor_repository --- .../inmemory/InMemoryBaseRepository.java | 36 +++++++++++++++++++ .../inmemory/InMemoryMealRepository.java | 29 ++++++--------- .../inmemory/InMemoryUserRepository.java | 36 +++---------------- 3 files changed, 51 insertions(+), 50 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java new file mode 100644 index 000000000000..d104014ae88f --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java @@ -0,0 +1,36 @@ +package ru.javawebinar.topjava.repository.inmemory; + +import ru.javawebinar.topjava.model.AbstractBaseEntity; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +public class InMemoryBaseRepository { + + private static final AtomicInteger counter = new AtomicInteger(0); + + private final Map map = new ConcurrentHashMap<>(); + + public T save(T entity) { + 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(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java index 7a194efbb5d2..645d2e09e5cc 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -1,7 +1,6 @@ package ru.javawebinar.topjava.repository.inmemory; import org.springframework.stereotype.Repository; -import org.springframework.util.CollectionUtils; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; import ru.javawebinar.topjava.util.MealsUtil; @@ -14,7 +13,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -24,9 +22,8 @@ @Repository public class InMemoryMealRepository implements MealRepository { - // Map userId -> (mealId-> meal) - private final Map> usersMealsMap = new ConcurrentHashMap<>(); - private final AtomicInteger counter = new AtomicInteger(0); + // Map userId -> mealRepository + private final Map> usersMealsMap = new ConcurrentHashMap<>(); { MealsUtil.meals.forEach(meal -> save(meal, USER_ID)); @@ -37,25 +34,19 @@ public class InMemoryMealRepository implements MealRepository { @Override public Meal save(Meal meal, int userId) { - // We cannot use method reference "ConcurrentHashMap::new" here. It will be equivalent wrong "new ConcurrentHashMap<>(userId)" - Map meals = usersMealsMap.computeIfAbsent(userId, uId -> new ConcurrentHashMap<>()); - if (meal.isNew()) { - meal.setId(counter.incrementAndGet()); - meals.put(meal.getId(), meal); - return meal; - } - return meals.computeIfPresent(meal.getId(), (id, oldMeal) -> meal); + InMemoryBaseRepository meals = usersMealsMap.computeIfAbsent(userId, uId -> new InMemoryBaseRepository<>()); + return meals.save(meal); } @Override public boolean delete(int id, int userId) { - Map meals = usersMealsMap.get(userId); - return meals != null && meals.remove(id) != null; + InMemoryBaseRepository meals = usersMealsMap.get(userId); + return meals != null && meals.delete(id); } @Override public Meal get(int id, int userId) { - Map meals = usersMealsMap.get(userId); + InMemoryBaseRepository meals = usersMealsMap.get(userId); return meals == null ? null : meals.get(id); } @@ -70,9 +61,9 @@ public List getAll(int userId) { } private List filterByPredicate(int userId, Predicate filter) { - Map meals = usersMealsMap.get(userId); - return CollectionUtils.isEmpty(meals) ? Collections.emptyList() : - meals.values().stream() + InMemoryBaseRepository meals = usersMealsMap.get(userId); + return meals == null ? Collections.emptyList() : + meals.getCollection().stream() .filter(filter) .sorted(Comparator.comparing(Meal::getDateTime).reversed()) .collect(Collectors.toList()); diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java index 3565868cc632..f8968a815041 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java @@ -6,50 +6,24 @@ import java.util.Comparator; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @Repository -public class InMemoryUserRepository implements UserRepository { +public class InMemoryUserRepository extends InMemoryBaseRepository implements UserRepository { - public static final int USER_ID = 1; - public static final int ADMIN_ID = 2; - - private final Map usersMap = new ConcurrentHashMap<>(); - private final AtomicInteger counter = new AtomicInteger(0); - - @Override - public User save(User user) { - if (user.isNew()) { - user.setId(counter.incrementAndGet()); - usersMap.put(user.getId(), user); - return user; - } - return usersMap.computeIfPresent(user.getId(), (id, oldUser) -> user); - } - - @Override - public boolean delete(int id) { - return usersMap.remove(id) != null; - } - - @Override - public User get(int id) { - return usersMap.get(id); - } + static final int USER_ID = 1; + static final int ADMIN_ID = 2; @Override public List getAll() { - return usersMap.values().stream() + return getCollection().stream() .sorted(Comparator.comparing(User::getName).thenComparing(User::getEmail)) .collect(Collectors.toList()); } @Override public User getByEmail(String email) { - return usersMap.values().stream() + return getCollection().stream() .filter(u -> email.equals(u.getEmail())) .findFirst() .orElse(null); From f798411139daf1715dcd5111ec2010d15b559be8 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 16 Feb 2023 08:03:04 +0700 Subject: [PATCH 016/125] apply patch 3_05_HW2_MealServlet --- .../javawebinar/topjava/web/MealServlet.java | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java index d6c561396c6b..dc644f27e19a 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -1,11 +1,10 @@ package ru.javawebinar.topjava.web; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.util.StringUtils; import ru.javawebinar.topjava.model.Meal; -import ru.javawebinar.topjava.repository.MealRepository; -import ru.javawebinar.topjava.repository.inmemory.InMemoryMealRepository; -import ru.javawebinar.topjava.util.MealsUtil; +import ru.javawebinar.topjava.web.meal.MealRestController; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -17,27 +16,35 @@ import java.util.Objects; public class MealServlet extends HttpServlet { - private static final Logger log = LoggerFactory.getLogger(MealServlet.class); - private MealRepository repository; + private ConfigurableApplicationContext springContext; + private MealRestController mealController; @Override public void init() { - repository = new InMemoryMealRepository(); + springContext = new ClassPathXmlApplicationContext("spring/spring-app.xml"); + mealController = springContext.getBean(MealRestController.class); + } + + @Override + public void destroy() { + springContext.close(); + super.destroy(); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { request.setCharacterEncoding("UTF-8"); - String id = request.getParameter("id"); - - Meal meal = new Meal(id.isEmpty() ? null : Integer.valueOf(id), + Meal meal = new Meal( LocalDateTime.parse(request.getParameter("dateTime")), request.getParameter("description"), Integer.parseInt(request.getParameter("calories"))); - log.info(meal.isNew() ? "Create {}" : "Update {}", meal); - repository.save(meal, SecurityUtil.authUserId()); + if (StringUtils.hasLength(request.getParameter("id"))) { + mealController.update(meal, getId(request)); + } else { + mealController.create(meal); + } response.sendRedirect("meals"); } @@ -48,23 +55,20 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t switch (action == null ? "all" : action) { case "delete": int id = getId(request); - log.info("Delete id={}", id); - repository.delete(id, SecurityUtil.authUserId()); + mealController.delete(id); response.sendRedirect("meals"); break; case "create": case "update": final Meal meal = "create".equals(action) ? new Meal(LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), "", 1000) : - repository.get(getId(request), SecurityUtil.authUserId()); + mealController.get(getId(request)); request.setAttribute("meal", meal); request.getRequestDispatcher("/mealForm.jsp").forward(request, response); break; case "all": default: - log.info("getAll"); - request.setAttribute("meals", - MealsUtil.getTos(repository.getAll(SecurityUtil.authUserId()), MealsUtil.DEFAULT_CALORIES_PER_DAY)); + request.setAttribute("meals", mealController.getAll()); request.getRequestDispatcher("/meals.jsp").forward(request, response); break; } From 7e03ebee0687f65e90b92ad26172155c37c51c0b Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 16 Feb 2023 08:03:32 +0700 Subject: [PATCH 017/125] apply patch 3_06_HW2 --- .../topjava/util/DateTimeUtil.java | 14 ++++++++ .../javawebinar/topjava/web/MealServlet.java | 13 +++++++ src/main/webapp/css/style.css | 24 +++++++++++++ src/main/webapp/mealForm.jsp | 19 +--------- src/main/webapp/meals.jsp | 36 ++++++++++++------- 5 files changed, 76 insertions(+), 30 deletions(-) create mode 100644 src/main/webapp/css/style.css diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java index d46d38a9d08f..91df7fd242b9 100644 --- a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -1,7 +1,11 @@ package ru.javawebinar.topjava.util; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.format.DateTimeFormatter; public class DateTimeUtil { @@ -22,4 +26,14 @@ public static LocalDateTime atStartOfNextDayOrMax(LocalDate localDate) { public static String toString(LocalDateTime ldt) { return ldt == null ? "" : ldt.format(DATE_TIME_FORMATTER); } + + public static @Nullable + LocalDate parseLocalDate(@Nullable String str) { + return StringUtils.hasLength(str) ? LocalDate.parse(str) : null; + } + + public static @Nullable + LocalTime parseLocalTime(@Nullable String str) { + return StringUtils.hasLength(str) ? LocalTime.parse(str) : null; + } } diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java index dc644f27e19a..544b902480f9 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -11,10 +11,15 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.temporal.ChronoUnit; import java.util.Objects; +import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalDate; +import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalTime; + public class MealServlet extends HttpServlet { private ConfigurableApplicationContext springContext; @@ -66,6 +71,14 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t request.setAttribute("meal", meal); request.getRequestDispatcher("/mealForm.jsp").forward(request, response); break; + case "filter": + LocalDate startDate = parseLocalDate(request.getParameter("startDate")); + LocalDate endDate = parseLocalDate(request.getParameter("endDate")); + LocalTime startTime = parseLocalTime(request.getParameter("startTime")); + LocalTime endTime = parseLocalTime(request.getParameter("endTime")); + request.setAttribute("meals", mealController.getBetween(startDate, startTime, endDate, endTime)); + request.getRequestDispatcher("/meals.jsp").forward(request, response); + break; case "all": default: request.setAttribute("meals", mealController.getAll()); diff --git a/src/main/webapp/css/style.css b/src/main/webapp/css/style.css new file mode 100644 index 000000000000..cfffdcb53359 --- /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/mealForm.jsp b/src/main/webapp/mealForm.jsp index 28f140b65fce..98a6f48735dc 100644 --- a/src/main/webapp/mealForm.jsp +++ b/src/main/webapp/mealForm.jsp @@ -4,24 +4,7 @@ Meal - +
    diff --git a/src/main/webapp/meals.jsp b/src/main/webapp/meals.jsp index 10f92805fd64..7d9bf3e425b2 100644 --- a/src/main/webapp/meals.jsp +++ b/src/main/webapp/meals.jsp @@ -2,25 +2,37 @@ <%@ 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" %> -<%--<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>--%> - Meal list - + Meals +

    Home


    Meals

    +
    + +
    +
    From Date (inclusive):
    +
    +
    +
    +
    To Date (inclusive):
    +
    +
    +
    +
    From Time (inclusive):
    +
    +
    +
    +
    To Time (exclusive):
    +
    +
    + +
    +
    Add Meal

    @@ -35,7 +47,7 @@ - + + + + + + + + + + +
    <%--${meal.dateTime.toLocalDate()} ${meal.dateTime.toLocalTime()}--%> <%--<%=TimeUtil.toString(meal.getDateTime())%>--%> From 19272ffea079474f432e805a57f3dc6ddb201ae3 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 16 Feb 2023 08:03:51 +0700 Subject: [PATCH 018/125] apply patch 3_07_HW2 --- .../ru/javawebinar/topjava/web/SecurityUtil.java | 8 +++++++- .../java/ru/javawebinar/topjava/web/UserServlet.java | 7 +++++++ src/main/webapp/index.html | 12 ++++++++---- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java index e78a4b284a9a..b9639bf1b9ea 100644 --- a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java +++ b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java @@ -4,8 +4,14 @@ public class SecurityUtil { + private static int id = 1; + public static int authUserId() { - return 1; + return id; + } + + public static void setAuthUserId(int id) { + SecurityUtil.id = id; } public static int authUserCaloriesPerDay() { diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java index f6cf12e69976..226023400c70 100644 --- a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java @@ -13,6 +13,13 @@ public class UserServlet extends HttpServlet { private static final Logger log = getLogger(UserServlet.class); + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + int userId = Integer.parseInt(request.getParameter("userId")); + SecurityUtil.setAuthUserId(userId); + response.sendRedirect("meals"); + } + @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { log.debug("forward to users"); diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index 714683bd3856..e887926bab6e 100644 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -6,9 +6,13 @@

    Проект Java Enterprise (Topjava)


    - +
    + Meals of  + + +
    From 70ac34b8abf83966b6e933f28299881e50f7499b Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 16 Feb 2023 08:25:37 +0700 Subject: [PATCH 019/125] apply patch 3_08 --- pom.xml | 6 ++++++ .../inmemory/InMemoryMealRepository.java | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/pom.xml b/pom.xml index 96bde22954f1..fdd970e1bcbe 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,12 @@ runtime + + javax.annotation + javax.annotation-api + 1.3.2 + + org.springframework diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java index 645d2e09e5cc..7045913076cd 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -1,11 +1,15 @@ package ru.javawebinar.topjava.repository.inmemory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Repository; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; import ru.javawebinar.topjava.util.MealsUtil; import ru.javawebinar.topjava.util.Util; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; import java.time.LocalDateTime; import java.time.Month; import java.util.Collections; @@ -21,6 +25,7 @@ @Repository public class InMemoryMealRepository implements MealRepository { + private static final Logger log = LoggerFactory.getLogger(InMemoryMealRepository.class); // Map userId -> mealRepository private final Map> usersMealsMap = new ConcurrentHashMap<>(); @@ -38,6 +43,16 @@ public Meal save(Meal meal, int userId) { 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) { InMemoryBaseRepository meals = usersMealsMap.get(userId); From 82e9d50b3d1b9149f9efc3940b116f45aba2347e Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 16 Feb 2023 14:46:40 +0700 Subject: [PATCH 020/125] apply patch 3_09 --- pom.xml | 19 +++++++ .../ru/javawebinar/topjava/SpringMain.java | 0 .../ru/javawebinar/topjava/UserTestData.java | 15 ++++++ .../inmemory/InMemoryBaseRepository.java | 8 ++- .../inmemory/InMemoryMealRepository.java | 4 +- .../inmemory/InMemoryUserRepository.java | 12 ++++- .../user/InMemoryAdminRestControllerTest.java | 52 +++++++++++++++++++ 7 files changed, 104 insertions(+), 6 deletions(-) rename src/{main => test}/java/ru/javawebinar/topjava/SpringMain.java (100%) create mode 100644 src/test/java/ru/javawebinar/topjava/UserTestData.java rename src/{main => test}/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java (80%) rename src/{main => test}/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java (94%) rename src/{main => test}/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java (80%) create mode 100644 src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java diff --git a/pom.xml b/pom.xml index fdd970e1bcbe..1ae3319e7453 100644 --- a/pom.xml +++ b/pom.xml @@ -20,6 +20,9 @@ 1.2.11 1.7.36 + + + 4.13.2 @@ -40,6 +43,14 @@ ${java.version} + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + -Dfile.encoding=UTF-8 + + @@ -84,6 +95,14 @@ jstl 1.2 + + + + junit + junit + ${junit.version} + test + diff --git a/src/main/java/ru/javawebinar/topjava/SpringMain.java b/src/test/java/ru/javawebinar/topjava/SpringMain.java similarity index 100% rename from src/main/java/ru/javawebinar/topjava/SpringMain.java rename to src/test/java/ru/javawebinar/topjava/SpringMain.java 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 000000000000..dcc5b3474fda --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java @@ -0,0 +1,15 @@ +package ru.javawebinar.topjava; + +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; + +public class UserTestData { + public static final int USER_ID = 1; + public static final int ADMIN_ID = 2; + public static final int GUEST_ID = 3; + 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"); +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java similarity index 80% rename from src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java rename to src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java index d104014ae88f..15d5528dcfc7 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java @@ -9,9 +9,9 @@ public class InMemoryBaseRepository { - private static final AtomicInteger counter = new AtomicInteger(0); + static final AtomicInteger counter = new AtomicInteger(0); - private final Map map = new ConcurrentHashMap<>(); + final Map map = new ConcurrentHashMap<>(); public T save(T entity) { if (entity.isNew()) { @@ -33,4 +33,8 @@ public T get(int id) { Collection getCollection() { return map.values(); } + + void put(T entity) { + map.put(entity.getId(), entity); + } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java similarity index 94% rename from src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java rename to src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java index 7045913076cd..d8a0fada56c7 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -20,8 +20,8 @@ import java.util.function.Predicate; import java.util.stream.Collectors; -import static ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository.ADMIN_ID; -import static ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository.USER_ID; +import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; +import static ru.javawebinar.topjava.UserTestData.USER_ID; @Repository public class InMemoryMealRepository implements MealRepository { diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java similarity index 80% rename from src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java rename to src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java index f8968a815041..fa8fc861bbf6 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java @@ -8,11 +8,19 @@ import java.util.List; import java.util.stream.Collectors; +import static ru.javawebinar.topjava.UserTestData.*; + + @Repository public class InMemoryUserRepository extends InMemoryBaseRepository implements UserRepository { - static final int USER_ID = 1; - static final int ADMIN_ID = 2; + public void init() { + map.clear(); + put(user); + put(admin); + put(guest); + counter.getAndSet(GUEST_ID + 1); + } @Override public List getAll() { 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 000000000000..33c83774d9b3 --- /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"); + 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 From 7201c243e6dcb709eb1d81876497c924ab208044 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 16 Feb 2023 16:46:56 +0700 Subject: [PATCH 021/125] apply patch 3_10 --- pom.xml | 6 +++ ...InMemoryAdminRestControllerSpringTest.java | 41 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java diff --git a/pom.xml b/pom.xml index 1ae3319e7453..0070ca37e9ed 100644 --- a/pom.xml +++ b/pom.xml @@ -103,6 +103,12 @@ ${junit.version} test + + org.springframework + spring-test + ${spring.version} + test + 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 000000000000..afdffdfafaa1 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java @@ -0,0 +1,41 @@ +package ru.javawebinar.topjava.web.user; + +import org.junit.Assert; +import org.junit.Before; +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") +@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 From f2177519017f3a54c960bf902d597845463d554c Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 18 Feb 2023 14:14:25 +0700 Subject: [PATCH 022/125] apply patch 3_11 --- .gitignore | 1 + pom.xml | 9 +++++++++ src/main/resources/db/postgres.properties | 3 +++ 3 files changed, 13 insertions(+) create mode 100644 src/main/resources/db/postgres.properties diff --git a/.gitignore b/.gitignore index c31578dce3f6..f29adb5cb9cd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ target log *.patch .DS_Store +/pgdata diff --git a/pom.xml b/pom.xml index 0070ca37e9ed..660c6037f1d4 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,8 @@ 1.2.11 1.7.36 + + 42.5.3 4.13.2 @@ -82,6 +84,13 @@ ${spring.version} + + + org.postgresql + postgresql + ${postgresql.version} + + javax.servlet diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties new file mode 100644 index 000000000000..9cade556463e --- /dev/null +++ b/src/main/resources/db/postgres.properties @@ -0,0 +1,3 @@ +database.url=jdbc:postgresql://localhost:5432/topjava +database.username=user +database.password=password From afae5994bf03284bdc6cf1c752b0b073eb4bc964 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 18 Feb 2023 15:51:47 +0700 Subject: [PATCH 023/125] apply patch 3_12 --- .gitignore | 3 +- pom.xml | 5 ++ .../repository/jdbc/JdbcUserRepository.java | 81 +++++++++++++++++++ src/main/resources/db/initDB.sql | 25 ++++++ src/main/resources/db/populateDB.sql | 12 +++ src/main/resources/db/postgres.properties | 2 +- src/main/resources/spring/spring-db.xml | 25 ++++++ 7 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java create mode 100644 src/main/resources/db/initDB.sql create mode 100644 src/main/resources/db/populateDB.sql create mode 100644 src/main/resources/spring/spring-db.xml diff --git a/.gitignore b/.gitignore index f29adb5cb9cd..97034d1994cc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ target log *.patch .DS_Store -/pgdata +/db +src/main/resources/db/postgres.properties diff --git a/pom.xml b/pom.xml index 660c6037f1d4..bcad3acfb324 100644 --- a/pom.xml +++ b/pom.xml @@ -83,6 +83,11 @@ spring-context ${spring.version} + + org.springframework + spring-jdbc + ${spring.version} + diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java new file mode 100644 index 000000000000..7f6f8626a869 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java @@ -0,0 +1,81 @@ +package ru.javawebinar.topjava.repository.jdbc; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.List; + +@Repository +public class JdbcUserRepository implements UserRepository { + + private static final BeanPropertyRowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(User.class); + + private final JdbcTemplate jdbcTemplate; + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + private final SimpleJdbcInsert insertUser; + + @Autowired + public JdbcUserRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + this.insertUser = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("users") + .usingGeneratedKeyColumns("id"); + + this.jdbcTemplate = jdbcTemplate; + this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; + } + + @Override + public User save(User user) { + MapSqlParameterSource map = new MapSqlParameterSource() + .addValue("id", user.getId()) + .addValue("name", user.getName()) + .addValue("email", user.getEmail()) + .addValue("password", user.getPassword()) + .addValue("registered", user.getRegistered()) + .addValue("enabled", user.isEnabled()) + .addValue("caloriesPerDay", user.getCaloriesPerDay()); + + if (user.isNew()) { + Number newKey = insertUser.executeAndReturnKey(map); + user.setId(newKey.intValue()); + } else if (namedParameterJdbcTemplate.update( + "UPDATE users SET name=:name, email=:email, password=:password, " + + "registered=:registered, enabled=:enabled, calories_per_day=:caloriesPerDay WHERE id=:id", map) == 0) { + return null; + } + return user; + } + + @Override + public boolean delete(int id) { + return jdbcTemplate.update("DELETE FROM users WHERE id=?", id) != 0; + } + + @Override + public User get(int id) { + List users = jdbcTemplate.query("SELECT * FROM users WHERE id=?", ROW_MAPPER, id); + return DataAccessUtils.singleResult(users); + } + + @Override + public User getByEmail(String email) { +// return jdbcTemplate.queryForObject("SELECT * FROM users WHERE email=?", ROW_MAPPER, email); + List users = jdbcTemplate.query("SELECT * FROM users WHERE email=?", ROW_MAPPER, email); + return DataAccessUtils.singleResult(users); + } + + @Override + public List getAll() { + return jdbcTemplate.query("SELECT * FROM users ORDER BY name, email", ROW_MAPPER); + } +} diff --git a/src/main/resources/db/initDB.sql b/src/main/resources/db/initDB.sql new file mode 100644 index 000000000000..c6be0dba07b1 --- /dev/null +++ b/src/main/resources/db/initDB.sql @@ -0,0 +1,25 @@ +DROP TABLE IF EXISTS user_role; +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 +); \ 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 000000000000..2de046f63e70 --- /dev/null +++ b/src/main/resources/db/populateDB.sql @@ -0,0 +1,12 @@ +DELETE FROM user_role; +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); diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties index 9cade556463e..52b4382ca444 100644 --- a/src/main/resources/db/postgres.properties +++ b/src/main/resources/db/postgres.properties @@ -1,3 +1,3 @@ -database.url=jdbc:postgresql://localhost:5432/topjava +database.url=jdbc:postgresql://localhost:5434/topjava database.username=user database.password=password diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml new file mode 100644 index 000000000000..8ec56c9e63f0 --- /dev/null +++ b/src/main/resources/spring/spring-db.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 9b1c8c1037ab0232372d6a322538043265eb9c14 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 18 Feb 2023 18:43:40 +0700 Subject: [PATCH 024/125] apply patch 3_13 --- pom.xml | 7 ++ .../topjava/model/AbstractBaseEntity.java | 22 +++++ .../topjava/model/AbstractNamedEntity.java | 3 + .../ru/javawebinar/topjava/model/User.java | 16 +++- .../repository/jdbc/JdbcMealRepository.java | 37 ++++++++ src/main/resources/spring/spring-app.xml | 2 +- .../ru/javawebinar/topjava/UserTestData.java | 40 ++++++++- .../inmemory/InMemoryBaseRepository.java | 4 +- .../topjava/service/UserServiceTest.java | 88 +++++++++++++++++++ ...InMemoryAdminRestControllerSpringTest.java | 2 + .../user/InMemoryAdminRestControllerTest.java | 1 + 11 files changed, 213 insertions(+), 9 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java create mode 100644 src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java diff --git a/pom.xml b/pom.xml index bcad3acfb324..b13cfe33b821 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,7 @@ 42.5.3 4.13.2 + 3.24.2 @@ -123,6 +124,12 @@ ${spring.version} test + + org.assertj + assertj-core + ${assertj.version} + test + diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java index 8f27c902e047..bbda59736dae 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -1,8 +1,13 @@ package ru.javawebinar.topjava.model; public abstract class AbstractBaseEntity { + public static final int START_SEQ = 100000; + protected Integer id; + protected AbstractBaseEntity() { + } + protected AbstractBaseEntity(Integer id) { this.id = id; } @@ -23,4 +28,21 @@ public boolean isNew() { public String toString() { return getClass().getSimpleName() + ":" + id; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AbstractBaseEntity that = (AbstractBaseEntity) o; + return id != null && id.equals(that.id); + } + + @Override + public int hashCode() { + return id == null ? 0 : id; + } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java index 2054a3d3cf9b..b87e9e18bda4 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java @@ -4,6 +4,9 @@ public abstract class AbstractNamedEntity extends AbstractBaseEntity { protected String name; + protected AbstractNamedEntity() { + } + protected AbstractNamedEntity(Integer id, String name) { super(id); this.name = name; diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index 0ec0257d923d..8ff7317adcc6 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -12,24 +12,32 @@ public class User extends AbstractNamedEntity { private String password; - private boolean enabled; + private boolean enabled = true; private Date registered = new Date(); private Set roles; - private int caloriesPerDay; + private int caloriesPerDay = DEFAULT_CALORIES_PER_DAY; + + public User() { + } + + public User(User u) { + this(u.id, u.name, u.email, u.password, u.caloriesPerDay, u.enabled, u.registered, u.roles); + } public User(Integer id, String name, String email, String password, Role... roles) { - this(id, name, email, password, DEFAULT_CALORIES_PER_DAY, true, Arrays.asList(roles)); + this(id, name, email, password, DEFAULT_CALORIES_PER_DAY, true, new Date(), Arrays.asList(roles)); } - public User(Integer id, String name, String email, String password, int caloriesPerDay, boolean enabled, Collection roles) { + public User(Integer id, String name, String email, String password, int caloriesPerDay, boolean enabled, Date registered, Collection roles) { super(id, name); this.email = email; this.password = password; this.caloriesPerDay = caloriesPerDay; this.enabled = enabled; + this.registered = registered; setRoles(roles); } diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java new file mode 100644 index 000000000000..c29288c39b39 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java @@ -0,0 +1,37 @@ +package ru.javawebinar.topjava.repository.jdbc; + +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.MealRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public class JdbcMealRepository implements MealRepository { + + @Override + public Meal save(Meal meal, int userId) { + return null; + } + + @Override + public boolean delete(int id, int userId) { + return false; + } + + @Override + public Meal get(int id, int userId) { + return null; + } + + @Override + public List getAll(int userId) { + return null; + } + + @Override + public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { + return null; + } +} diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml index cac42ba13e3d..4c05aea4978b 100644 --- a/src/main/resources/spring/spring-app.xml +++ b/src/main/resources/spring/spring-app.xml @@ -9,7 +9,7 @@ --> - + diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java index dcc5b3474fda..125f43333180 100644 --- a/src/test/java/ru/javawebinar/topjava/UserTestData.java +++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java @@ -3,13 +3,47 @@ import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; +import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; + public class UserTestData { - public static final int USER_ID = 1; - public static final int ADMIN_ID = 2; - public static final int GUEST_ID = 3; + 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; + } + + public static void assertMatch(User actual, User expected) { + assertThat(actual).usingRecursiveComparison().ignoringFields("registered", "roles").isEqualTo(expected); + } + + public static void assertMatch(Iterable actual, User... expected) { + assertMatch(actual, Arrays.asList(expected)); + } + + public static void assertMatch(Iterable actual, Iterable expected) { + assertThat(actual).usingRecursiveFieldByFieldElementComparatorIgnoringFields("registered", "roles").isEqualTo(expected); + } } diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java index 15d5528dcfc7..7d9f43b9862f 100644 --- a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java @@ -7,9 +7,11 @@ 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(0); + static final AtomicInteger counter = new AtomicInteger(START_SEQ); final Map map = new ConcurrentHashMap<>(); 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 000000000000..c74607024dd5 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java @@ -0,0 +1,88 @@ +package ru.javawebinar.topjava.service; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +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.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")) +public class UserServiceTest { + + @Autowired + private UserService service; + + @Test + public void create() { + User created = service.create(getNew()); + Integer newId = created.getId(); + User newUser = getNew(); + newUser.setId(newId); + assertMatch(created, newUser); + 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); + 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"); + assertMatch(user, admin); + } + + @Test + public void update() { + User updated = getUpdated(); + service.update(updated); + assertMatch(service.get(USER_ID), getUpdated()); + } + + @Test + public void getAll() { + List all = service.getAll(); + 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 index afdffdfafaa1..bdd730c22049 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java @@ -2,6 +2,7 @@ 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; @@ -15,6 +16,7 @@ @ContextConfiguration("classpath:spring/spring-app.xml") @RunWith(SpringRunner.class) +@Ignore public class InMemoryAdminRestControllerSpringTest { @Autowired diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java index 33c83774d9b3..9ba51812d871 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java @@ -13,6 +13,7 @@ import static ru.javawebinar.topjava.UserTestData.NOT_FOUND; import static ru.javawebinar.topjava.UserTestData.USER_ID; +@Ignore public class InMemoryAdminRestControllerTest { private static final Logger log = LoggerFactory.getLogger(InMemoryAdminRestControllerTest.class); From 4f880bd759a36b4f89cae613d63d3dc3fad970e6 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 18 Feb 2023 18:50:20 +0700 Subject: [PATCH 025/125] apply patch 3_14 --- pom.xml | 7 ++++++ src/main/resources/logback.xml | 4 ++-- .../topjava/service/UserServiceTest.java | 7 ++++++ src/test/resources/logback-test.xml | 22 +++++++++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 src/test/resources/logback-test.xml diff --git a/pom.xml b/pom.xml index b13cfe33b821..31999c9fc75c 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,13 @@ ${slf4j.version} + + org.slf4j + jul-to-slf4j + ${slf4j.version} + runtime + + ch.qos.logback logback-classic diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index bdf004d037fb..b73b151765a9 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -9,14 +9,14 @@ UTF-8 - %date %-5level %logger{0} [%file:%line] %msg%n + %date %-5level %logger{50}.%M:%L - %msg%n UTF-8 - %-5level %logger{0} [%file:%line] %msg%n + %d{HH:mm:ss.SSS} %-5level %class{50}.%M:%L - %msg%n diff --git a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java index c74607024dd5..d10a5ff59a27 100644 --- a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java @@ -2,6 +2,7 @@ import org.junit.Test; import org.junit.runner.RunWith; +import org.slf4j.bridge.SLF4JBridgeHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.test.context.ContextConfiguration; @@ -26,6 +27,12 @@ @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) public class UserServiceTest { + static { + // Only for postgres driver logging + // It uses java.util.logging and logged via jul-to-slf4j bridge + SLF4JBridgeHandler.install(); + } + @Autowired private UserService service; diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 000000000000..428ceca2ee02 --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,22 @@ + + + + true + + + + + UTF-8 + %d{HH:mm:ss.SSS} %-5level %class{50}.%M:%L - %msg%n + + + + + + + + + + + + \ No newline at end of file From cb913e1137c23048ca6b4052c471319dca1d914e Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 18 Feb 2023 18:50:38 +0700 Subject: [PATCH 026/125] apply patch 3_15 --- src/main/java/ru/javawebinar/topjava/web/MealServlet.java | 2 +- src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java | 4 +++- src/main/webapp/index.html | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java index 544b902480f9..bca1e4235d1f 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -27,7 +27,7 @@ public class MealServlet extends HttpServlet { @Override public void init() { - springContext = new ClassPathXmlApplicationContext("spring/spring-app.xml"); + springContext = new ClassPathXmlApplicationContext("spring/spring-app.xml", "spring/spring-db.xml"); mealController = springContext.getBean(MealRestController.class); } diff --git a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java index b9639bf1b9ea..588217547e60 100644 --- a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java +++ b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java @@ -1,10 +1,12 @@ package ru.javawebinar.topjava.web; +import ru.javawebinar.topjava.model.AbstractBaseEntity; + import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY; public class SecurityUtil { - private static int id = 1; + private static int id = AbstractBaseEntity.START_SEQ; public static int authUserId() { return id; diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index e887926bab6e..57d710a81abb 100644 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -9,8 +9,8 @@

    Проект Meals of  From 193e56d88c2d4b1f7e322ac4c1e27dd1fffbbc88 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 23 Feb 2023 11:55:43 +0700 Subject: [PATCH 027/125] Apply patch 4_1_HW3 --- .../ru/javawebinar/topjava/model/Meal.java | 21 ++++++- .../repository/jdbc/JdbcMealRepository.java | 59 +++++++++++++++++-- .../repository/jdbc/JdbcUserRepository.java | 15 ++--- src/main/resources/db/initDB.sql | 14 ++++- src/main/resources/db/populateDB.sql | 12 ++++ src/main/resources/db/postgres.properties | 2 +- 6 files changed, 102 insertions(+), 21 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index 9eed15f706be..1c1e67b15a3b 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -5,11 +5,14 @@ import java.time.LocalTime; public class Meal extends AbstractBaseEntity { - private final LocalDateTime dateTime; + private LocalDateTime dateTime; - private final String description; + private String description; - private final int calories; + private int calories; + + public Meal() { + } public Meal(LocalDateTime dateTime, String description, int calories) { this(null, dateTime, description, calories); @@ -42,6 +45,18 @@ public LocalTime getTime() { return dateTime.toLocalTime(); } + public void setDateTime(LocalDateTime dateTime) { + this.dateTime = dateTime; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setCalories(int calories) { + this.calories = calories; + } + @Override public String toString() { return "Meal{" + diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java index c29288c39b39..9c2a240bbbe5 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java @@ -1,5 +1,13 @@ package ru.javawebinar.topjava.repository.jdbc; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.stereotype.Repository; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; @@ -10,28 +18,69 @@ @Repository public class JdbcMealRepository implements MealRepository { + private static final RowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(Meal.class); + + private final JdbcTemplate jdbcTemplate; + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + private final SimpleJdbcInsert insertMeal; + + @Autowired + public JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + this.insertMeal = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("meal") + .usingGeneratedKeyColumns("id"); + + this.jdbcTemplate = jdbcTemplate; + this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; + } + @Override public Meal save(Meal meal, int userId) { - return null; + MapSqlParameterSource map = new MapSqlParameterSource() + .addValue("id", meal.getId()) + .addValue("description", meal.getDescription()) + .addValue("calories", meal.getCalories()) + .addValue("date_time", meal.getDateTime()) + .addValue("user_id", userId); + + if (meal.isNew()) { + Number newId = insertMeal.executeAndReturnKey(map); + meal.setId(newId.intValue()); + } else { + if (namedParameterJdbcTemplate.update("" + + "UPDATE meal " + + " SET description=:description, calories=:calories, date_time=:date_time " + + " WHERE id=:id AND user_id=:user_id", map) == 0) { + return null; + } + } + return meal; } @Override public boolean delete(int id, int userId) { - return false; + return jdbcTemplate.update("DELETE FROM meal WHERE id=? AND user_id=?", id, userId) != 0; } @Override public Meal get(int id, int userId) { - return null; + List meals = jdbcTemplate.query( + "SELECT * FROM meal WHERE id = ? AND user_id = ?", ROW_MAPPER, id, userId); + return DataAccessUtils.singleResult(meals); } @Override public List getAll(int userId) { - return null; + return jdbcTemplate.query( + "SELECT * FROM meal WHERE user_id=? ORDER BY date_time DESC", ROW_MAPPER, userId); } @Override public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { - return null; + return jdbcTemplate.query( + "SELECT * FROM meal WHERE user_id=? AND date_time >= ? AND date_time < ? ORDER BY date_time DESC", + ROW_MAPPER, userId, startDateTime, endDateTime); } } diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java index 7f6f8626a869..412bfbebc5df 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java @@ -4,7 +4,7 @@ import org.springframework.dao.support.DataAccessUtils; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.stereotype.Repository; @@ -36,21 +36,14 @@ public JdbcUserRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate @Override public User save(User user) { - MapSqlParameterSource map = new MapSqlParameterSource() - .addValue("id", user.getId()) - .addValue("name", user.getName()) - .addValue("email", user.getEmail()) - .addValue("password", user.getPassword()) - .addValue("registered", user.getRegistered()) - .addValue("enabled", user.isEnabled()) - .addValue("caloriesPerDay", user.getCaloriesPerDay()); + BeanPropertySqlParameterSource parameterSource = new BeanPropertySqlParameterSource(user); if (user.isNew()) { - Number newKey = insertUser.executeAndReturnKey(map); + Number newKey = insertUser.executeAndReturnKey(parameterSource); user.setId(newKey.intValue()); } else if (namedParameterJdbcTemplate.update( "UPDATE users SET name=:name, email=:email, password=:password, " + - "registered=:registered, enabled=:enabled, calories_per_day=:caloriesPerDay WHERE id=:id", map) == 0) { + "registered=:registered, enabled=:enabled, calories_per_day=:caloriesPerDay WHERE id=:id", parameterSource) == 0) { return null; } return user; diff --git a/src/main/resources/db/initDB.sql b/src/main/resources/db/initDB.sql index c6be0dba07b1..4bf3d8446ac5 100644 --- a/src/main/resources/db/initDB.sql +++ b/src/main/resources/db/initDB.sql @@ -1,4 +1,5 @@ DROP TABLE IF EXISTS user_role; +DROP TABLE IF EXISTS meal; DROP TABLE IF EXISTS users; DROP SEQUENCE IF EXISTS global_seq; @@ -22,4 +23,15 @@ CREATE TABLE user_role role VARCHAR NOT NULL, CONSTRAINT user_roles_idx UNIQUE (user_id, role), FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE -); \ No newline at end of file +); + +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/populateDB.sql b/src/main/resources/db/populateDB.sql index 2de046f63e70..5021dc6f13b8 100644 --- a/src/main/resources/db/populateDB.sql +++ b/src/main/resources/db/populateDB.sql @@ -1,4 +1,5 @@ DELETE FROM user_role; +DELETE FROM meal; DELETE FROM users; ALTER SEQUENCE global_seq RESTART WITH 100000; @@ -10,3 +11,14 @@ VALUES ('User', 'user@yandex.ru', 'password'), 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 index 52b4382ca444..9cade556463e 100644 --- a/src/main/resources/db/postgres.properties +++ b/src/main/resources/db/postgres.properties @@ -1,3 +1,3 @@ -database.url=jdbc:postgresql://localhost:5434/topjava +database.url=jdbc:postgresql://localhost:5432/topjava database.username=user database.password=password From 207aad68594a9fec4e1d38fe09ff01cee67cbcc6 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 26 Feb 2023 19:55:46 +0700 Subject: [PATCH 028/125] apply patch 4_2_HW3_optional --- .../javawebinar/topjava/util/MealsUtil.java | 13 -- .../ru/javawebinar/topjava/MealTestData.java | 50 ++++++++ .../inmemory/InMemoryMealRepository.java | 13 +- .../topjava/service/MealServiceTest.java | 116 ++++++++++++++++++ 4 files changed, 171 insertions(+), 21 deletions(-) create mode 100644 src/test/java/ru/javawebinar/topjava/MealTestData.java create mode 100644 src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index d10e1e674d47..ab552409679c 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -4,10 +4,7 @@ import ru.javawebinar.topjava.to.MealTo; import java.time.LocalDate; -import java.time.LocalDateTime; import java.time.LocalTime; -import java.time.Month; -import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; @@ -17,16 +14,6 @@ public class MealsUtil { public static final int DEFAULT_CALORIES_PER_DAY = 2000; - public static final List meals = Arrays.asList( - new Meal(LocalDateTime.of(2020, Month.JANUARY, 30, 10, 0), "Завтрак", 500), - new Meal(LocalDateTime.of(2020, Month.JANUARY, 30, 13, 0), "Обед", 1000), - new Meal(LocalDateTime.of(2020, Month.JANUARY, 30, 20, 0), "Ужин", 500), - new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 0, 0), "Еда на граничное значение", 100), - new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 10, 0), "Завтрак", 1000), - new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 13, 0), "Обед", 500), - new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 20, 0), "Ужин", 410) - ); - public static List getTos(Collection meals, int caloriesPerDay) { return filterByPredicate(meals, caloriesPerDay, meal -> true); } 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 000000000000..c3a851ee235e --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java @@ -0,0 +1,50 @@ +package ru.javawebinar.topjava; + +import ru.javawebinar.topjava.model.Meal; + +import java.time.Month; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.List; + +import static java.time.LocalDateTime.of; +import static org.assertj.core.api.Assertions.assertThat; +import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; + +public class MealTestData { + 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 = Arrays.asList(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); + } + + public static void assertMatch(Meal actual, Meal expected) { + assertThat(actual).usingRecursiveComparison().isEqualTo(expected); + } + + public static void assertMatch(Iterable actual, Meal... expected) { + assertMatch(actual, Arrays.asList(expected)); + } + + public static void assertMatch(Iterable actual, Iterable expected) { + assertThat(actual).usingRecursiveFieldByFieldElementComparator().isEqualTo(expected); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java index d8a0fada56c7..cdb98a0f79c5 100644 --- a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -3,15 +3,15 @@ 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.MealsUtil; import ru.javawebinar.topjava.util.Util; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.time.LocalDateTime; -import java.time.Month; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -20,9 +20,6 @@ import java.util.function.Predicate; import java.util.stream.Collectors; -import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; -import static ru.javawebinar.topjava.UserTestData.USER_ID; - @Repository public class InMemoryMealRepository implements MealRepository { private static final Logger log = LoggerFactory.getLogger(InMemoryMealRepository.class); @@ -31,9 +28,9 @@ public class InMemoryMealRepository implements MealRepository { private final Map> usersMealsMap = new ConcurrentHashMap<>(); { - MealsUtil.meals.forEach(meal -> save(meal, USER_ID)); - save(new Meal(LocalDateTime.of(2015, Month.JUNE, 1, 14, 0), "Админ ланч", 510), ADMIN_ID); - save(new Meal(LocalDateTime.of(2015, Month.JUNE, 1, 21, 0), "Админ ужин", 1500), ADMIN_ID); + InMemoryBaseRepository userMeals = new InMemoryBaseRepository<>(); + MealTestData.meals.forEach(userMeals::put); + usersMealsMap.put(UserTestData.USER_ID, userMeals); } 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 000000000000..b75f916fc336 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java @@ -0,0 +1,116 @@ +package ru.javawebinar.topjava.service; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.bridge.SLF4JBridgeHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +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.model.Meal; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import java.time.LocalDate; +import java.time.Month; + +import static org.junit.Assert.assertThrows; +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")) +public class MealServiceTest { + + static { + SLF4JBridgeHandler.install(); + } + + @Autowired + private MealService service; + + @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.getId(); + Meal newMeal = getNew(); + newMeal.setId(newId); + assertMatch(created, newMeal); + 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); + 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); + assertMatch(service.get(MEAL1_ID, USER_ID), getUpdated()); + } + + @Test + public void updateNotOwn() { + assertThrows(NotFoundException.class, () -> service.update(meal1, ADMIN_ID)); + assertMatch(service.get(MEAL1_ID, USER_ID), meal1); + } + + @Test + public void getAll() { + assertMatch(service.getAll(USER_ID), meals); + } + + @Test + public void getBetweenInclusive() { + assertMatch(service.getBetweenInclusive( + LocalDate.of(2020, Month.JANUARY, 30), + LocalDate.of(2020, Month.JANUARY, 30), USER_ID), + meal3, meal2, meal1); + } + + @Test + public void getBetweenWithNullDates() { + assertMatch(service.getBetweenInclusive(null, null, USER_ID), meals); + } +} \ No newline at end of file From fbe13c33c8ab2fbaca2ff4e89b637fbd2760682d Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 26 Feb 2023 19:56:14 +0700 Subject: [PATCH 029/125] apply patch 4_3 --- .../javawebinar/topjava/MatcherFactory.java | 37 +++++++++++++++++++ .../ru/javawebinar/topjava/MealTestData.java | 15 +------- .../ru/javawebinar/topjava/UserTestData.java | 16 +------- .../topjava/service/MealServiceTest.java | 20 +++++----- .../topjava/service/UserServiceTest.java | 12 +++--- 5 files changed, 57 insertions(+), 43 deletions(-) create mode 100644 src/test/java/ru/javawebinar/topjava/MatcherFactory.java 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 000000000000..200e27e5f231 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/MatcherFactory.java @@ -0,0 +1,37 @@ +package ru.javawebinar.topjava; + +import java.util.Arrays; + +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, Arrays.asList(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 index c3a851ee235e..5190f5082325 100644 --- a/src/test/java/ru/javawebinar/topjava/MealTestData.java +++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java @@ -8,10 +8,11 @@ import java.util.List; import static java.time.LocalDateTime.of; -import static org.assertj.core.api.Assertions.assertThat; import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; public class MealTestData { + public static final MatcherFactory.Matcher MEAL_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(); + 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; @@ -35,16 +36,4 @@ public static Meal getNew() { public static Meal getUpdated() { return new Meal(MEAL1_ID, meal1.getDateTime().plus(2, ChronoUnit.MINUTES), "Обновленный завтрак", 200); } - - public static void assertMatch(Meal actual, Meal expected) { - assertThat(actual).usingRecursiveComparison().isEqualTo(expected); - } - - public static void assertMatch(Iterable actual, Meal... expected) { - assertMatch(actual, Arrays.asList(expected)); - } - - public static void assertMatch(Iterable actual, Iterable expected) { - assertThat(actual).usingRecursiveFieldByFieldElementComparator().isEqualTo(expected); - } } diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java index 125f43333180..bfaba979da6c 100644 --- a/src/test/java/ru/javawebinar/topjava/UserTestData.java +++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java @@ -3,14 +3,14 @@ import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; -import java.util.Arrays; import java.util.Collections; import java.util.Date; -import static org.assertj.core.api.Assertions.assertThat; 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; @@ -34,16 +34,4 @@ public static User getUpdated() { updated.setRoles(Collections.singletonList(Role.ADMIN)); return updated; } - - public static void assertMatch(User actual, User expected) { - assertThat(actual).usingRecursiveComparison().ignoringFields("registered", "roles").isEqualTo(expected); - } - - public static void assertMatch(Iterable actual, User... expected) { - assertMatch(actual, Arrays.asList(expected)); - } - - public static void assertMatch(Iterable actual, Iterable expected) { - assertThat(actual).usingRecursiveFieldByFieldElementComparatorIgnoringFields("registered", "roles").isEqualTo(expected); - } } diff --git a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java index b75f916fc336..652aa8f1182c 100644 --- a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java @@ -57,8 +57,8 @@ public void create() { int newId = created.getId(); Meal newMeal = getNew(); newMeal.setId(newId); - assertMatch(created, newMeal); - assertMatch(service.get(newId, USER_ID), newMeal); + MEAL_MATCHER.assertMatch(created, newMeal); + MEAL_MATCHER.assertMatch(service.get(newId, USER_ID), newMeal); } @Test @@ -70,7 +70,7 @@ public void duplicateDateTimeCreate() { @Test public void get() { Meal actual = service.get(ADMIN_MEAL_ID, ADMIN_ID); - assertMatch(actual, adminMeal1); + MEAL_MATCHER.assertMatch(actual, adminMeal1); } @Test @@ -87,30 +87,30 @@ public void getNotOwn() { public void update() { Meal updated = getUpdated(); service.update(updated, USER_ID); - assertMatch(service.get(MEAL1_ID, USER_ID), getUpdated()); + MEAL_MATCHER.assertMatch(service.get(MEAL1_ID, USER_ID), getUpdated()); } @Test public void updateNotOwn() { assertThrows(NotFoundException.class, () -> service.update(meal1, ADMIN_ID)); - assertMatch(service.get(MEAL1_ID, USER_ID), meal1); + MEAL_MATCHER.assertMatch(service.get(MEAL1_ID, USER_ID), meal1); } @Test public void getAll() { - assertMatch(service.getAll(USER_ID), meals); + MEAL_MATCHER.assertMatch(service.getAll(USER_ID), meals); } @Test public void getBetweenInclusive() { - assertMatch(service.getBetweenInclusive( - LocalDate.of(2020, Month.JANUARY, 30), - LocalDate.of(2020, Month.JANUARY, 30), USER_ID), + 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() { - assertMatch(service.getBetweenInclusive(null, null, USER_ID), meals); + 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 index d10a5ff59a27..b3991d8e71f8 100644 --- a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java @@ -42,8 +42,8 @@ public void create() { Integer newId = created.getId(); User newUser = getNew(); newUser.setId(newId); - assertMatch(created, newUser); - assertMatch(service.get(newId), newUser); + USER_MATCHER.assertMatch(created, newUser); + USER_MATCHER.assertMatch(service.get(newId), newUser); } @Test @@ -66,7 +66,7 @@ public void deletedNotFound() { @Test public void get() { User user = service.get(USER_ID); - assertMatch(user, UserTestData.user); + USER_MATCHER.assertMatch(user, UserTestData.user); } @Test @@ -77,19 +77,19 @@ public void getNotFound() { @Test public void getByEmail() { User user = service.getByEmail("admin@gmail.com"); - assertMatch(user, admin); + USER_MATCHER.assertMatch(user, admin); } @Test public void update() { User updated = getUpdated(); service.update(updated); - assertMatch(service.get(USER_ID), getUpdated()); + USER_MATCHER.assertMatch(service.get(USER_ID), getUpdated()); } @Test public void getAll() { List all = service.getAll(); - assertMatch(all, admin, guest, user); + USER_MATCHER.assertMatch(all, admin, guest, user); } } \ No newline at end of file From af3847bdfd92d77207ed3a51b79d30cc39453d08 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 26 Feb 2023 19:57:07 +0700 Subject: [PATCH 030/125] apply patch 4_4 --- src/main/resources/spring/spring-db.xml | 9 +++++++++ .../ru/javawebinar/topjava/service/MealServiceTest.java | 5 ----- .../ru/javawebinar/topjava/service/UserServiceTest.java | 7 ------- src/test/resources/logback-test.xml | 2 +- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 8ec56c9e63f0..283fee39434a 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -6,6 +6,15 @@ + + + + + diff --git a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java index 652aa8f1182c..e9a102c3f328 100644 --- a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java @@ -2,7 +2,6 @@ import org.junit.Test; import org.junit.runner.RunWith; -import org.slf4j.bridge.SLF4JBridgeHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.test.context.ContextConfiguration; @@ -28,10 +27,6 @@ @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) public class MealServiceTest { - static { - SLF4JBridgeHandler.install(); - } - @Autowired private MealService service; diff --git a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java index b3991d8e71f8..f2542bc9e133 100644 --- a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java @@ -2,7 +2,6 @@ import org.junit.Test; import org.junit.runner.RunWith; -import org.slf4j.bridge.SLF4JBridgeHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.test.context.ContextConfiguration; @@ -27,12 +26,6 @@ @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) public class UserServiceTest { - static { - // Only for postgres driver logging - // It uses java.util.logging and logged via jul-to-slf4j bridge - SLF4JBridgeHandler.install(); - } - @Autowired private UserService service; diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index 428ceca2ee02..da4eed7b9468 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -12,7 +12,7 @@ - + From 3a6d07760ec24211e35226629cb467fdb258120c Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 26 Feb 2023 20:36:34 +0700 Subject: [PATCH 031/125] apply patch 4_5 --- .codacy.yml | 5 +++++ README.md | 2 ++ .../ru/javawebinar/topjava/model/AbstractBaseEntity.java | 7 +++++++ .../java/ru/javawebinar/topjava/service/MealService.java | 5 ++++- .../java/ru/javawebinar/topjava/service/UserService.java | 6 +++++- .../java/ru/javawebinar/topjava/util/DateTimeUtil.java | 3 +++ src/main/java/ru/javawebinar/topjava/util/MealsUtil.java | 3 +++ src/main/java/ru/javawebinar/topjava/util/Util.java | 4 ++++ .../java/ru/javawebinar/topjava/util/ValidationUtil.java | 5 ++++- src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java | 3 +++ .../repository/inmemory/InMemoryBaseRepository.java | 5 ++++- .../repository/inmemory/InMemoryMealRepository.java | 6 ++---- .../repository/inmemory/InMemoryUserRepository.java | 2 ++ .../ru/javawebinar/topjava/service/MealServiceTest.java | 2 +- .../ru/javawebinar/topjava/service/UserServiceTest.java | 2 +- 15 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 .codacy.yml diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 000000000000..311a8f4e0029 --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,5 @@ +--- +exclude_paths: + - 'src/main/webapp/**' + - '**.md' + - '**.sql' \ No newline at end of file diff --git a/README.md b/README.md index ae8379af196e..56f7aefe973b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/bee16f3145654047a0505c62aeefd8a2)](https://www.codacy.com/gh/warorc/topjava/dashboard) + Java Enterprise Online Project =============================== Разработка полнофункционального Spring/JPA Enterprise приложения c авторизацией и правами доступа на основе ролей с использованием наиболее популярных инструментов и технологий Java: Maven, Spring MVC, Security, JPA(Hibernate), REST(Jackson), Bootstrap (css,js), DataTables, jQuery + plugins, Java 8 Stream and Time API и хранением в базах данных Postgresql и HSQLDB. diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java index bbda59736dae..2b5aacfb754f 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -1,5 +1,7 @@ package ru.javawebinar.topjava.model; +import org.springframework.util.Assert; + public abstract class AbstractBaseEntity { public static final int START_SEQ = 100000; @@ -20,6 +22,11 @@ public Integer getId() { return id; } + public int id() { + Assert.notNull(id, "Entity must have id"); + return id; + } + public boolean isNew() { return this.id == null; } diff --git a/src/main/java/ru/javawebinar/topjava/service/MealService.java b/src/main/java/ru/javawebinar/topjava/service/MealService.java index 7957a0895d47..df874378eb82 100644 --- a/src/main/java/ru/javawebinar/topjava/service/MealService.java +++ b/src/main/java/ru/javawebinar/topjava/service/MealService.java @@ -2,6 +2,7 @@ import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; +import org.springframework.util.Assert; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; @@ -38,10 +39,12 @@ public List getAll(int userId) { } public void update(Meal meal, int userId) { - checkNotFoundWithId(repository.save(meal, userId), meal.getId()); + Assert.notNull(meal, "meal must not be null"); + checkNotFoundWithId(repository.save(meal, userId), meal.id()); } public Meal create(Meal meal, int userId) { + Assert.notNull(meal, "meal must not be null"); return repository.save(meal, userId); } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/service/UserService.java b/src/main/java/ru/javawebinar/topjava/service/UserService.java index 8fbe8dc0601f..09ccee68c43f 100644 --- a/src/main/java/ru/javawebinar/topjava/service/UserService.java +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -1,6 +1,7 @@ package ru.javawebinar.topjava.service; import org.springframework.stereotype.Service; +import org.springframework.util.Assert; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.UserRepository; @@ -19,6 +20,7 @@ public UserService(UserRepository repository) { } public User create(User user) { + Assert.notNull(user, "user must not be null"); return repository.save(user); } @@ -31,6 +33,7 @@ public User get(int id) { } public User getByEmail(String email) { + Assert.notNull(email, "email must not be null"); return checkNotFound(repository.getByEmail(email), "email=" + email); } @@ -39,6 +42,7 @@ public List getAll() { } public void update(User user) { - checkNotFoundWithId(repository.save(user), user.getId()); + Assert.notNull(user, "user must not be null"); + checkNotFoundWithId(repository.save(user), user.id()); } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java index 91df7fd242b9..7c1ac9fecdba 100644 --- a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -15,6 +15,9 @@ public class DateTimeUtil { private static final LocalDateTime MIN_DATE = LocalDateTime.of(1, 1, 1, 0, 0); private static final LocalDateTime MAX_DATE = LocalDateTime.of(3000, 1, 1, 0, 0); + private DateTimeUtil() { + } + public static LocalDateTime atStartOfDayOrMin(LocalDate localDate) { return localDate != null ? localDate.atStartOfDay() : MIN_DATE; } diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index ab552409679c..d767652eb99e 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -14,6 +14,9 @@ public class MealsUtil { public static final int DEFAULT_CALORIES_PER_DAY = 2000; + private MealsUtil() { + } + public static List getTos(Collection meals, int caloriesPerDay) { return filterByPredicate(meals, caloriesPerDay, meal -> true); } diff --git a/src/main/java/ru/javawebinar/topjava/util/Util.java b/src/main/java/ru/javawebinar/topjava/util/Util.java index a17a6927f975..9a083383d04e 100644 --- a/src/main/java/ru/javawebinar/topjava/util/Util.java +++ b/src/main/java/ru/javawebinar/topjava/util/Util.java @@ -3,6 +3,10 @@ import org.springframework.lang.Nullable; public class Util { + + private Util() { + } + public static > boolean isBetweenHalfOpen(T value, @Nullable T start, @Nullable T end) { return (start == null || value.compareTo(start) >= 0) && (end == null || value.compareTo(end) < 0); } diff --git a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java index 971eb9c0c72b..5212eea75062 100644 --- a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java @@ -6,6 +6,9 @@ public class ValidationUtil { + private ValidationUtil() { + } + public static T checkNotFoundWithId(T object, int id) { checkNotFoundWithId(object != null, id); return object; @@ -36,7 +39,7 @@ public static void assureIdConsistent(AbstractBaseEntity entity, int id) { // conservative when you reply, but accept liberally (http://stackoverflow.com/a/32728226/548473) if (entity.isNew()) { entity.setId(id); - } else if (entity.getId() != id) { + } else if (entity.id() != id) { throw new IllegalArgumentException(entity + " must be with id=" + id); } } diff --git a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java index 588217547e60..4bad5863e3c6 100644 --- a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java +++ b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java @@ -8,6 +8,9 @@ public class SecurityUtil { private static int id = AbstractBaseEntity.START_SEQ; + private SecurityUtil() { + } + public static int authUserId() { return id; } diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java index 7d9f43b9862f..03770da2134b 100644 --- a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java @@ -4,6 +4,7 @@ import java.util.Collection; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; @@ -16,6 +17,7 @@ public class InMemoryBaseRepository { 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); @@ -37,6 +39,7 @@ Collection getCollection() { } void put(T entity) { - map.put(entity.getId(), 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 index cdb98a0f79c5..05527fca8747 100644 --- a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -12,10 +12,7 @@ import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.time.LocalDateTime; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -36,6 +33,7 @@ public class InMemoryMealRepository implements MealRepository { @Override public Meal save(Meal meal, int userId) { + Objects.requireNonNull(meal, "meal must not be null"); InMemoryBaseRepository meals = usersMealsMap.computeIfAbsent(userId, uId -> new InMemoryBaseRepository<>()); return meals.save(meal); } diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java index fa8fc861bbf6..7e2fadbf88bd 100644 --- a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java @@ -6,6 +6,7 @@ import java.util.Comparator; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import static ru.javawebinar.topjava.UserTestData.*; @@ -31,6 +32,7 @@ public List getAll() { @Override public User getByEmail(String email) { + Objects.requireNonNull(email, "email must not be null"); return getCollection().stream() .filter(u -> email.equals(u.getEmail())) .findFirst() diff --git a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java index e9a102c3f328..12a4ba26605e 100644 --- a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java @@ -49,7 +49,7 @@ public void deleteNotOwn() { @Test public void create() { Meal created = service.create(getNew(), USER_ID); - int newId = created.getId(); + int newId = created.id(); Meal newMeal = getNew(); newMeal.setId(newId); MEAL_MATCHER.assertMatch(created, newMeal); diff --git a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java index f2542bc9e133..b318695cc6ac 100644 --- a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java @@ -32,7 +32,7 @@ public class UserServiceTest { @Test public void create() { User created = service.create(getNew()); - Integer newId = created.getId(); + int newId = created.id(); User newUser = getNew(); newUser.setId(newId); USER_MATCHER.assertMatch(created, newUser); From f10ed5923315780d2a8c6fbdb3d99c6c8c21c4e9 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 26 Feb 2023 21:00:01 +0700 Subject: [PATCH 032/125] apply patch 4_6 --- src/main/resources/db/postgres.properties | 1 + src/main/resources/spring/spring-db.xml | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties index 9cade556463e..663c63f19d7b 100644 --- a/src/main/resources/db/postgres.properties +++ b/src/main/resources/db/postgres.properties @@ -1,3 +1,4 @@ database.url=jdbc:postgresql://localhost:5432/topjava database.username=user database.password=password +database.init=true diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 283fee39434a..72a3f6178c3e 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -1,8 +1,10 @@ + http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd + http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> @@ -15,6 +17,11 @@ + + + + + From 540cb500646499bf9dbbb8a483fedf847b20e7f5 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 26 Feb 2023 21:16:07 +0700 Subject: [PATCH 033/125] apply patch 4_7 --- src/main/resources/spring/spring-app.xml | 1 - src/main/resources/spring/spring-db.xml | 2 ++ src/test/java/ru/javawebinar/topjava/SpringMain.java | 2 +- .../web/user/InMemoryAdminRestControllerSpringTest.java | 3 +-- .../topjava/web/user/InMemoryAdminRestControllerTest.java | 3 +-- src/test/resources/spring/inmemory.xml | 7 +++++++ 6 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 src/test/resources/spring/inmemory.xml diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml index 4c05aea4978b..4c17228b7a87 100644 --- a/src/main/resources/spring/spring-app.xml +++ b/src/main/resources/spring/spring-app.xml @@ -9,7 +9,6 @@ --> - diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 72a3f6178c3e..8e33d32f5f49 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -17,6 +17,8 @@ + + diff --git a/src/test/java/ru/javawebinar/topjava/SpringMain.java b/src/test/java/ru/javawebinar/topjava/SpringMain.java index b869d1c6e382..e41f1ae119be 100644 --- a/src/test/java/ru/javawebinar/topjava/SpringMain.java +++ b/src/test/java/ru/javawebinar/topjava/SpringMain.java @@ -17,7 +17,7 @@ public class SpringMain { public static void main(String[] args) { // java 7 automatic resource management (ARM) - try (ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml")) { + 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)); diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java index bdd730c22049..2386eeee4ff3 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java @@ -14,9 +14,8 @@ import static ru.javawebinar.topjava.UserTestData.NOT_FOUND; import static ru.javawebinar.topjava.UserTestData.USER_ID; -@ContextConfiguration("classpath:spring/spring-app.xml") +@ContextConfiguration({"classpath:spring/spring-app.xml", "classpath:spring/inmemory.xml"}) @RunWith(SpringRunner.class) -@Ignore public class InMemoryAdminRestControllerSpringTest { @Autowired diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java index 9ba51812d871..7cc2a833c161 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java @@ -13,7 +13,6 @@ import static ru.javawebinar.topjava.UserTestData.NOT_FOUND; import static ru.javawebinar.topjava.UserTestData.USER_ID; -@Ignore public class InMemoryAdminRestControllerTest { private static final Logger log = LoggerFactory.getLogger(InMemoryAdminRestControllerTest.class); @@ -23,7 +22,7 @@ public class InMemoryAdminRestControllerTest { @BeforeClass public static void beforeClass() { - appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml"); + 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); diff --git a/src/test/resources/spring/inmemory.xml b/src/test/resources/spring/inmemory.xml new file mode 100644 index 000000000000..c6a2710cbd88 --- /dev/null +++ b/src/test/resources/spring/inmemory.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file From cf3eaadb2fe767bfa8326fbd364f3faf0e314d28 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 26 Feb 2023 23:00:26 +0700 Subject: [PATCH 034/125] apply patch 4_8 --- pom.xml | 27 +++++++- .../topjava/model/AbstractBaseEntity.java | 8 +++ .../topjava/model/AbstractNamedEntity.java | 10 +++ .../ru/javawebinar/topjava/model/Meal.java | 13 ++++ .../ru/javawebinar/topjava/model/User.java | 25 ++++++++ .../repository/jpa/JpaMealRepository.java | 37 +++++++++++ .../repository/jpa/JpaUserRepository.java | 61 +++++++++++++++++++ src/main/resources/db/postgres.properties | 4 ++ src/main/resources/spring/spring-db.xml | 36 +++++++++-- 9 files changed, 214 insertions(+), 7 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java create mode 100644 src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java diff --git a/pom.xml b/pom.xml index 31999c9fc75c..46084d98cb3a 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,11 @@ UTF-8 5.3.25 + + 5.6.15.Final + 6.2.5.Final + 3.0.1-b12 + 1.2.11 1.7.36 @@ -93,7 +98,7 @@ org.springframework - spring-jdbc + spring-orm ${spring.version} @@ -104,6 +109,26 @@ ${postgresql.version} + + + org.hibernate + hibernate-core + ${hibernate.version} + + + org.hibernate.validator + hibernate-validator + ${hibernate-validator.version} + + + + + org.glassfish + javax.el + ${javax-el.version} + provided + + javax.servlet diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java index 2b5aacfb754f..d9343a0740a8 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -2,9 +2,17 @@ import org.springframework.util.Assert; +import javax.persistence.*; + +@MappedSuperclass +// http://stackoverflow.com/questions/594597/hibernate-annotations-which-is-better-field-or-property-access +@Access(AccessType.FIELD) public abstract class AbstractBaseEntity { public static final int START_SEQ = 100000; + @Id + @SequenceGenerator(name = "global_seq", sequenceName = "global_seq", allocationSize = 1, initialValue = START_SEQ) + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq") protected Integer id; protected AbstractBaseEntity() { diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java index b87e9e18bda4..0b32aac584f5 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java @@ -1,7 +1,17 @@ package ru.javawebinar.topjava.model; +import javax.persistence.Column; +import javax.persistence.MappedSuperclass; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + + +@MappedSuperclass public abstract class AbstractNamedEntity extends AbstractBaseEntity { + @NotBlank + @Size(min = 2, max = 128) + @Column(name = "name", nullable = false) protected String name; protected AbstractNamedEntity() { diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index 1c1e67b15a3b..788ae8f6fe5b 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -1,5 +1,7 @@ package ru.javawebinar.topjava.model; +import javax.persistence.FetchType; +import javax.persistence.ManyToOne; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; @@ -11,6 +13,9 @@ public class Meal extends AbstractBaseEntity { private int calories; + @ManyToOne(fetch = FetchType.LAZY) + private User user; + public Meal() { } @@ -57,6 +62,14 @@ public void setCalories(int calories) { this.calories = calories; } + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + @Override public String toString() { return "Meal{" + diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index 8ff7317adcc6..72237f874fb4 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -1,23 +1,48 @@ package ru.javawebinar.topjava.model; +import org.hibernate.validator.constraints.Range; import org.springframework.util.CollectionUtils; +import javax.persistence.*; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; import java.util.*; import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY; +@Entity +@Table(name = "users") public class User extends AbstractNamedEntity { + @Column(name = "email", nullable = false, unique = true) + @Email + @NotBlank + @Size(max = 128) private String email; + @Column(name = "password", nullable = false) + @NotBlank + @Size(min = 5, max = 128) private String password; + @Column(name = "enabled", nullable = false, columnDefinition = "bool default true") private boolean enabled = true; + @Column(name = "registered", nullable = false, columnDefinition = "timestamp default now()", updatable = false) + @NotNull private Date registered = new Date(); + @Enumerated(EnumType.STRING) + @CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"), + uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "role"}, name = "uk_user_role")}) + @Column(name = "role") + @ElementCollection(fetch = FetchType.EAGER) private Set roles; + @Column(name = "calories_per_day", nullable = false, columnDefinition = "int default 2000") + @Range(min = 10, max = 10000) private int caloriesPerDay = DEFAULT_CALORIES_PER_DAY; public User() { diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java new file mode 100644 index 000000000000..9cc19a4dc31b --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java @@ -0,0 +1,37 @@ +package ru.javawebinar.topjava.repository.jpa; + +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.MealRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public class JpaMealRepository implements MealRepository { + + @Override + public Meal save(Meal meal, int userId) { + return null; + } + + @Override + public boolean delete(int id, int userId) { + return false; + } + + @Override + public Meal get(int id, int userId) { + return null; + } + + @Override + public List getAll(int userId) { + return null; + } + + @Override + public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java new file mode 100644 index 000000000000..68ae67d0152e --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java @@ -0,0 +1,61 @@ +package ru.javawebinar.topjava.repository.jpa; + +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; +import java.util.List; + +@Repository +public class JpaUserRepository implements UserRepository { + +/* + @Autowired + private SessionFactory sessionFactory; + + private Session openSession() { + return sessionFactory.getCurrentSession(); + } +*/ + + @PersistenceContext + private EntityManager em; + + @Override + public User save(User user) { + if (user.isNew()) { + em.persist(user); + return user; + } else { + return em.merge(user); + } + } + + @Override + public User get(int id) { + return em.find(User.class, id); + } + + @Override + public boolean delete(int id) { + +/* User ref = em.getReference(User.class, id); + em.remove(ref); +*/ + Query query = em.createQuery("DELETE FROM User u WHERE u.id=:id"); + return query.setParameter("id", id).executeUpdate() != 0; + } + + @Override + public User getByEmail(String email) { + return null; + } + + @Override + public List getAll() { + return null; + } +} diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties index 663c63f19d7b..0a62b790dad6 100644 --- a/src/main/resources/db/postgres.properties +++ b/src/main/resources/db/postgres.properties @@ -1,4 +1,8 @@ database.url=jdbc:postgresql://localhost:5432/topjava database.username=user database.password=password + database.init=true +jpa.showSql=true +hibernate.format_sql=true +hibernate.use_sql_comments=true \ No newline at end of file diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 8e33d32f5f49..959e712ad856 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -1,5 +1,6 @@ - + @@ -33,11 +34,34 @@ - - - + + + + + + + + + + - - + + + + + + \ No newline at end of file From 3aebdf9820a0d34197817e8ad8d146443f5fc057 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 26 Feb 2023 23:00:40 +0700 Subject: [PATCH 035/125] apply patch 4_9 --- .../ru/javawebinar/topjava/model/User.java | 9 +++++++++ .../repository/jpa/JpaUserRepository.java | 20 +++++++++++++++---- src/main/resources/spring/spring-db.xml | 10 +++++++++- .../topjava/service/MealServiceTest.java | 2 ++ 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index 72237f874fb4..b1892571f4b9 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -12,10 +12,19 @@ import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY; +@NamedQueries({ + @NamedQuery(name = User.DELETE, query = "DELETE FROM User u WHERE u.id=:id"), + @NamedQuery(name = User.BY_EMAIL, query = "SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.email=?1"), + @NamedQuery(name = User.ALL_SORTED, query = "SELECT u FROM User u LEFT JOIN FETCH u.roles ORDER BY u.name, u.email"), +}) @Entity @Table(name = "users") public class User extends AbstractNamedEntity { + public static final String DELETE = "User.delete"; + public static final String BY_EMAIL = "User.getByEmail"; + public static final String ALL_SORTED = "User.getAllSorted"; + @Column(name = "email", nullable = false, unique = true) @Email @NotBlank diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java index 68ae67d0152e..3a1bbddc4cf2 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java @@ -1,15 +1,17 @@ package ru.javawebinar.topjava.repository.jpa; +import org.springframework.dao.support.DataAccessUtils; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.UserRepository; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; -import javax.persistence.Query; import java.util.List; @Repository +@Transactional(readOnly = true) public class JpaUserRepository implements UserRepository { /* @@ -25,6 +27,7 @@ private Session openSession() { private EntityManager em; @Override + @Transactional public User save(User user) { if (user.isNew()) { em.persist(user); @@ -40,22 +43,31 @@ public User get(int id) { } @Override + @Transactional public boolean delete(int id) { /* User ref = em.getReference(User.class, id); em.remove(ref); -*/ + Query query = em.createQuery("DELETE FROM User u WHERE u.id=:id"); return query.setParameter("id", id).executeUpdate() != 0; +*/ + return em.createNamedQuery(User.DELETE) + .setParameter("id", id) + .executeUpdate() != 0; } @Override public User getByEmail(String email) { - return null; + List users = em.createNamedQuery(User.BY_EMAIL, User.class) + .setParameter(1, email) + .getResultList(); + return DataAccessUtils.singleResult(users); } @Override public List getAll() { - return null; + return em.createNamedQuery(User.ALL_SORTED, User.class) + .getResultList(); } } diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 959e712ad856..fc45d2a4a5e0 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -3,9 +3,11 @@ xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:jdbc="http://www.springframework.org/schema/jdbc" + xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd - http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> + http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd + http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> @@ -54,6 +56,12 @@ + + + + + diff --git a/src/main/resources/db/hsqldb.properties b/src/main/resources/db/hsqldb.properties new file mode 100644 index 000000000000..c7944e25c223 --- /dev/null +++ b/src/main/resources/db/hsqldb.properties @@ -0,0 +1,12 @@ +#database.url=jdbc:hsqldb:file:D:/temp/topjava + +database.url=jdbc:hsqldb:mem:topjava +database.username=sa +database.password= +database.driverClassName=org.hsqldb.jdbcDriver + +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_hsql.sql b/src/main/resources/db/initDB_hsql.sql new file mode 100644 index 000000000000..9e0e195e600b --- /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/postgres.properties b/src/main/resources/db/postgres.properties index 0a62b790dad6..fa02ee04911f 100644 --- a/src/main/resources/db/postgres.properties +++ b/src/main/resources/db/postgres.properties @@ -1,8 +1,10 @@ database.url=jdbc:postgresql://localhost:5432/topjava database.username=user database.password=password +database.driverClassName=org.postgresql.Driver 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/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index fc45d2a4a5e0..970261d06d54 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -9,7 +9,10 @@ http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> + + + - + From 5a00c14f044d039fa28e8d040bd412fb16b6a778 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 2 Mar 2023 12:20:28 +0700 Subject: [PATCH 037/125] apply 5_1 patch --- patch-20230302T033752Z-001.zip | Bin 0 -> 18040 bytes pom.xml | 2 +- .../ru/javawebinar/topjava/model/User.java | 2 +- .../repository/jdbc/JdbcUserRepository.java | 7 ++++--- .../javawebinar/topjava/util/MealsUtil.java | 2 +- .../javawebinar/topjava/web/MealServlet.java | 18 ++++++++---------- .../javawebinar/topjava/MatcherFactory.java | 4 ++-- .../ru/javawebinar/topjava/MealTestData.java | 3 +-- .../inmemory/InMemoryMealRepository.java | 13 ++++++------- .../inmemory/InMemoryUserRepository.java | 3 +-- 10 files changed, 25 insertions(+), 29 deletions(-) create mode 100644 patch-20230302T033752Z-001.zip diff --git a/patch-20230302T033752Z-001.zip b/patch-20230302T033752Z-001.zip new file mode 100644 index 0000000000000000000000000000000000000000..d9a1eda68bc41afc65cb7dd07257738ca4f631a4 GIT binary patch literal 18040 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%0ABHTGk)$6WH$(5_Rmy;*y8|d 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 J$}3JF9sths(0>2` literal 0 HcmV?d00001 diff --git a/pom.xml b/pom.xml index 4668c2ef371d..8f6705ce0176 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ http://topjava.herokuapp.com/ - 1.8 + 17 UTF-8 UTF-8 5.3.25 diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index b1892571f4b9..e6233f3ded37 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -62,7 +62,7 @@ public User(User u) { } public User(Integer id, String name, String email, String password, Role... roles) { - this(id, name, email, password, DEFAULT_CALORIES_PER_DAY, true, new Date(), Arrays.asList(roles)); + this(id, name, email, password, DEFAULT_CALORIES_PER_DAY, true, new Date(), List.of(roles)); } public User(Integer id, String name, String email, String password, int caloriesPerDay, boolean enabled, Date registered, Collection roles) { diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java index 412bfbebc5df..0d4baa50eef2 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java @@ -41,9 +41,10 @@ public User save(User user) { if (user.isNew()) { Number newKey = insertUser.executeAndReturnKey(parameterSource); user.setId(newKey.intValue()); - } else if (namedParameterJdbcTemplate.update( - "UPDATE users SET name=:name, email=:email, password=:password, " + - "registered=:registered, enabled=:enabled, calories_per_day=:caloriesPerDay WHERE id=:id", parameterSource) == 0) { + } else if (namedParameterJdbcTemplate.update(""" + UPDATE users SET name=:name, email=:email, password=:password, + registered=:registered, enabled=:enabled, calories_per_day=:caloriesPerDay WHERE id=:id + """, parameterSource) == 0) { return null; } return user; diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index d767652eb99e..658671b9df10 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -35,7 +35,7 @@ private static List filterByPredicate(Collection meals, int calori return meals.stream() .filter(filter) .map(meal -> createTo(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)) - .collect(Collectors.toList()); + .toList(); } private static MealTo createTo(Meal meal, boolean excess) { diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java index bca1e4235d1f..13efd88b1737 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -58,32 +58,30 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t String action = request.getParameter("action"); switch (action == null ? "all" : action) { - case "delete": + case "delete" -> { int id = getId(request); mealController.delete(id); response.sendRedirect("meals"); - break; - case "create": - case "update": + } + case "create", "update" -> { final Meal meal = "create".equals(action) ? new Meal(LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), "", 1000) : mealController.get(getId(request)); request.setAttribute("meal", meal); request.getRequestDispatcher("/mealForm.jsp").forward(request, response); - break; - case "filter": + } + case "filter" -> { LocalDate startDate = parseLocalDate(request.getParameter("startDate")); LocalDate endDate = parseLocalDate(request.getParameter("endDate")); LocalTime startTime = parseLocalTime(request.getParameter("startTime")); LocalTime endTime = parseLocalTime(request.getParameter("endTime")); request.setAttribute("meals", mealController.getBetween(startDate, startTime, endDate, endTime)); request.getRequestDispatcher("/meals.jsp").forward(request, response); - break; - case "all": - default: + } + default -> { request.setAttribute("meals", mealController.getAll()); request.getRequestDispatcher("/meals.jsp").forward(request, response); - break; + } } } diff --git a/src/test/java/ru/javawebinar/topjava/MatcherFactory.java b/src/test/java/ru/javawebinar/topjava/MatcherFactory.java index 200e27e5f231..c5f04086a356 100644 --- a/src/test/java/ru/javawebinar/topjava/MatcherFactory.java +++ b/src/test/java/ru/javawebinar/topjava/MatcherFactory.java @@ -1,6 +1,6 @@ package ru.javawebinar.topjava; -import java.util.Arrays; +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -27,7 +27,7 @@ public void assertMatch(T actual, T expected) { @SafeVarargs public final void assertMatch(Iterable actual, T... expected) { - assertMatch(actual, Arrays.asList(expected)); + assertMatch(actual, List.of(expected)); } public void assertMatch(Iterable actual, Iterable expected) { diff --git a/src/test/java/ru/javawebinar/topjava/MealTestData.java b/src/test/java/ru/javawebinar/topjava/MealTestData.java index 5190f5082325..03b594012e37 100644 --- a/src/test/java/ru/javawebinar/topjava/MealTestData.java +++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java @@ -4,7 +4,6 @@ import java.time.Month; import java.time.temporal.ChronoUnit; -import java.util.Arrays; import java.util.List; import static java.time.LocalDateTime.of; @@ -27,7 +26,7 @@ public class MealTestData { 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 = Arrays.asList(meal7, meal6, meal5, meal4, meal3, meal2, meal1); + 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); diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java index 05527fca8747..5c65ced864e4 100644 --- a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -15,7 +15,6 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; -import java.util.stream.Collectors; @Repository public class InMemoryMealRepository implements MealRepository { @@ -25,7 +24,7 @@ public class InMemoryMealRepository implements MealRepository { private final Map> usersMealsMap = new ConcurrentHashMap<>(); { - InMemoryBaseRepository userMeals = new InMemoryBaseRepository<>(); + var userMeals = new InMemoryBaseRepository(); MealTestData.meals.forEach(userMeals::put); usersMealsMap.put(UserTestData.USER_ID, userMeals); } @@ -34,7 +33,7 @@ public class InMemoryMealRepository implements MealRepository { @Override public Meal save(Meal meal, int userId) { Objects.requireNonNull(meal, "meal must not be null"); - InMemoryBaseRepository meals = usersMealsMap.computeIfAbsent(userId, uId -> new InMemoryBaseRepository<>()); + var meals = usersMealsMap.computeIfAbsent(userId, uId -> new InMemoryBaseRepository<>()); return meals.save(meal); } @@ -50,13 +49,13 @@ public void preDestroy() { @Override public boolean delete(int id, int userId) { - InMemoryBaseRepository meals = usersMealsMap.get(userId); + var meals = usersMealsMap.get(userId); return meals != null && meals.delete(id); } @Override public Meal get(int id, int userId) { - InMemoryBaseRepository meals = usersMealsMap.get(userId); + var meals = usersMealsMap.get(userId); return meals == null ? null : meals.get(id); } @@ -71,11 +70,11 @@ public List getAll(int userId) { } private List filterByPredicate(int userId, Predicate filter) { - InMemoryBaseRepository meals = usersMealsMap.get(userId); + var meals = usersMealsMap.get(userId); return meals == null ? Collections.emptyList() : meals.getCollection().stream() .filter(filter) .sorted(Comparator.comparing(Meal::getDateTime).reversed()) - .collect(Collectors.toList()); + .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 index 7e2fadbf88bd..f3585dfffd7f 100644 --- a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java @@ -7,7 +7,6 @@ import java.util.Comparator; import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; import static ru.javawebinar.topjava.UserTestData.*; @@ -27,7 +26,7 @@ public void init() { public List getAll() { return getCollection().stream() .sorted(Comparator.comparing(User::getName).thenComparing(User::getEmail)) - .collect(Collectors.toList()); + .toList(); } @Override From bbd0afb8a7195ce15a427dbd96601b7819447caf Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 2 Mar 2023 16:23:39 +0700 Subject: [PATCH 038/125] apply 5_2 patch --- .../ru/javawebinar/topjava/model/Meal.java | 33 +++++++++++++++-- .../repository/jpa/JpaMealRepository.java | 35 ++++++++++++++++--- .../ru/javawebinar/topjava/MealTestData.java | 2 +- .../topjava/service/MealServiceTest.java | 2 -- 4 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index 788ae8f6fe5b..5e61c4525a03 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -1,19 +1,48 @@ package ru.javawebinar.topjava.model; -import javax.persistence.FetchType; -import javax.persistence.ManyToOne; +import org.hibernate.validator.constraints.Range; + +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +@NamedQueries({ + @NamedQuery(name = Meal.ALL_SORTED, query = "SELECT m FROM Meal m WHERE m.user.id=:userId ORDER BY m.dateTime DESC"), + @NamedQuery(name = Meal.DELETE, query = "DELETE FROM Meal m WHERE m.id=:id AND m.user.id=:userId"), + @NamedQuery(name = Meal.GET_BETWEEN, query = """ + SELECT m FROM Meal m + WHERE m.user.id=:userId AND m.dateTime >= :startDateTime AND m.dateTime < :endDateTime ORDER BY m.dateTime DESC + """), +// @NamedQuery(name = Meal.UPDATE, query = "UPDATE Meal m SET m.dateTime = :datetime, m.calories= :calories," + +// "m.description=:desc where m.id=:id and m.user.id=:userId") +}) +@Entity +@Table(name = "meal", uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "date_time"}, name = "meals_unique_user_datetime_idx")}) public class Meal extends AbstractBaseEntity { + public static final String ALL_SORTED = "Meal.getAll"; + public static final String DELETE = "Meal.delete"; + public static final String GET_BETWEEN = "Meal.getBetween"; + + @Column(name = "date_time", nullable = false) + @NotNull private LocalDateTime dateTime; + @Column(name = "description", nullable = false) + @NotBlank + @Size(min = 2, max = 120) private String description; + @Column(name = "calories", nullable = false) + @Range(min = 10, max = 5000) private int calories; @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @NotNull private User user; public Meal() { diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java index 9cc19a4dc31b..6df9fd99f575 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java @@ -1,37 +1,62 @@ package ru.javawebinar.topjava.repository.jpa; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.MealRepository; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; import java.time.LocalDateTime; import java.util.List; @Repository +@Transactional(readOnly = true) public class JpaMealRepository implements MealRepository { + @PersistenceContext + private EntityManager em; + @Override + @Transactional public Meal save(Meal meal, int userId) { - return null; + meal.setUser(em.getReference(User.class, userId)); + if (meal.isNew()) { + em.persist(meal); + return meal; + } + return get(meal.id(), userId) == null ? null : em.merge(meal); } @Override + @Transactional public boolean delete(int id, int userId) { - return false; + return em.createNamedQuery(Meal.DELETE) + .setParameter("id", id) + .setParameter("userId", userId) + .executeUpdate() != 0; } @Override public Meal get(int id, int userId) { - return null; + Meal meal = em.find(Meal.class, id); + return meal != null && meal.getUser().getId() == userId ? meal : null; } @Override public List getAll(int userId) { - return null; + return em.createNamedQuery(Meal.ALL_SORTED, Meal.class) + .setParameter("userId", userId) + .getResultList(); } @Override public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { - return null; + return em.createNamedQuery(Meal.GET_BETWEEN, Meal.class) + .setParameter("userId", userId) + .setParameter("startDateTime", startDateTime) + .setParameter("endDateTime", endDateTime) + .getResultList(); } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/MealTestData.java b/src/test/java/ru/javawebinar/topjava/MealTestData.java index 03b594012e37..5e9a9f6f3edb 100644 --- a/src/test/java/ru/javawebinar/topjava/MealTestData.java +++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java @@ -10,7 +10,7 @@ import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; public class MealTestData { - public static final MatcherFactory.Matcher MEAL_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(); + public static final MatcherFactory.Matcher MEAL_MATCHER = Matc public static final int NOT_FOUND = 10; public static final int MEAL1_ID = START_SEQ + 3; diff --git a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java index 9f0f9a54310f..12a4ba26605e 100644 --- a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java @@ -1,6 +1,5 @@ package ru.javawebinar.topjava.service; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -26,7 +25,6 @@ }) @RunWith(SpringRunner.class) @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) -@Ignore public class MealServiceTest { @Autowired From 7d97eb47f2914e7e1937b79a773d7bc80474c305 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 2 Mar 2023 19:36:12 +0700 Subject: [PATCH 039/125] apply 5_3 patch --- .../ru/javawebinar/topjava/model/AbstractBaseEntity.java | 5 ++++- src/main/resources/spring/spring-db.xml | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java index d9343a0740a8..1cd6897add4a 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -1,5 +1,6 @@ package ru.javawebinar.topjava.model; +import org.hibernate.Hibernate; import org.springframework.util.Assert; import javax.persistence.*; @@ -13,6 +14,8 @@ public abstract class AbstractBaseEntity { @Id @SequenceGenerator(name = "global_seq", sequenceName = "global_seq", allocationSize = 1, initialValue = START_SEQ) @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq") +// See https://hibernate.atlassian.net/browse/HHH-3718 and https://hibernate.atlassian.net/browse/HHH-12034 +// Proxy initialization when accessing its identifier managed now by JPA_PROXY_COMPLIANCE setting protected Integer id; protected AbstractBaseEntity() { @@ -49,7 +52,7 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { + if (o == null || !getClass().equals(Hibernate.getClass(o))) { return false; } AbstractBaseEntity that = (AbstractBaseEntity) o; diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 970261d06d54..ae30dc794b54 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -48,6 +48,7 @@ + From fc165ecc1806355064e61a6b03451f7351fd79eb Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 2 Mar 2023 19:37:14 +0700 Subject: [PATCH 040/125] apply 5_4 patch --- src/main/resources/logback.xml | 2 +- .../topjava/service/MealServiceTest.java | 34 ++++++++++++++++++- src/test/resources/logback-test.xml | 16 +++++++-- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index b73b151765a9..d5fbdd95db91 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -16,7 +16,7 @@ UTF-8 - %d{HH:mm:ss.SSS} %-5level %class{50}.%M:%L - %msg%n + %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%class{50}.%M:%L) - %msg%n diff --git a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java index 12a4ba26605e..2af6ee183cd0 100644 --- a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java @@ -1,7 +1,13 @@ package ru.javawebinar.topjava.service; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; +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.ContextConfiguration; @@ -13,8 +19,10 @@ 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; @@ -26,10 +34,33 @@ @RunWith(SpringRunner.class) @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) 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); @@ -87,7 +118,8 @@ public void update() { @Test public void updateNotOwn() { - assertThrows(NotFoundException.class, () -> service.update(meal1, ADMIN_ID)); + NotFoundException exception = assertThrows(NotFoundException.class, () -> service.update(meal1, ADMIN_ID)); + Assert.assertEquals("Not found entity with id=" + MEAL1_ID, exception.getMessage()); MEAL_MATCHER.assertMatch(service.get(MEAL1_ID, USER_ID), meal1); } diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index da4eed7b9468..803655475302 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -7,12 +7,22 @@ UTF-8 - %d{HH:mm:ss.SSS} %-5level %class{50}.%M:%L - %msg%n + %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%class{50}.%M:%L) - %msg%n - - + + + UTF-8 + %magenta(%msg%n) + + + + + + + + From 81070a59a7c2c0143ba1618431cdcf1b7ecef453 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 2 Mar 2023 20:46:58 +0700 Subject: [PATCH 041/125] apply 5_5 patch --- pom.xml | 57 ++++++++++++------- .../java/ru/javawebinar/topjava/Profiles.java | 15 +++++ src/main/resources/db/hsqldb.properties | 1 - src/main/resources/db/postgres.properties | 1 - src/main/resources/spring/spring-db.xml | 55 ++++++++++-------- .../topjava/service/MealServiceTest.java | 3 + .../topjava/service/UserServiceTest.java | 3 + 7 files changed, 90 insertions(+), 45 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/Profiles.java diff --git a/pom.xml b/pom.xml index 8f6705ce0176..26dd81645beb 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,7 @@ UTF-8 UTF-8 5.3.25 + 9.0.72 5.6.15.Final @@ -70,13 +71,6 @@ ${slf4j.version} - - org.slf4j - jul-to-slf4j - ${slf4j.version} - runtime - - ch.qos.logback logback-classic @@ -102,19 +96,6 @@ ${spring.version} - - - org.postgresql - postgresql - ${postgresql.version} - - - org.hsqldb - hsqldb - 2.3.4 - - - org.hibernate @@ -171,6 +152,42 @@ + + hsqldb + + + org.hsqldb + hsqldb + 2.3.4 + + + + + + postgres + + + org.postgresql + postgresql + ${postgresql.version} + + + org.apache.tomcat + tomcat-jdbc + ${tomcat.version} + provided + + + org.slf4j + jul-to-slf4j + ${slf4j.version} + runtime + + + + true + + diff --git a/src/main/java/ru/javawebinar/topjava/Profiles.java b/src/main/java/ru/javawebinar/topjava/Profiles.java new file mode 100644 index 000000000000..c438cbbe7a6e --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/Profiles.java @@ -0,0 +1,15 @@ +package ru.javawebinar.topjava; + +public class Profiles { + public static final String + JDBC = "jdbc", + JPA = "jpa"; + + public static final String REPOSITORY_IMPLEMENTATION = JPA; + + public static final String + POSTGRES_DB = "postgres", + HSQL_DB = "hsqldb"; + + public static final String ACTIVE_DB = POSTGRES_DB; +} diff --git a/src/main/resources/db/hsqldb.properties b/src/main/resources/db/hsqldb.properties index c7944e25c223..17c03ef4ebda 100644 --- a/src/main/resources/db/hsqldb.properties +++ b/src/main/resources/db/hsqldb.properties @@ -3,7 +3,6 @@ database.url=jdbc:hsqldb:mem:topjava database.username=sa database.password= -database.driverClassName=org.hsqldb.jdbcDriver database.init=true jdbc.initLocation=classpath:db/initDB_hsql.sql diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties index fa02ee04911f..75d7d68730ad 100644 --- a/src/main/resources/db/postgres.properties +++ b/src/main/resources/db/postgres.properties @@ -1,7 +1,6 @@ database.url=jdbc:postgresql://localhost:5432/topjava database.username=user database.password=password -database.driverClassName=org.postgresql.Driver database.init=true jdbc.initLocation=classpath:db/initDB.sql diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index ae30dc794b54..c3cc57e6ca47 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -9,20 +9,6 @@ http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> - - - - - - - - - - @@ -30,15 +16,6 @@ - - - - - - - - @@ -76,4 +53,36 @@ --> + + + + + + + + + + + + + + + + + + \ 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 index 2af6ee183cd0..979a93209cd0 100644 --- a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java @@ -9,11 +9,13 @@ import org.junit.runner.RunWith; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; import org.springframework.dao.DataAccessException; 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.Profiles; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.util.exception.NotFoundException; @@ -33,6 +35,7 @@ }) @RunWith(SpringRunner.class) @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) +@ActiveProfiles(Profiles.ACTIVE_DB) public class MealServiceTest { private static final Logger log = getLogger("result"); diff --git a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java index b318695cc6ac..75c9967702e8 100644 --- a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java @@ -4,10 +4,12 @@ import org.junit.runner.RunWith; 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.Profiles; import ru.javawebinar.topjava.UserTestData; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; @@ -24,6 +26,7 @@ }) @RunWith(SpringRunner.class) @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) +@ActiveProfiles(Profiles.ACTIVE_DB) public class UserServiceTest { @Autowired From 1d67990c985025f4b637264d327c815f525b67a2 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 2 Mar 2023 20:50:48 +0700 Subject: [PATCH 042/125] apply 5_6 patch --- .../java/ru/javawebinar/topjava/Profiles.java | 13 ++++++++++++- .../topjava/ActiveDbProfileResolver.java | 19 +++++++++++++++++++ .../topjava/service/MealServiceTest.java | 8 ++++---- .../topjava/service/UserServiceTest.java | 4 ++-- 4 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java diff --git a/src/main/java/ru/javawebinar/topjava/Profiles.java b/src/main/java/ru/javawebinar/topjava/Profiles.java index c438cbbe7a6e..1eaf40be3a9b 100644 --- a/src/main/java/ru/javawebinar/topjava/Profiles.java +++ b/src/main/java/ru/javawebinar/topjava/Profiles.java @@ -1,5 +1,7 @@ package ru.javawebinar.topjava; +import org.springframework.util.ClassUtils; + public class Profiles { public static final String JDBC = "jdbc", @@ -11,5 +13,14 @@ public class Profiles { POSTGRES_DB = "postgres", HSQL_DB = "hsqldb"; - public static final String ACTIVE_DB = POSTGRES_DB; + // Get DB profile depending of DB driver in classpath + public static String getActiveDbProfile() { + if (ClassUtils.isPresent("org.postgresql.Driver", null)) { + return POSTGRES_DB; + } else if (ClassUtils.isPresent("org.hsqldb.jdbcDriver", null)) { + return HSQL_DB; + } else { + throw new IllegalStateException("Could not find DB driver"); + } + } } 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 000000000000..43f143cc7664 --- /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/service/MealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java index 979a93209cd0..f6c3bb48f6c1 100644 --- a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java @@ -9,13 +9,13 @@ import org.junit.runner.RunWith; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.ActiveProfiles; 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.Profiles; +import ru.javawebinar.topjava.ActiveDbProfileResolver; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.util.exception.NotFoundException; @@ -35,7 +35,7 @@ }) @RunWith(SpringRunner.class) @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) -@ActiveProfiles(Profiles.ACTIVE_DB) +@ActiveProfiles(resolver = ActiveDbProfileResolver.class) public class MealServiceTest { private static final Logger log = getLogger("result"); @@ -121,7 +121,7 @@ public void update() { @Test public void updateNotOwn() { - NotFoundException exception = assertThrows(NotFoundException.class, () -> service.update(meal1, ADMIN_ID)); + 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); } diff --git a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java index 75c9967702e8..73bcb2e92c97 100644 --- a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java @@ -9,7 +9,7 @@ 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.Profiles; +import ru.javawebinar.topjava.ActiveDbProfileResolver; import ru.javawebinar.topjava.UserTestData; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; @@ -26,7 +26,7 @@ }) @RunWith(SpringRunner.class) @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) -@ActiveProfiles(Profiles.ACTIVE_DB) +@ActiveProfiles(resolver = ActiveDbProfileResolver.class) public class UserServiceTest { @Autowired From 7bdebe50cd8761fafea1f47e65c10a8c0104aac6 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 2 Mar 2023 23:41:17 +0700 Subject: [PATCH 043/125] apply 5_7 patch --- pom.xml | 18 +++++--- .../java/ru/javawebinar/topjava/Profiles.java | 5 ++- .../topjava/model/AbstractBaseEntity.java | 5 ++- .../datajpa/CrudMealRepository.java | 7 +++ .../datajpa/CrudUserRepository.java | 19 ++++++++ .../datajpa/DataJpaMealRepository.java | 43 ++++++++++++++++++ .../datajpa/DataJpaUserRepository.java | 44 +++++++++++++++++++ src/main/resources/spring/spring-db.xml | 16 ++++++- .../topjava/service/MealServiceTest.java | 6 +-- 9 files changed, 149 insertions(+), 14 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java create mode 100644 src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java create mode 100644 src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java create mode 100644 src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java diff --git a/pom.xml b/pom.xml index 26dd81645beb..45de7a7e98df 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,7 @@ UTF-8 UTF-8 5.3.25 + 2.7.8 9.0.72 @@ -88,12 +89,11 @@ org.springframework spring-context - ${spring.version} - org.springframework - spring-orm - ${spring.version} + org.springframework.data + spring-data-jpa + ${spring-data-jpa.version} @@ -140,7 +140,6 @@ org.springframework spring-test - ${spring.version} test @@ -191,5 +190,14 @@ + + + org.springframework + spring-framework-bom + ${spring.version} + pom + import + + diff --git a/src/main/java/ru/javawebinar/topjava/Profiles.java b/src/main/java/ru/javawebinar/topjava/Profiles.java index 1eaf40be3a9b..e4111a2afd28 100644 --- a/src/main/java/ru/javawebinar/topjava/Profiles.java +++ b/src/main/java/ru/javawebinar/topjava/Profiles.java @@ -5,9 +5,10 @@ public class Profiles { public static final String JDBC = "jdbc", - JPA = "jpa"; + JPA = "jpa", + DATAJPA = "datajpa"; - public static final String REPOSITORY_IMPLEMENTATION = JPA; + public static final String REPOSITORY_IMPLEMENTATION = DATAJPA; public static final String POSTGRES_DB = "postgres", diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java index 1cd6897add4a..2ab30f34f272 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -1,6 +1,7 @@ package ru.javawebinar.topjava.model; import org.hibernate.Hibernate; +import org.springframework.data.domain.Persistable; import org.springframework.util.Assert; import javax.persistence.*; @@ -8,7 +9,7 @@ @MappedSuperclass // http://stackoverflow.com/questions/594597/hibernate-annotations-which-is-better-field-or-property-access @Access(AccessType.FIELD) -public abstract class AbstractBaseEntity { +public abstract class AbstractBaseEntity implements Persistable { public static final int START_SEQ = 100000; @Id @@ -29,6 +30,7 @@ public void setId(Integer id) { this.id = id; } + @Override public Integer getId() { return id; } @@ -38,6 +40,7 @@ public int id() { return id; } + @Override public boolean isNew() { return this.id == null; } diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java new file mode 100644 index 000000000000..a3659675c910 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java @@ -0,0 +1,7 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.javawebinar.topjava.model.Meal; + +public interface CrudMealRepository extends JpaRepository { +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java new file mode 100644 index 000000000000..24c42a814220 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java @@ -0,0 +1,19 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.User; + +@Transactional(readOnly = true) +public interface CrudUserRepository extends JpaRepository { + @Transactional + @Modifying +// @Query(name = User.DELETE) + @Query("DELETE FROM User u WHERE u.id=:id") + int delete(@Param("id") int id); + + User getByEmail(String email); +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java new file mode 100644 index 000000000000..d1b4c8efe9cc --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java @@ -0,0 +1,43 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.MealRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public class DataJpaMealRepository implements MealRepository { + + private final CrudMealRepository crudRepository; + + public DataJpaMealRepository(CrudMealRepository crudRepository) { + this.crudRepository = crudRepository; + } + + @Override + public Meal save(Meal meal, int userId) { + return null; + } + + @Override + public boolean delete(int id, int userId) { + return false; + } + + @Override + public Meal get(int id, int userId) { + return null; + } + + @Override + public List getAll(int userId) { + return null; + } + + @Override + public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { + return null; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java new file mode 100644 index 000000000000..bc240d2d5a90 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java @@ -0,0 +1,44 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.List; + +@Repository +public class DataJpaUserRepository implements UserRepository { + private static final Sort SORT_NAME_EMAIL = Sort.by(Sort.Direction.ASC, "name", "email"); + + private final CrudUserRepository crudRepository; + + public DataJpaUserRepository(CrudUserRepository crudRepository) { + this.crudRepository = crudRepository; + } + + @Override + public User save(User user) { + return crudRepository.save(user); + } + + @Override + public boolean delete(int id) { + return crudRepository.delete(id) != 0; + } + + @Override + public User get(int id) { + return crudRepository.findById(id).orElse(null); + } + + @Override + public User getByEmail(String email) { + return crudRepository.getByEmail(email); + } + + @Override + public List getAll() { + return crudRepository.findAll(SORT_NAME_EMAIL); + } +} diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index c3cc57e6ca47..4106dbf1fb86 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -4,12 +4,17 @@ xmlns:context="http://www.springframework.org/schema/context" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:tx="http://www.springframework.org/schema/tx" + xmlns:jpa="http://www.springframework.org/schema/data/jpa" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd - http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> + http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd + http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd"> - + + + + @@ -85,4 +90,11 @@ p:username="${database.username}" p:password="${database.password}"/> + + + + + + + \ 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 index f6c3bb48f6c1..4c78a2cb6fa9 100644 --- a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java @@ -1,9 +1,6 @@ package ru.javawebinar.topjava.service; -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.Rule; -import org.junit.Test; +import org.junit.*; import org.junit.rules.Stopwatch; import org.junit.runner.Description; import org.junit.runner.RunWith; @@ -36,6 +33,7 @@ @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"); From 6ae8cde8628399c2057efff22e7baab875c37e62 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Fri, 3 Mar 2023 13:48:55 +0700 Subject: [PATCH 044/125] apply 5_8 patch --- pom.xml | 31 ++++++++++++++++++- .../topjava/service/UserService.java | 6 ++++ src/main/resources/cache/ehcache.xml | 25 +++++++++++++++ src/main/resources/spring/spring-app.xml | 1 + src/main/resources/spring/spring-cache.xml | 20 ++++++++++++ .../topjava/service/UserServiceTest.java | 10 ++++++ 6 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/cache/ehcache.xml create mode 100644 src/main/resources/spring/spring-cache.xml diff --git a/pom.xml b/pom.xml index 45de7a7e98df..06c06e6603f4 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,9 @@ 6.2.5.Final 3.0.1-b12 + + 3.10.8 + 1.2.11 1.7.36 @@ -88,7 +91,7 @@ org.springframework - spring-context + spring-context-support org.springframework.data @@ -116,6 +119,32 @@ provided + + + javax.cache + cache-api + 1.1.0 + + + org.ehcache + ehcache + runtime + ${ehcache.version} + + + org.glassfish.jaxb + jaxb-runtime + + + + + + + org.glassfish.jaxb + jaxb-runtime + 2.4.0-b180830.0438 + + javax.servlet diff --git a/src/main/java/ru/javawebinar/topjava/service/UserService.java b/src/main/java/ru/javawebinar/topjava/service/UserService.java index 09ccee68c43f..e0979b7ee4fd 100644 --- a/src/main/java/ru/javawebinar/topjava/service/UserService.java +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -1,5 +1,7 @@ package ru.javawebinar.topjava.service; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import ru.javawebinar.topjava.model.User; @@ -19,11 +21,13 @@ public UserService(UserRepository repository) { this.repository = repository; } + @CacheEvict(value = "users", allEntries = true) public User create(User user) { Assert.notNull(user, "user must not be null"); return repository.save(user); } + @CacheEvict(value = "users", allEntries = true) public void delete(int id) { checkNotFoundWithId(repository.delete(id), id); } @@ -37,10 +41,12 @@ public User getByEmail(String email) { return checkNotFound(repository.getByEmail(email), "email=" + email); } + @Cacheable("users") public List getAll() { return repository.getAll(); } + @CacheEvict(value = "users", allEntries = true) public void update(User user) { Assert.notNull(user, "user must not be null"); checkNotFoundWithId(repository.save(user), user.id()); diff --git a/src/main/resources/cache/ehcache.xml b/src/main/resources/cache/ehcache.xml new file mode 100644 index 000000000000..05589f71f06e --- /dev/null +++ b/src/main/resources/cache/ehcache.xml @@ -0,0 +1,25 @@ + + + + + + + + + + 5 + + 5000 + + + + + + + 1 + + + + diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml index 4c17228b7a87..d6c643e972e1 100644 --- a/src/main/resources/spring/spring-app.xml +++ b/src/main/resources/spring/spring-app.xml @@ -9,6 +9,7 @@ --> + diff --git a/src/main/resources/spring/spring-cache.xml b/src/main/resources/spring/spring-cache.xml new file mode 100644 index 000000000000..73325fee065f --- /dev/null +++ b/src/main/resources/spring/spring-cache.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + \ 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 index 73bcb2e92c97..edc3aed7bbd2 100644 --- a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java @@ -1,8 +1,10 @@ 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; @@ -32,6 +34,14 @@ 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()); From c7af1b7f06f681b77e53c247d5b65bc6c9ec9824 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Fri, 3 Mar 2023 14:40:26 +0700 Subject: [PATCH 045/125] fix: problems with removed class --- src/test/java/ru/javawebinar/topjava/MealTestData.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/ru/javawebinar/topjava/MealTestData.java b/src/test/java/ru/javawebinar/topjava/MealTestData.java index 5e9a9f6f3edb..03b594012e37 100644 --- a/src/test/java/ru/javawebinar/topjava/MealTestData.java +++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java @@ -10,7 +10,7 @@ import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; public class MealTestData { - public static final MatcherFactory.Matcher MEAL_MATCHER = Matc + public static final MatcherFactory.Matcher MEAL_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(); public static final int NOT_FOUND = 10; public static final int MEAL1_ID = START_SEQ + 3; From 33f2bdc12734db40fb19548f1581ef7f11b7fd4c Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Wed, 8 Mar 2023 18:34:23 +0700 Subject: [PATCH 046/125] remove patches zip file --- patch-20230302T033752Z-001.zip | Bin 18040 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 patch-20230302T033752Z-001.zip diff --git a/patch-20230302T033752Z-001.zip b/patch-20230302T033752Z-001.zip deleted file mode 100644 index d9a1eda68bc41afc65cb7dd07257738ca4f631a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18040 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%0ABHTGk)$6WH$(5_Rmy;*y8|d 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 J$}3JF9sths(0>2` From 58ded9afbee6da9f8ea8ae16f117f902678c6090 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 9 Mar 2023 14:14:41 +0700 Subject: [PATCH 047/125] apply patch 6_01 --- patch-20230309T061337Z-001.zip | Bin 0 -> 31363 bytes .../datajpa/CrudMealRepository.java | 21 +++++++++++++- .../datajpa/DataJpaMealRepository.java | 26 ++++++++++++------ 3 files changed, 38 insertions(+), 9 deletions(-) create mode 100644 patch-20230309T061337Z-001.zip diff --git a/patch-20230309T061337Z-001.zip b/patch-20230309T061337Z-001.zip new file mode 100644 index 0000000000000000000000000000000000000000..7a457a697e974cc14fffda3b010cad5bc7e5d3b0 GIT binary patch literal 31363 zcma&NV~{S}wypivELY94?W$R}ZQHi(S+;H4wr$(C?V81J?eoRjXYCs&&b=9te=_>` zkr^4SJ$()35`9$W+IV)O6R81m6U zsNni&DWM$$P zDbQXO5JpNnIm8RH6W1hEr*N+x3JSDJZo|ufL<7t3{+5_U7>`k>>$gIWgZqORUT=&B zCv7nN15J`^{jngkIuVczkejTglRQM*n4>l|%ecWORH!K~T95K2QS4R_=U%1WdlaNV ziU>@Emqi~SBISKe6}}yk4Ⓢ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 { -} + + @Modifying + @Transactional + @Query("DELETE FROM Meal m WHERE m.id=:id AND m.user.id=:userId") + int delete(@Param("id") int id, @Param("userId") int userId); + + @Query("SELECT m FROM Meal m WHERE m.user.id=:userId ORDER BY m.dateTime DESC") + List getAll(@Param("userId") int userId); + + @Query("SELECT m from Meal m WHERE m.user.id=:userId AND m.dateTime >= :startDate AND m.dateTime < :endDate ORDER BY m.dateTime DESC") + List getBetweenHalfOpen(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate, @Param("userId") int userId); +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java index d1b4c8efe9cc..c988d66a9cb1 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java @@ -1,6 +1,7 @@ package ru.javawebinar.topjava.repository.datajpa; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; @@ -10,34 +11,43 @@ @Repository public class DataJpaMealRepository implements MealRepository { - private final CrudMealRepository crudRepository; + private final CrudMealRepository crudMealRepository; + private final CrudUserRepository crudUserRepository; - public DataJpaMealRepository(CrudMealRepository crudRepository) { - this.crudRepository = crudRepository; + public DataJpaMealRepository(CrudMealRepository crudMealRepository, CrudUserRepository crudUserRepository) { + this.crudMealRepository = crudMealRepository; + this.crudUserRepository = crudUserRepository; } @Override + @Transactional public Meal save(Meal meal, int userId) { - return null; + if (!meal.isNew() && get(meal.id(), userId) == null) { + return null; + } + meal.setUser(crudUserRepository.getReferenceById(userId)); + return crudMealRepository.save(meal); } @Override public boolean delete(int id, int userId) { - return false; + return crudMealRepository.delete(id, userId) != 0; } @Override public Meal get(int id, int userId) { - return null; + return crudMealRepository.findById(id) + .filter(meal -> meal.getUser().getId() == userId) + .orElse(null); } @Override public List getAll(int userId) { - return null; + return crudMealRepository.getAll(userId); } @Override public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { - return null; + return crudMealRepository.getBetweenHalfOpen(startDateTime, endDateTime, userId); } } From b1145136f428989a91db0a0a780e1bf8d68b0ea8 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 9 Mar 2023 14:14:54 +0700 Subject: [PATCH 048/125] apply patch 6_02 --- src/main/resources/spring/spring-db.xml | 84 +++++++++---------- ...Test.java => AbstractMealServiceTest.java} | 48 +---------- .../topjava/service/AbstractServiceTest.java | 54 ++++++++++++ ...Test.java => AbstractUserServiceTest.java} | 16 +--- .../datajpa/DataJpaMealServiceTest.java | 10 +++ .../datajpa/DataJpaUserServiceTest.java | 10 +++ .../service/jdbc/JdbcMealServiceTest.java | 10 +++ .../service/jdbc/JdbcUserServiceTest.java | 10 +++ .../service/jpa/JpaMealServiceTest.java | 10 +++ .../service/jpa/JpaUserServiceTest.java | 10 +++ 10 files changed, 160 insertions(+), 102 deletions(-) rename src/test/java/ru/javawebinar/topjava/service/{MealServiceTest.java => AbstractMealServiceTest.java} (62%) create mode 100644 src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java rename src/test/java/ru/javawebinar/topjava/service/{UserServiceTest.java => AbstractUserServiceTest.java} (77%) create mode 100644 src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java create mode 100644 src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java create mode 100644 src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java create mode 100644 src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java create mode 100644 src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java create mode 100644 src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 4106dbf1fb86..7a334acebd47 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -11,53 +11,12 @@ http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -92,9 +51,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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/AbstractMealServiceTest.java similarity index 62% rename from src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java rename to src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java index 4c78a2cb6fa9..0637bd205af5 100644 --- a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java @@ -1,67 +1,25 @@ 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.junit.Assert; +import org.junit.Test; 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"); - } - }; +public abstract class AbstractMealServiceTest extends AbstractServiceTest { @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); diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java new file mode 100644 index 000000000000..2418754e6e7e --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java @@ -0,0 +1,54 @@ +package ru.javawebinar.topjava.service; + +import org.junit.AfterClass; +import org.junit.Rule; +import org.junit.rules.Stopwatch; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +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 java.util.concurrent.TimeUnit; + +import static org.slf4j.LoggerFactory.getLogger; + +@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 abstract class AbstractServiceTest { + 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 Stopwatch stopwatch = new Stopwatch() { + @Override + protected void finished(long nanos, Description description) { + String result = String.format("%-95s %7d", description.getDisplayName(), TimeUnit.NANOSECONDS.toMillis(nanos)); + results.append(result).append('\n'); + log.info(result + " ms\n"); + } + }; + + + // https://dzone.com/articles/applying-new-jdk-11-string-methods + private static final String DELIM = "-".repeat(103); + + @AfterClass + public static void printResult() { + log.info("\n" + DELIM + + "\nTest Duration, ms" + + "\n" + DELIM + "\n" + results + DELIM + "\n"); + results.setLength(0); + } +} \ 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/AbstractUserServiceTest.java similarity index 77% rename from src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java rename to src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java index edc3aed7bbd2..3d1025de9d23 100644 --- a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -2,16 +2,9 @@ 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; @@ -22,14 +15,7 @@ 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 { +public abstract class AbstractUserServiceTest extends AbstractServiceTest { @Autowired private UserService service; diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java new file mode 100644 index 000000000000..e28cc6d6bc53 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java @@ -0,0 +1,10 @@ +package ru.javawebinar.topjava.service.datajpa; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.AbstractMealServiceTest; + +import static ru.javawebinar.topjava.Profiles.DATAJPA; + +@ActiveProfiles(DATAJPA) +public class DataJpaMealServiceTest extends AbstractMealServiceTest { +} diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java new file mode 100644 index 000000000000..a0757e739339 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java @@ -0,0 +1,10 @@ +package ru.javawebinar.topjava.service.datajpa; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.AbstractUserServiceTest; + +import static ru.javawebinar.topjava.Profiles.DATAJPA; + +@ActiveProfiles(DATAJPA) +public class DataJpaUserServiceTest extends AbstractUserServiceTest { +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java new file mode 100644 index 000000000000..9ff4ae615b2b --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java @@ -0,0 +1,10 @@ +package ru.javawebinar.topjava.service.jdbc; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.AbstractMealServiceTest; + +import static ru.javawebinar.topjava.Profiles.JDBC; + +@ActiveProfiles(JDBC) +public class JdbcMealServiceTest extends AbstractMealServiceTest { +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java new file mode 100644 index 000000000000..419f68ed1098 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java @@ -0,0 +1,10 @@ +package ru.javawebinar.topjava.service.jdbc; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.AbstractUserServiceTest; + +import static ru.javawebinar.topjava.Profiles.JDBC; + +@ActiveProfiles(JDBC) +public class JdbcUserServiceTest extends AbstractUserServiceTest { +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java new file mode 100644 index 000000000000..70e7bf865421 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java @@ -0,0 +1,10 @@ +package ru.javawebinar.topjava.service.jpa; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.AbstractMealServiceTest; + +import static ru.javawebinar.topjava.Profiles.JPA; + +@ActiveProfiles(JPA) +public class JpaMealServiceTest extends AbstractMealServiceTest { +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java new file mode 100644 index 000000000000..d1b3e4699785 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java @@ -0,0 +1,10 @@ +package ru.javawebinar.topjava.service.jpa; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.AbstractUserServiceTest; + +import static ru.javawebinar.topjava.Profiles.JPA; + +@ActiveProfiles(JPA) +public class JpaUserServiceTest extends AbstractUserServiceTest { +} \ No newline at end of file From 75667857d59829271d20cfb67f04157aa0d44e30 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 9 Mar 2023 14:15:11 +0700 Subject: [PATCH 049/125] apply patch 6_03 --- .../ru/javawebinar/topjava/TimingRules.java | 42 +++++++++++++++++++ .../topjava/service/AbstractServiceTest.java | 37 +++------------- 2 files changed, 48 insertions(+), 31 deletions(-) create mode 100644 src/test/java/ru/javawebinar/topjava/TimingRules.java diff --git a/src/test/java/ru/javawebinar/topjava/TimingRules.java b/src/test/java/ru/javawebinar/topjava/TimingRules.java new file mode 100644 index 000000000000..fdd3d8779dbc --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/TimingRules.java @@ -0,0 +1,42 @@ +package ru.javawebinar.topjava; + +import org.junit.rules.ExternalResource; +import org.junit.rules.Stopwatch; +import org.junit.runner.Description; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +public class TimingRules { + private static final Logger log = LoggerFactory.getLogger("result"); + + private static final StringBuilder results = new StringBuilder(); + + // http://stackoverflow.com/questions/14892125/what-is-the-best-practice-to-determine-the-execution-time-of-the-bussiness-relev + public static final Stopwatch STOPWATCH = new Stopwatch() { + @Override + protected void finished(long nanos, Description description) { + String result = String.format("%-95s %7d", description.getDisplayName(), TimeUnit.NANOSECONDS.toMillis(nanos)); + results.append(result).append('\n'); + log.info(result + " ms\n"); + } + }; + + // https://dzone.com/articles/applying-new-jdk-11-string-methods + private static final String DELIM = "-".repeat(103); + + public static final ExternalResource SUMMARY = new ExternalResource() { + @Override + protected void before() throws Throwable { + results.setLength(0); + } + + @Override + protected void after() { + log.info("\n" + DELIM + + "\nTest Duration, ms" + + "\n" + DELIM + "\n" + results + DELIM + "\n"); + } + }; +} diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java index 2418754e6e7e..036d28c82434 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java @@ -1,21 +1,17 @@ package ru.javawebinar.topjava.service; -import org.junit.AfterClass; +import org.junit.ClassRule; import org.junit.Rule; +import org.junit.rules.ExternalResource; import org.junit.rules.Stopwatch; -import org.junit.runner.Description; import org.junit.runner.RunWith; -import org.slf4j.Logger; 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 java.util.concurrent.TimeUnit; - -import static org.slf4j.LoggerFactory.getLogger; +import ru.javawebinar.topjava.TimingRules; @ContextConfiguration({ "classpath:spring/spring-app.xml", @@ -25,30 +21,9 @@ @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) @ActiveProfiles(resolver = ActiveDbProfileResolver.class) public abstract class AbstractServiceTest { - private static final Logger log = getLogger("result"); - - private static final StringBuilder results = new StringBuilder(); + @ClassRule + public static ExternalResource summary = TimingRules.SUMMARY; @Rule - // http://stackoverflow.com/questions/14892125/what-is-the-best-practice-to-determine-the-execution-time-of-the-bussiness-relev - public Stopwatch stopwatch = new Stopwatch() { - @Override - protected void finished(long nanos, Description description) { - String result = String.format("%-95s %7d", description.getDisplayName(), TimeUnit.NANOSECONDS.toMillis(nanos)); - results.append(result).append('\n'); - log.info(result + " ms\n"); - } - }; - - - // https://dzone.com/articles/applying-new-jdk-11-string-methods - private static final String DELIM = "-".repeat(103); - - @AfterClass - public static void printResult() { - log.info("\n" + DELIM + - "\nTest Duration, ms" + - "\n" + DELIM + "\n" + results + DELIM + "\n"); - results.setLength(0); - } + public Stopwatch stopwatch = TimingRules.STOPWATCH; } \ No newline at end of file From b555178d0b88f5aa98c2714f29978edd8b7725e9 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 9 Mar 2023 14:27:10 +0700 Subject: [PATCH 050/125] apply patch 6_04 --- patch-20230309T061337Z-001.zip | Bin 31363 -> 0 bytes .../repository/jdbc/JdbcMealRepository.java | 42 +++++++++++++++--- .../javawebinar/topjava/web/MealServlet.java | 9 ++-- 3 files changed, 41 insertions(+), 10 deletions(-) delete mode 100644 patch-20230309T061337Z-001.zip diff --git a/patch-20230309T061337Z-001.zip b/patch-20230309T061337Z-001.zip deleted file mode 100644 index 7a457a697e974cc14fffda3b010cad5bc7e5d3b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31363 zcma&NV~{S}wypivELY94?W$R}ZQHi(S+;H4wr$(C?V81J?eoRjXYCs&&b=9te=_>` zkr^4SJ$()35`9$W+IV)O6R81m6U zsNni&DWM$$P zDbQXO5JpNnIm8RH6W1hEr*N+x3JSDJZo|ufL<7t3{+5_U7>`k>>$gIWgZqORUT=&B zCv7nN15J`^{jngkIuVczkejTglRQM*n4>l|%ecWORH!K~T95K2QS4R_=U%1WdlaNV ziU>@Emqi~SBISKe6}}yk4Ⓢ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 implements MealRepository { private static final RowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(Meal.class); @@ -26,8 +27,7 @@ public class JdbcMealRepository implements MealRepository { private final SimpleJdbcInsert insertMeal; - @Autowired - public JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { this.insertMeal = new SimpleJdbcInsert(jdbcTemplate) .withTableName("meal") .usingGeneratedKeyColumns("id"); @@ -36,13 +36,41 @@ public JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; } + protected abstract T toDbDateTime(LocalDateTime ldt); + + @Repository + @Profile(Profiles.POSTGRES_DB) + public static class Java8JdbcMealRepository extends JdbcMealRepository { + public Java8JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + super(jdbcTemplate, namedParameterJdbcTemplate); + } + + @Override + protected LocalDateTime toDbDateTime(LocalDateTime ldt) { + return ldt; + } + } + + @Repository + @Profile(Profiles.HSQL_DB) + public static class TimestampJdbcMealRepository extends JdbcMealRepository { + public TimestampJdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + super(jdbcTemplate, namedParameterJdbcTemplate); + } + + @Override + protected Timestamp toDbDateTime(LocalDateTime ldt) { + return Timestamp.valueOf(ldt); + } + } + @Override public Meal save(Meal meal, int userId) { MapSqlParameterSource map = new MapSqlParameterSource() .addValue("id", meal.getId()) .addValue("description", meal.getDescription()) .addValue("calories", meal.getCalories()) - .addValue("date_time", meal.getDateTime()) + .addValue("date_time", toDbDateTime(meal.getDateTime())) .addValue("user_id", userId); if (meal.isNew()) { @@ -81,6 +109,6 @@ public List getAll(int userId) { public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { return jdbcTemplate.query( "SELECT * FROM meal WHERE user_id=? AND date_time >= ? AND date_time < ? ORDER BY date_time DESC", - ROW_MAPPER, userId, startDateTime, endDateTime); + ROW_MAPPER, userId, toDbDateTime(startDateTime), toDbDateTime(endDateTime)); } } diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java index 13efd88b1737..62909cd33d4e 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -1,8 +1,8 @@ package ru.javawebinar.topjava.web; -import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.util.StringUtils; +import ru.javawebinar.topjava.Profiles; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.web.meal.MealRestController; @@ -22,12 +22,15 @@ public class MealServlet extends HttpServlet { - private ConfigurableApplicationContext springContext; + private ClassPathXmlApplicationContext springContext; private MealRestController mealController; @Override public void init() { - springContext = new ClassPathXmlApplicationContext("spring/spring-app.xml", "spring/spring-db.xml"); + springContext = new ClassPathXmlApplicationContext(new String[]{"spring/spring-app.xml", "spring/spring-db.xml"}, false); +// springContext.setConfigLocations("spring/spring-app.xml", "spring/spring-db.xml"); + springContext.getEnvironment().setActiveProfiles(Profiles.getActiveDbProfile(), Profiles.REPOSITORY_IMPLEMENTATION); + springContext.refresh(); mealController = springContext.getBean(MealRestController.class); } From 303756a3ec760b8931a9ddbbf87b9e674bd084a7 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 9 Mar 2023 14:27:23 +0700 Subject: [PATCH 051/125] apply patch 6_05 --- pom.xml | 2 +- .../repository/jdbc/JdbcMealRepository.java | 40 +++---------------- 2 files changed, 6 insertions(+), 36 deletions(-) diff --git a/pom.xml b/pom.xml index 06c06e6603f4..707bb97d622b 100644 --- a/pom.xml +++ b/pom.xml @@ -186,7 +186,7 @@ org.hsqldb hsqldb - 2.3.4 + 2.7.1 diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java index 865265fe5d47..b7f268f3a5ee 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java @@ -1,6 +1,5 @@ package ru.javawebinar.topjava.repository.jdbc; -import org.springframework.context.annotation.Profile; import org.springframework.dao.support.DataAccessUtils; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; @@ -9,15 +8,14 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.stereotype.Repository; -import ru.javawebinar.topjava.Profiles; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; -import java.sql.Timestamp; import java.time.LocalDateTime; import java.util.List; -public abstract class JdbcMealRepository implements MealRepository { +@Repository +public class JdbcMealRepository implements MealRepository { private static final RowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(Meal.class); @@ -27,7 +25,7 @@ public abstract class JdbcMealRepository implements MealRepository { private final SimpleJdbcInsert insertMeal; - JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + public JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { this.insertMeal = new SimpleJdbcInsert(jdbcTemplate) .withTableName("meal") .usingGeneratedKeyColumns("id"); @@ -36,41 +34,13 @@ public abstract class JdbcMealRepository implements MealRepository { this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; } - protected abstract T toDbDateTime(LocalDateTime ldt); - - @Repository - @Profile(Profiles.POSTGRES_DB) - public static class Java8JdbcMealRepository extends JdbcMealRepository { - public Java8JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { - super(jdbcTemplate, namedParameterJdbcTemplate); - } - - @Override - protected LocalDateTime toDbDateTime(LocalDateTime ldt) { - return ldt; - } - } - - @Repository - @Profile(Profiles.HSQL_DB) - public static class TimestampJdbcMealRepository extends JdbcMealRepository { - public TimestampJdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { - super(jdbcTemplate, namedParameterJdbcTemplate); - } - - @Override - protected Timestamp toDbDateTime(LocalDateTime ldt) { - return Timestamp.valueOf(ldt); - } - } - @Override public Meal save(Meal meal, int userId) { MapSqlParameterSource map = new MapSqlParameterSource() .addValue("id", meal.getId()) .addValue("description", meal.getDescription()) .addValue("calories", meal.getCalories()) - .addValue("date_time", toDbDateTime(meal.getDateTime())) + .addValue("date_time", meal.getDateTime()) .addValue("user_id", userId); if (meal.isNew()) { @@ -109,6 +79,6 @@ public List getAll(int userId) { public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { return jdbcTemplate.query( "SELECT * FROM meal WHERE user_id=? AND date_time >= ? AND date_time < ? ORDER BY date_time DESC", - ROW_MAPPER, userId, toDbDateTime(startDateTime), toDbDateTime(endDateTime)); + ROW_MAPPER, userId, startDateTime, endDateTime); } } From fc74e5d0407a8f01b13340a507fbc7700ca5358f Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 9 Mar 2023 14:27:34 +0700 Subject: [PATCH 052/125] apply patch 6_06 --- .../ru/javawebinar/topjava/model/User.java | 8 ++++++++ .../topjava/repository/MealRepository.java | 4 ++++ .../topjava/repository/UserRepository.java | 4 ++++ .../datajpa/CrudMealRepository.java | 3 +++ .../datajpa/CrudUserRepository.java | 3 +++ .../datajpa/DataJpaMealRepository.java | 5 +++++ .../datajpa/DataJpaUserRepository.java | 5 +++++ .../topjava/service/MealService.java | 4 ++++ .../topjava/service/UserService.java | 4 ++++ .../ru/javawebinar/topjava/UserTestData.java | 2 +- .../service/AbstractMealServiceTest.java | 2 +- .../service/AbstractUserServiceTest.java | 2 +- .../datajpa/DataJpaMealServiceTest.java | 19 ++++++++++++++++++ .../datajpa/DataJpaUserServiceTest.java | 20 +++++++++++++++++++ 14 files changed, 82 insertions(+), 3 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index e6233f3ded37..7f23d8c8e33a 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -54,6 +54,10 @@ public class User extends AbstractNamedEntity { @Range(min = 10, max = 10000) private int caloriesPerDay = DEFAULT_CALORIES_PER_DAY; + @OneToMany(fetch = FetchType.LAZY, mappedBy = "user") + @OrderBy("dateTime DESC") + private List meals; + public User() { } @@ -123,6 +127,10 @@ public String getPassword() { return password; } + public List getMeals() { + return meals; + } + @Override public String toString() { return "User{" + diff --git a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java index 9461d5f9f693..1ad7f8d943d7 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java @@ -20,4 +20,8 @@ public interface MealRepository { // ORDERED dateTime desc List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId); + + default Meal getWithUser(int id, int userId) { + throw new UnsupportedOperationException(); + } } diff --git a/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java index 138369789175..9fecbddaaa19 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java @@ -18,4 +18,8 @@ public interface UserRepository { User getByEmail(String email); List getAll(); + + default User getWithMeals(int id) { + throw new UnsupportedOperationException(); + } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java index 1f8682741b20..9aeef134f236 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java @@ -23,4 +23,7 @@ public interface CrudMealRepository extends JpaRepository { @Query("SELECT m from Meal m WHERE m.user.id=:userId AND m.dateTime >= :startDate AND m.dateTime < :endDate ORDER BY m.dateTime DESC") List getBetweenHalfOpen(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate, @Param("userId") int userId); + + @Query("SELECT m FROM Meal m JOIN FETCH m.user WHERE m.id = ?1 and m.user.id = ?2") + Meal getWithUser(int id, int userId); } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java index 24c42a814220..8955fc4127b9 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java @@ -16,4 +16,7 @@ public interface CrudUserRepository extends JpaRepository { int delete(@Param("id") int id); User getByEmail(String email); + + @Query("SELECT u FROM User u LEFT JOIN FETCH u.meals WHERE u.id = ?1") + User getWithMeals(int id); } diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java index c988d66a9cb1..b5f4e3eebfdd 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java @@ -50,4 +50,9 @@ public List getAll(int userId) { public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { return crudMealRepository.getBetweenHalfOpen(startDateTime, endDateTime, userId); } + + @Override + public Meal getWithUser(int id, int userId) { + return crudMealRepository.getWithUser(id, userId); + } } diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java index bc240d2d5a90..608c855e0a05 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java @@ -41,4 +41,9 @@ public User getByEmail(String email) { public List getAll() { return crudRepository.findAll(SORT_NAME_EMAIL); } + + @Override + public User getWithMeals(int id) { + return crudRepository.getWithMeals(id); + } } diff --git a/src/main/java/ru/javawebinar/topjava/service/MealService.java b/src/main/java/ru/javawebinar/topjava/service/MealService.java index df874378eb82..5e08c9e5a3dc 100644 --- a/src/main/java/ru/javawebinar/topjava/service/MealService.java +++ b/src/main/java/ru/javawebinar/topjava/service/MealService.java @@ -47,4 +47,8 @@ public Meal create(Meal meal, int userId) { Assert.notNull(meal, "meal must not be null"); return repository.save(meal, userId); } + + public Meal getWithUser(int id, int userId) { + return checkNotFoundWithId(repository.getWithUser(id, userId), id); + } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/service/UserService.java b/src/main/java/ru/javawebinar/topjava/service/UserService.java index e0979b7ee4fd..2fa248762d62 100644 --- a/src/main/java/ru/javawebinar/topjava/service/UserService.java +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -51,4 +51,8 @@ public void update(User user) { Assert.notNull(user, "user must not be null"); checkNotFoundWithId(repository.save(user), user.id()); } + + public User getWithMeals(int id) { + return checkNotFoundWithId(repository.getWithMeals(id), id); + } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java index bfaba979da6c..3419c521532a 100644 --- a/src/test/java/ru/javawebinar/topjava/UserTestData.java +++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java @@ -9,7 +9,7 @@ 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 MatcherFactory.Matcher USER_MATCHER = MatcherFactory.usingIgnoringFieldsComparator("registered", "roles", "meals"); public static final int USER_ID = START_SEQ; public static final int ADMIN_ID = START_SEQ + 1; diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java index 0637bd205af5..962323b8fdc6 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java @@ -18,7 +18,7 @@ public abstract class AbstractMealServiceTest extends AbstractServiceTest { @Autowired - private MealService service; + protected MealService service; @Test public void delete() { diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java index 3d1025de9d23..aee0a93f43f6 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -18,7 +18,7 @@ public abstract class AbstractUserServiceTest extends AbstractServiceTest { @Autowired - private UserService service; + protected UserService service; @Autowired private CacheManager cacheManager; diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java index e28cc6d6bc53..83cbd7cc25e0 100644 --- a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java @@ -1,10 +1,29 @@ package ru.javawebinar.topjava.service.datajpa; +import org.junit.Assert; +import org.junit.Test; import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.MealTestData; +import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.service.AbstractMealServiceTest; +import ru.javawebinar.topjava.util.exception.NotFoundException; +import static ru.javawebinar.topjava.MealTestData.*; import static ru.javawebinar.topjava.Profiles.DATAJPA; +import static ru.javawebinar.topjava.UserTestData.*; @ActiveProfiles(DATAJPA) public class DataJpaMealServiceTest extends AbstractMealServiceTest { + @Test + public void getWithUser() { + Meal adminMeal = service.getWithUser(ADMIN_MEAL_ID, ADMIN_ID); + MEAL_MATCHER.assertMatch(adminMeal, adminMeal1); + USER_MATCHER.assertMatch(adminMeal.getUser(), admin); + } + + @Test + public void getWithUserNotFound() { + Assert.assertThrows(NotFoundException.class, + () -> service.getWithUser(MealTestData.NOT_FOUND, ADMIN_ID)); + } } diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java index a0757e739339..7733b7bfc784 100644 --- a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java @@ -1,10 +1,30 @@ package ru.javawebinar.topjava.service.datajpa; +import org.junit.Assert; +import org.junit.Test; import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.MealTestData; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.service.AbstractUserServiceTest; +import ru.javawebinar.topjava.util.exception.NotFoundException; +import static ru.javawebinar.topjava.MealTestData.MEAL_MATCHER; import static ru.javawebinar.topjava.Profiles.DATAJPA; +import static ru.javawebinar.topjava.UserTestData.*; @ActiveProfiles(DATAJPA) public class DataJpaUserServiceTest extends AbstractUserServiceTest { + @Test + public void getWithMeals() { + User user = service.getWithMeals(USER_ID); + USER_MATCHER.assertMatch(user, UserTestData.user); + MEAL_MATCHER.assertMatch(user.getMeals(), MealTestData.meals); + } + + @Test + public void getWithMealsNotFound() { + Assert.assertThrows(NotFoundException.class, + () -> service.getWithMeals(NOT_FOUND)); + } } \ No newline at end of file From b61b6939988bf480de5b566f79adb6252a45bb54 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 9 Mar 2023 14:27:45 +0700 Subject: [PATCH 053/125] apply patch 6_07 --- src/main/java/ru/javawebinar/topjava/model/User.java | 5 ++++- .../topjava/repository/datajpa/CrudUserRepository.java | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index 7f23d8c8e33a..f63bd100ff2a 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -1,5 +1,6 @@ package ru.javawebinar.topjava.model; +import org.hibernate.annotations.BatchSize; import org.hibernate.validator.constraints.Range; import org.springframework.util.CollectionUtils; @@ -15,7 +16,7 @@ @NamedQueries({ @NamedQuery(name = User.DELETE, query = "DELETE FROM User u WHERE u.id=:id"), @NamedQuery(name = User.BY_EMAIL, query = "SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.email=?1"), - @NamedQuery(name = User.ALL_SORTED, query = "SELECT u FROM User u LEFT JOIN FETCH u.roles ORDER BY u.name, u.email"), + @NamedQuery(name = User.ALL_SORTED, query = "SELECT u FROM User u ORDER BY u.name, u.email"), }) @Entity @Table(name = "users") @@ -48,6 +49,8 @@ public class User extends AbstractNamedEntity { uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "role"}, name = "uk_user_role")}) @Column(name = "role") @ElementCollection(fetch = FetchType.EAGER) +// @Fetch(FetchMode.SUBSELECT) + @BatchSize(size = 200) private Set roles; @Column(name = "calories_per_day", nullable = false, columnDefinition = "int default 2000") diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java index 8955fc4127b9..806884a9c90b 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java @@ -1,5 +1,6 @@ package ru.javawebinar.topjava.repository.datajpa; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -11,12 +12,12 @@ public interface CrudUserRepository extends JpaRepository { @Transactional @Modifying -// @Query(name = User.DELETE) @Query("DELETE FROM User u WHERE u.id=:id") int delete(@Param("id") int id); User getByEmail(String email); - @Query("SELECT u FROM User u LEFT JOIN FETCH u.meals WHERE u.id = ?1") + @EntityGraph(attributePaths = {"meals", "roles"}) + @Query("SELECT u FROM User u WHERE u.id=?1") User getWithMeals(int id); } From 9b84d1ff60d71b63f36ff8dc7fc8948025fddf40 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 9 Mar 2023 15:51:14 +0700 Subject: [PATCH 054/125] apply patch 6_08 --- .../javawebinar/topjava/util/ValidationUtil.java | 9 +++++++++ .../topjava/service/AbstractMealServiceTest.java | 10 ++++++++++ .../topjava/service/AbstractServiceTest.java | 15 +++++++++++++++ .../topjava/service/AbstractUserServiceTest.java | 12 ++++++++++++ 4 files changed, 46 insertions(+) diff --git a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java index 5212eea75062..986c7d97df5f 100644 --- a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java @@ -1,6 +1,8 @@ package ru.javawebinar.topjava.util; +import org.springframework.core.NestedExceptionUtils; +import org.springframework.lang.NonNull; import ru.javawebinar.topjava.model.AbstractBaseEntity; import ru.javawebinar.topjava.util.exception.NotFoundException; @@ -43,4 +45,11 @@ public static void assureIdConsistent(AbstractBaseEntity entity, int id) { throw new IllegalArgumentException(entity + " must be with id=" + id); } } + + // https://stackoverflow.com/a/65442410/548473 + @NonNull + public static Throwable getRootCause(@NonNull Throwable t) { + Throwable rootCause = NestedExceptionUtils.getRootCause(t); + return rootCause != null ? rootCause : t; + } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java index 962323b8fdc6..b19ffabe8e09 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java @@ -7,9 +7,11 @@ import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.util.exception.NotFoundException; +import javax.validation.ConstraintViolationException; import java.time.LocalDate; import java.time.Month; +import static java.time.LocalDateTime.of; import static org.junit.Assert.assertThrows; import static ru.javawebinar.topjava.MealTestData.*; import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; @@ -99,4 +101,12 @@ public void getBetweenInclusive() { public void getBetweenWithNullDates() { MEAL_MATCHER.assertMatch(service.getBetweenInclusive(null, null, USER_ID), meals); } + + @Test + public void createWithException() throws Exception { + validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), " ", 300), USER_ID)); + validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, null, "Description", 300), USER_ID)); + validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Description", 9), USER_ID)); + validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Description", 5001), USER_ID)); + } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java index 036d28c82434..06d99de9734e 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java @@ -13,6 +13,9 @@ import ru.javawebinar.topjava.ActiveDbProfileResolver; import ru.javawebinar.topjava.TimingRules; +import static org.junit.Assert.assertThrows; +import static ru.javawebinar.topjava.util.ValidationUtil.getRootCause; + @ContextConfiguration({ "classpath:spring/spring-app.xml", "classpath:spring/spring-db.xml" @@ -21,9 +24,21 @@ @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) @ActiveProfiles(resolver = ActiveDbProfileResolver.class) public abstract class AbstractServiceTest { + @ClassRule public static ExternalResource summary = TimingRules.SUMMARY; @Rule public Stopwatch stopwatch = TimingRules.STOPWATCH; + + // Check root cause in JUnit: https://github.com/junit-team/junit4/pull/778 + protected void validateRootCause(Class rootExceptionClass, Runnable runnable) { + assertThrows(rootExceptionClass, () -> { + try { + runnable.run(); + } catch (Exception e) { + throw getRootCause(e); + } + }); + } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java index aee0a93f43f6..ccc83f9318fa 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -10,7 +10,10 @@ import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.util.exception.NotFoundException; +import javax.validation.ConstraintViolationException; +import java.util.Date; import java.util.List; +import java.util.Set; import static org.junit.Assert.assertThrows; import static ru.javawebinar.topjava.UserTestData.*; @@ -84,4 +87,13 @@ public void getAll() { List all = service.getAll(); USER_MATCHER.assertMatch(all, admin, guest, user); } + + @Test + public void createWithException() throws Exception { + validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, " ", "mail@yandex.ru", "password", Role.USER))); + validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", " ", "password", Role.USER))); + validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", "mail@yandex.ru", " ", Role.USER))); + validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", "mail@yandex.ru", "password", 9, true, new Date(), Set.of()))); + validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", "mail@yandex.ru", "password", 10001, true, new Date(), Set.of()))); + } } \ No newline at end of file From 8812d79d7f04a4bdfea4af465285dba1cfea3523 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Fri, 10 Mar 2023 23:14:37 +0700 Subject: [PATCH 055/125] apply patch 6_09 --- pom.xml | 5 +++++ .../ru/javawebinar/topjava/model/User.java | 4 ++++ .../topjava/repository/JpaUtil.java | 21 +++++++++++++++++++ src/main/resources/spring/spring-db.xml | 8 +++++++ .../service/AbstractUserServiceTest.java | 5 +++++ .../service/jdbc/JdbcMealServiceTest.java | 2 ++ .../service/jdbc/JdbcUserServiceTest.java | 2 ++ 7 files changed, 47 insertions(+) create mode 100644 src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java diff --git a/pom.xml b/pom.xml index 707bb97d622b..0a6586a26f27 100644 --- a/pom.xml +++ b/pom.xml @@ -110,6 +110,11 @@ hibernate-validator ${hibernate-validator.version} + + org.hibernate + hibernate-jcache + ${hibernate.version} + diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index f63bd100ff2a..fb6a511c599f 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -1,6 +1,8 @@ package ru.javawebinar.topjava.model; import org.hibernate.annotations.BatchSize; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.validator.constraints.Range; import org.springframework.util.CollectionUtils; @@ -13,6 +15,7 @@ import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY; +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @NamedQueries({ @NamedQuery(name = User.DELETE, query = "DELETE FROM User u WHERE u.id=:id"), @NamedQuery(name = User.BY_EMAIL, query = "SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.email=?1"), @@ -44,6 +47,7 @@ public class User extends AbstractNamedEntity { @NotNull private Date registered = new Date(); + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @Enumerated(EnumType.STRING) @CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"), uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "role"}, name = "uk_user_role")}) diff --git a/src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java b/src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java new file mode 100644 index 000000000000..f3e51ad50fbb --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java @@ -0,0 +1,21 @@ +package ru.javawebinar.topjava.repository; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +public class JpaUtil { + + @PersistenceContext + private EntityManager em; + + public void clear2ndLevelHibernateCache() { + Session s = (Session) em.getDelegate(); + SessionFactory sf = s.getSessionFactory(); +// sf.getCache().evictEntityData(User.class, AbstractBaseEntity.START_SEQ); +// sf.getCache().evictEntityData(User.class); + sf.getCache().evictAllRegions(); + } +} diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 7a334acebd47..b601f31fac5c 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -73,6 +73,12 @@ + + + + + + @@ -87,6 +93,8 @@ + + diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java index ccc83f9318fa..1ebd08f87643 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -14,6 +14,7 @@ import java.util.Date; import java.util.List; import java.util.Set; +import ru.javawebinar.topjava.repository.JpaUtil; import static org.junit.Assert.assertThrows; import static ru.javawebinar.topjava.UserTestData.*; @@ -26,9 +27,13 @@ public abstract class AbstractUserServiceTest extends AbstractServiceTest { @Autowired private CacheManager cacheManager; + @Autowired + protected JpaUtil jpaUtil; + @Before public void setup() { cacheManager.getCache("users").clear(); + jpaUtil.clear2ndLevelHibernateCache(); } @Test diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java index 9ff4ae615b2b..802ca22599ef 100644 --- a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java @@ -1,10 +1,12 @@ package ru.javawebinar.topjava.service.jdbc; +import org.junit.Ignore; import org.springframework.test.context.ActiveProfiles; import ru.javawebinar.topjava.service.AbstractMealServiceTest; import static ru.javawebinar.topjava.Profiles.JDBC; @ActiveProfiles(JDBC) +@Ignore public class JdbcMealServiceTest extends AbstractMealServiceTest { } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java index 419f68ed1098..477b92cae517 100644 --- a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java @@ -1,10 +1,12 @@ package ru.javawebinar.topjava.service.jdbc; +import org.junit.Ignore; import org.springframework.test.context.ActiveProfiles; import ru.javawebinar.topjava.service.AbstractUserServiceTest; import static ru.javawebinar.topjava.Profiles.JDBC; @ActiveProfiles(JDBC) +@Ignore public class JdbcUserServiceTest extends AbstractUserServiceTest { } \ No newline at end of file From d3712f323bf51f7844a1ec3182dfd40243615f8f Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Fri, 10 Mar 2023 23:43:35 +0700 Subject: [PATCH 056/125] apply patch 6_10 --- .../topjava/model/AbstractBaseEntity.java | 1 + .../java/ru/javawebinar/topjava/model/Meal.java | 3 +++ .../java/ru/javawebinar/topjava/model/User.java | 13 ++++++++++--- src/main/resources/spring/spring-db.xml | 6 ++++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java index 2ab30f34f272..acb11612281c 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -14,6 +14,7 @@ public abstract class AbstractBaseEntity implements Persistable { @Id @SequenceGenerator(name = "global_seq", sequenceName = "global_seq", allocationSize = 1, initialValue = START_SEQ) + // @Column(name = "id", unique = true, nullable = false, columnDefinition = "integer default nextval('global_seq')") @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq") // See https://hibernate.atlassian.net/browse/HHH-3718 and https://hibernate.atlassian.net/browse/HHH-12034 // Proxy initialization when accessing its identifier managed now by JPA_PROXY_COMPLIANCE setting diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index 5e61c4525a03..c579bf169b6e 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -1,5 +1,7 @@ package ru.javawebinar.topjava.model; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.validator.constraints.Range; import javax.persistence.*; @@ -42,6 +44,7 @@ public class Meal extends AbstractBaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) @NotNull private User user; diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index fb6a511c599f..0b4d40a8f2e2 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -1,11 +1,15 @@ package ru.javawebinar.topjava.model; -import org.hibernate.annotations.BatchSize; import org.hibernate.annotations.Cache; -import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.*; import org.hibernate.validator.constraints.Range; import org.springframework.util.CollectionUtils; +import javax.persistence.Entity; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OrderBy; +import javax.persistence.Table; import javax.persistence.*; import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; @@ -55,14 +59,17 @@ public class User extends AbstractNamedEntity { @ElementCollection(fetch = FetchType.EAGER) // @Fetch(FetchMode.SUBSELECT) @BatchSize(size = 200) + @JoinColumn + @OnDelete(action = OnDeleteAction.CASCADE) private Set roles; @Column(name = "calories_per_day", nullable = false, columnDefinition = "int default 2000") @Range(min = 10, max = 10000) private int caloriesPerDay = DEFAULT_CALORIES_PER_DAY; - @OneToMany(fetch = FetchType.LAZY, mappedBy = "user") + @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")//, cascade = CascadeType.REMOVE, orphanRemoval = true) @OrderBy("dateTime DESC") + @OnDelete(action = OnDeleteAction.CASCADE) //https://stackoverflow.com/a/44988100/548473 private List meals; public User() { diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index b601f31fac5c..29211c981083 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -79,6 +79,12 @@ + From fe36ae320d8b40a15ace784fda2d81d3eaea329e Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 11 Mar 2023 16:18:54 +0700 Subject: [PATCH 057/125] apply patch 6_11 --- pom.xml | 11 ++++++++--- .../ru/javawebinar/topjava/web/MealServlet.java | 16 +++------------- .../ru/javawebinar/topjava/web/UserServlet.java | 14 +++++++++++++- src/main/webapp/WEB-INF/web.xml | 16 ++++++++++++++++ 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index 0a6586a26f27..ebf90ec1853d 100644 --- a/pom.xml +++ b/pom.xml @@ -152,9 +152,9 @@ - javax.servlet - javax.servlet-api - 4.0.1 + org.apache.tomcat + tomcat-servlet-api + ${tomcat.version} provided @@ -164,6 +164,11 @@ 1.2 + + org.springframework + spring-web + + junit diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java index 62909cd33d4e..e412a25dcc89 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -1,8 +1,8 @@ package ru.javawebinar.topjava.web; -import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.util.StringUtils; -import ru.javawebinar.topjava.Profiles; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.web.meal.MealRestController; @@ -22,24 +22,14 @@ public class MealServlet extends HttpServlet { - private ClassPathXmlApplicationContext springContext; private MealRestController mealController; @Override public void init() { - springContext = new ClassPathXmlApplicationContext(new String[]{"spring/spring-app.xml", "spring/spring-db.xml"}, false); -// springContext.setConfigLocations("spring/spring-app.xml", "spring/spring-db.xml"); - springContext.getEnvironment().setActiveProfiles(Profiles.getActiveDbProfile(), Profiles.REPOSITORY_IMPLEMENTATION); - springContext.refresh(); + WebApplicationContext springContext = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); mealController = springContext.getBean(MealRestController.class); } - @Override - public void destroy() { - springContext.close(); - super.destroy(); - } - @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { request.setCharacterEncoding("UTF-8"); diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java index 226023400c70..6276ac82c27d 100644 --- a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java @@ -1,6 +1,9 @@ package ru.javawebinar.topjava.web; import org.slf4j.Logger; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; +import ru.javawebinar.topjava.web.user.AdminRestController; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -13,6 +16,14 @@ public class UserServlet extends HttpServlet { private static final Logger log = getLogger(UserServlet.class); + private AdminRestController adminController; + + @Override + public void init() throws ServletException { + WebApplicationContext springContext = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); + adminController = springContext.getBean(AdminRestController.class); + } + @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int userId = Integer.parseInt(request.getParameter("userId")); @@ -22,7 +33,8 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - log.debug("forward to users"); + log.debug("getAll"); + request.setAttribute("users", adminController.getAll()); request.getRequestDispatcher("/users.jsp").forward(request, response); } } diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 0ac5a7610ed4..df132d4036b2 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -6,6 +6,22 @@ TopJava + + spring.profiles.default + postgres,datajpa + + + + contextConfigLocation + + classpath:spring/spring-app.xml + classpath:spring/spring-db.xml + + + + + org.springframework.web.context.ContextLoaderListener + userServlet ru.javawebinar.topjava.web.UserServlet From 97e68da567e4c6c0a6194c51927905edbcbcade3 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 11 Mar 2023 16:37:10 +0700 Subject: [PATCH 058/125] apply patch 6_12 --- src/main/resources/messages/app.properties | 12 ++++++ src/main/resources/messages/app_ru.properties | 12 ++++++ src/main/webapp/css/style.css | 8 ++++ src/main/webapp/fragments/bodyHeader.jsp | 7 ++++ src/main/webapp/fragments/footer.jsp | 5 +++ src/main/webapp/fragments/headTag.jsp | 10 +++++ src/main/webapp/index.html | 18 -------- src/main/webapp/index.jsp | 23 ++++++++++ src/main/webapp/meals.jsp | 2 +- src/main/webapp/users.jsp | 42 +++++++++++++++---- 10 files changed, 113 insertions(+), 26 deletions(-) create mode 100644 src/main/resources/messages/app.properties create mode 100644 src/main/resources/messages/app_ru.properties create mode 100644 src/main/webapp/fragments/bodyHeader.jsp create mode 100644 src/main/webapp/fragments/footer.jsp create mode 100644 src/main/webapp/fragments/headTag.jsp delete mode 100644 src/main/webapp/index.html create mode 100644 src/main/webapp/index.jsp diff --git a/src/main/resources/messages/app.properties b/src/main/resources/messages/app.properties new file mode 100644 index 000000000000..ccd4c66b5397 --- /dev/null +++ b/src/main/resources/messages/app.properties @@ -0,0 +1,12 @@ +app.title=Calories management +app.home=Home +app.footer=Internship Spring 5/JPA Enterprise (Topjava) application +app.login=Login as +user.title=Users +user.name=Name +user.email=Email +user.roles=Roles +user.active=Active +user.registered=Registered +meal.title=Meals +common.select=Select \ No newline at end of file diff --git a/src/main/resources/messages/app_ru.properties b/src/main/resources/messages/app_ru.properties new file mode 100644 index 000000000000..c50d3594bef4 --- /dev/null +++ b/src/main/resources/messages/app_ru.properties @@ -0,0 +1,12 @@ +app.title=??????? ??????? +app.home=??????? +app.footer=?????????? ?????????? Spring 5/JPA Enterprise (Topjava) +app.login=????? ??? +user.title=???????????? +user.name=??? +user.email=????? +user.roles=???? +user.active=???????? +user.registered=??????????????? +meal.title=??? ??? +common.select=??????? \ No newline at end of file diff --git a/src/main/webapp/css/style.css b/src/main/webapp/css/style.css index cfffdcb53359..a55147510899 100644 --- a/src/main/webapp/css/style.css +++ b/src/main/webapp/css/style.css @@ -22,3 +22,11 @@ tr[data-meal-excess="false"] { tr[data-meal-excess="true"] { color: red; } + +header, footer { + background: none repeat scroll 0 0 #A6C9E2; + color: #2E6E9E; + font-size: 20px; + padding: 5px 20px; + margin: 6px 0; +} diff --git a/src/main/webapp/fragments/bodyHeader.jsp b/src/main/webapp/fragments/bodyHeader.jsp new file mode 100644 index 000000000000..345cc15a9429 --- /dev/null +++ b/src/main/webapp/fragments/bodyHeader.jsp @@ -0,0 +1,7 @@ +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + +

    + | | +
    \ No newline at end of file diff --git a/src/main/webapp/fragments/footer.jsp b/src/main/webapp/fragments/footer.jsp new file mode 100644 index 000000000000..c59bff2e54bf --- /dev/null +++ b/src/main/webapp/fragments/footer.jsp @@ -0,0 +1,5 @@ +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + +
    +
    \ No newline at end of file diff --git a/src/main/webapp/fragments/headTag.jsp b/src/main/webapp/fragments/headTag.jsp new file mode 100644 index 000000000000..c1532783af5e --- /dev/null +++ b/src/main/webapp/fragments/headTag.jsp @@ -0,0 +1,10 @@ +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %> + + + + + <fmt:message key="app.title"/> + + \ No newline at end of file diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html deleted file mode 100644 index 57d710a81abb..000000000000 --- a/src/main/webapp/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - Java Enterprise (Topjava) - - -

    Проект Java Enterprise (Topjava)

    -
    -
    - Meals of  - - -
    - - diff --git a/src/main/webapp/index.jsp b/src/main/webapp/index.jsp new file mode 100644 index 000000000000..654d7b2385ae --- /dev/null +++ b/src/main/webapp/index.jsp @@ -0,0 +1,23 @@ +<%@ 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" %> + + + + + + + +
    +
    +
    + : + + +
    + + + \ No newline at end of file diff --git a/src/main/webapp/meals.jsp b/src/main/webapp/meals.jsp index 7d9bf3e425b2..0074532217bf 100644 --- a/src/main/webapp/meals.jsp +++ b/src/main/webapp/meals.jsp @@ -9,7 +9,7 @@
    -

    Home

    +

    Home


    Meals

    diff --git a/src/main/webapp/users.jsp b/src/main/webapp/users.jsp index 650c8dda479c..454accf84d5c 100644 --- a/src/main/webapp/users.jsp +++ b/src/main/webapp/users.jsp @@ -1,11 +1,39 @@ <%@ page contentType="text/html;charset=UTF-8" %> - - - Users - +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + + -

    Home

    -
    -

    Users

    + + +
    +

    + + + + + + + + + + + + + + + + + + + + + +
    ${user.email}${user.roles}${user.enabled}
    +
    + \ No newline at end of file From 0f07c3d5720c3f473357595cc01dbf4c8511d923 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 11 Mar 2023 17:52:17 +0700 Subject: [PATCH 059/125] apply patch 6_13 --- pom.xml | 42 ++++++++++++++ src/main/resources/db/tomcat.properties | 5 ++ src/main/resources/messages/app_ru.properties | 24 ++++---- src/main/resources/spring/spring-db.xml | 9 ++- src/main/resources/tomcat/context.xml | 57 +++++++++++++++++++ 5 files changed, 124 insertions(+), 13 deletions(-) create mode 100644 src/main/resources/db/tomcat.properties create mode 100644 src/main/resources/tomcat/context.xml diff --git a/pom.xml b/pom.xml index ebf90ec1853d..25862acd42fc 100644 --- a/pom.xml +++ b/pom.xml @@ -64,6 +64,48 @@ -Dfile.encoding=UTF-8 + + + + + org.codehaus.cargo + cargo-maven3-plugin + 1.10.5 + + + tomcat9x + + UTF-8 + tomcat,datajpa + + + + org.postgresql + postgresql + + + + + + + src/main/resources/tomcat/context.xml + conf/Catalina/localhost/ + ${project.build.finalName}.xml + + + + + + ru.javawebinar + topjava + war + + ${project.build.finalName} + + + + + diff --git a/src/main/resources/db/tomcat.properties b/src/main/resources/db/tomcat.properties new file mode 100644 index 000000000000..2e073681ad16 --- /dev/null +++ b/src/main/resources/db/tomcat.properties @@ -0,0 +1,5 @@ +database.init=false +jdbc.initLocation=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/messages/app_ru.properties b/src/main/resources/messages/app_ru.properties index c50d3594bef4..a9760be8d75d 100644 --- a/src/main/resources/messages/app_ru.properties +++ b/src/main/resources/messages/app_ru.properties @@ -1,12 +1,12 @@ -app.title=??????? ??????? -app.home=??????? -app.footer=?????????? ?????????? Spring 5/JPA Enterprise (Topjava) -app.login=????? ??? -user.title=???????????? -user.name=??? -user.email=????? -user.roles=???? -user.active=???????? -user.registered=??????????????? -meal.title=??? ??? -common.select=??????? \ No newline at end of file +app.title=Подсчет калорий +app.home=Главная +app.footer=Приложение стажировки Spring 5/JPA Enterprise (Topjava) +app.login=Зайти как +user.title=Пользователи +user.name=Имя +user.email=Почта +user.roles=Роли +user.active=Активный +user.registered=Зарегистрирован +meal.title=Моя еда +common.select=Выбрать diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 29211c981083..2b62f074828b 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -5,11 +5,13 @@ xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jpa="http://www.springframework.org/schema/data/jpa" + xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd - http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd"> + http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd + http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd"> @@ -50,6 +52,11 @@ p:password="${database.password}"/> + + + + + diff --git a/src/main/resources/tomcat/context.xml b/src/main/resources/tomcat/context.xml new file mode 100644 index 000000000000..9311d5904aea --- /dev/null +++ b/src/main/resources/tomcat/context.xml @@ -0,0 +1,57 @@ + + + + + + + + WEB-INF/web.xml + ${catalina.base}/conf/web.xml + + + + + + + + + From bcb9978f7c409fac7efde79be40ff685a54d5e86 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 12 Mar 2023 15:20:30 +0700 Subject: [PATCH 060/125] apply patch 6_14 --- pom.xml | 2 +- .../topjava/web/RootController.java | 41 +++++++++++++++++++ .../javawebinar/topjava/web/UserServlet.java | 40 ------------------ src/main/resources/logback.xml | 1 + src/main/resources/spring/spring-app.xml | 3 -- src/main/resources/spring/spring-mvc.xml | 18 ++++++++ .../jsp}/fragments/bodyHeader.jsp | 2 +- .../{ => WEB-INF/jsp}/fragments/footer.jsp | 0 .../{ => WEB-INF/jsp}/fragments/headTag.jsp | 2 +- src/main/webapp/{ => WEB-INF/jsp}/index.jsp | 0 .../webapp/{ => WEB-INF/jsp}/mealForm.jsp | 0 src/main/webapp/{ => WEB-INF/jsp}/meals.jsp | 0 src/main/webapp/{ => WEB-INF/jsp}/users.jsp | 0 src/main/webapp/WEB-INF/web.xml | 25 +++++------ src/main/webapp/{ => resources}/css/style.css | 0 15 files changed, 73 insertions(+), 61 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/web/RootController.java delete mode 100644 src/main/java/ru/javawebinar/topjava/web/UserServlet.java create mode 100644 src/main/resources/spring/spring-mvc.xml rename src/main/webapp/{ => WEB-INF/jsp}/fragments/bodyHeader.jsp (66%) rename src/main/webapp/{ => WEB-INF/jsp}/fragments/footer.jsp (100%) rename src/main/webapp/{ => WEB-INF/jsp}/fragments/headTag.jsp (85%) rename src/main/webapp/{ => WEB-INF/jsp}/index.jsp (100%) rename src/main/webapp/{ => WEB-INF/jsp}/mealForm.jsp (100%) rename src/main/webapp/{ => WEB-INF/jsp}/meals.jsp (100%) rename src/main/webapp/{ => WEB-INF/jsp}/users.jsp (100%) rename src/main/webapp/{ => resources}/css/style.css (100%) diff --git a/pom.xml b/pom.xml index 25862acd42fc..1c2a4f78da9c 100644 --- a/pom.xml +++ b/pom.xml @@ -208,7 +208,7 @@ org.springframework - spring-web + spring-webmvc diff --git a/src/main/java/ru/javawebinar/topjava/web/RootController.java b/src/main/java/ru/javawebinar/topjava/web/RootController.java new file mode 100644 index 000000000000..f432d93b1fdf --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/RootController.java @@ -0,0 +1,41 @@ +package ru.javawebinar.topjava.web; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import ru.javawebinar.topjava.service.UserService; + +import javax.servlet.http.HttpServletRequest; + +@Controller +public class RootController { + private static final Logger log = LoggerFactory.getLogger(RootController.class); + + @Autowired + private UserService service; + + @GetMapping("/") + public String root() { + log.info("root"); + return "index"; + } + + @GetMapping("/users") + public String getUsers(Model model) { + log.info("users"); + model.addAttribute("users", service.getAll()); + return "users"; + } + + @PostMapping("/users") + public String setUser(HttpServletRequest request) { + int userId = Integer.parseInt(request.getParameter("userId")); + log.info("setUser {}", userId); + SecurityUtil.setAuthUserId(userId); + return "redirect:meals"; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java deleted file mode 100644 index 6276ac82c27d..000000000000 --- a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java +++ /dev/null @@ -1,40 +0,0 @@ -package ru.javawebinar.topjava.web; - -import org.slf4j.Logger; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.context.support.WebApplicationContextUtils; -import ru.javawebinar.topjava.web.user.AdminRestController; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -import static org.slf4j.LoggerFactory.getLogger; - -public class UserServlet extends HttpServlet { - private static final Logger log = getLogger(UserServlet.class); - - private AdminRestController adminController; - - @Override - public void init() throws ServletException { - WebApplicationContext springContext = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); - adminController = springContext.getBean(AdminRestController.class); - } - - @Override - protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - int userId = Integer.parseInt(request.getParameter("userId")); - SecurityUtil.setAuthUserId(userId); - response.sendRedirect("meals"); - } - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - log.debug("getAll"); - request.setAttribute("users", adminController.getAll()); - request.getRequestDispatcher("/users.jsp").forward(request, response); - } -} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index d5fbdd95db91..1d27e3f1ea2b 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -21,6 +21,7 @@ + diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml index d6c643e972e1..d57b656aeb75 100644 --- a/src/main/resources/spring/spring-app.xml +++ b/src/main/resources/spring/spring-app.xml @@ -12,7 +12,4 @@ - - - \ No newline at end of file diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml new file mode 100644 index 000000000000..7c71d5af5939 --- /dev/null +++ b/src/main/resources/spring/spring-mvc.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/fragments/bodyHeader.jsp b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp similarity index 66% rename from src/main/webapp/fragments/bodyHeader.jsp rename to src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp index 345cc15a9429..888df90f2527 100644 --- a/src/main/webapp/fragments/bodyHeader.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp @@ -3,5 +3,5 @@
    - | | + | |
    \ No newline at end of file diff --git a/src/main/webapp/fragments/footer.jsp b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp similarity index 100% rename from src/main/webapp/fragments/footer.jsp rename to src/main/webapp/WEB-INF/jsp/fragments/footer.jsp diff --git a/src/main/webapp/fragments/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp similarity index 85% rename from src/main/webapp/fragments/headTag.jsp rename to src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp index c1532783af5e..da40d461d76a 100644 --- a/src/main/webapp/fragments/headTag.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp @@ -6,5 +6,5 @@ <fmt:message key="app.title"/> - + \ No newline at end of file diff --git a/src/main/webapp/index.jsp b/src/main/webapp/WEB-INF/jsp/index.jsp similarity index 100% rename from src/main/webapp/index.jsp rename to src/main/webapp/WEB-INF/jsp/index.jsp diff --git a/src/main/webapp/mealForm.jsp b/src/main/webapp/WEB-INF/jsp/mealForm.jsp similarity index 100% rename from src/main/webapp/mealForm.jsp rename to src/main/webapp/WEB-INF/jsp/mealForm.jsp diff --git a/src/main/webapp/meals.jsp b/src/main/webapp/WEB-INF/jsp/meals.jsp similarity index 100% rename from src/main/webapp/meals.jsp rename to src/main/webapp/WEB-INF/jsp/meals.jsp diff --git a/src/main/webapp/users.jsp b/src/main/webapp/WEB-INF/jsp/users.jsp similarity index 100% rename from src/main/webapp/users.jsp rename to src/main/webapp/WEB-INF/jsp/users.jsp diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index df132d4036b2..a6a4f622b4f7 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -19,26 +19,21 @@ + org.springframework.web.context.ContextLoaderListener - userServlet - ru.javawebinar.topjava.web.UserServlet - 0 + mvc-dispatcher + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + classpath:spring/spring-mvc.xml + + 1 - userServlet - /users - - - - mealServlet - ru.javawebinar.topjava.web.MealServlet - 0 - - - mealServlet - /meals + mvc-dispatcher + / diff --git a/src/main/webapp/css/style.css b/src/main/webapp/resources/css/style.css similarity index 100% rename from src/main/webapp/css/style.css rename to src/main/webapp/resources/css/style.css From e177f904c69579b0917d2f5e5e46c8075ea2b498 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 12 Mar 2023 15:50:25 +0700 Subject: [PATCH 061/125] apply patch 6_15 --- .../messages/app.properties | 0 .../messages/app_ru.properties | 0 src/main/resources/spring/spring-mvc.xml | 18 ++++++++++++++++++ .../WEB-INF/jsp/fragments/bodyHeader.jsp | 5 ++--- .../webapp/WEB-INF/jsp/fragments/footer.jsp | 5 ++--- .../webapp/WEB-INF/jsp/fragments/headTag.jsp | 5 ++--- src/main/webapp/WEB-INF/jsp/index.jsp | 8 +++----- src/main/webapp/WEB-INF/jsp/users.jsp | 15 +++++++-------- 8 files changed, 34 insertions(+), 22 deletions(-) rename {src/main/resources => config}/messages/app.properties (100%) rename {src/main/resources => config}/messages/app_ru.properties (100%) diff --git a/src/main/resources/messages/app.properties b/config/messages/app.properties similarity index 100% rename from src/main/resources/messages/app.properties rename to config/messages/app.properties diff --git a/src/main/resources/messages/app_ru.properties b/config/messages/app_ru.properties similarity index 100% rename from src/main/resources/messages/app_ru.properties rename to config/messages/app_ru.properties diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml index 7c71d5af5939..aa5599feaca4 100644 --- a/src/main/resources/spring/spring-mvc.xml +++ b/src/main/resources/spring/spring-mvc.xml @@ -15,4 +15,22 @@ + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp index 888df90f2527..5b5efe57e65b 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp @@ -1,7 +1,6 @@ <%@page contentType="text/html" pageEncoding="UTF-8" %> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> -
    - | | + | |
    \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp index c59bff2e54bf..0935c441a36b 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp @@ -1,5 +1,4 @@ <%@page contentType="text/html" pageEncoding="UTF-8" %> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> - +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
    -
    \ No newline at end of file +
    \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp index da40d461d76a..6d77694e3406 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp @@ -1,10 +1,9 @@ <%@page contentType="text/html" pageEncoding="UTF-8" %> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> <%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %> - - <fmt:message key="app.title"/> + <spring:message code="app.title"/> \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/index.jsp b/src/main/webapp/WEB-INF/jsp/index.jsp index 654d7b2385ae..84719196538d 100644 --- a/src/main/webapp/WEB-INF/jsp/index.jsp +++ b/src/main/webapp/WEB-INF/jsp/index.jsp @@ -1,8 +1,6 @@ <%@ 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="spring" uri="http://www.springframework.org/tags" %> @@ -11,11 +9,11 @@
    - : - +
    diff --git a/src/main/webapp/WEB-INF/jsp/users.jsp b/src/main/webapp/WEB-INF/jsp/users.jsp index 454accf84d5c..4d3d8678906c 100644 --- a/src/main/webapp/WEB-INF/jsp/users.jsp +++ b/src/main/webapp/WEB-INF/jsp/users.jsp @@ -1,8 +1,7 @@ <%@ 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="spring" uri="http://www.springframework.org/tags" %> @@ -10,16 +9,16 @@
    -

    +

    - - - - - + + + + + From bf7840da39272be3d168903d702280359f286aaa Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 18 Mar 2023 20:54:19 +0700 Subject: [PATCH 062/125] fix: eeror by removing users from matcher --- src/test/java/ru/javawebinar/topjava/MealTestData.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/ru/javawebinar/topjava/MealTestData.java b/src/test/java/ru/javawebinar/topjava/MealTestData.java index 03b594012e37..d044e3f90a22 100644 --- a/src/test/java/ru/javawebinar/topjava/MealTestData.java +++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java @@ -10,7 +10,7 @@ import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; public class MealTestData { - public static final MatcherFactory.Matcher MEAL_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(); + 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; From c9129c1af016b8caa2849d0f9b682cb7ec79fc15 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 25 Mar 2023 20:56:03 +0700 Subject: [PATCH 063/125] apply patch 7_0 --- .../topjava/service/jdbc/JdbcMealServiceTest.java | 7 ++++++- .../web/user/InMemoryAdminRestControllerSpringTest.java | 1 + .../topjava/web/user/InMemoryAdminRestControllerTest.java | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java index 802ca22599ef..996fdde1a580 100644 --- a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java @@ -1,12 +1,17 @@ package ru.javawebinar.topjava.service.jdbc; import org.junit.Ignore; +import org.junit.Test; import org.springframework.test.context.ActiveProfiles; import ru.javawebinar.topjava.service.AbstractMealServiceTest; import static ru.javawebinar.topjava.Profiles.JDBC; @ActiveProfiles(JDBC) -@Ignore public class JdbcMealServiceTest extends AbstractMealServiceTest { + @Test + @Override + @Ignore + public void createWithException() throws Exception { + } } \ 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 index 2386eeee4ff3..dd39ec6611e1 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java @@ -16,6 +16,7 @@ @ContextConfiguration({"classpath:spring/spring-app.xml", "classpath:spring/inmemory.xml"}) @RunWith(SpringRunner.class) +@Ignore public class InMemoryAdminRestControllerSpringTest { @Autowired diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java index 7cc2a833c161..eee560577c2b 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java @@ -13,6 +13,7 @@ import static ru.javawebinar.topjava.UserTestData.NOT_FOUND; import static ru.javawebinar.topjava.UserTestData.USER_ID; +@Ignore public class InMemoryAdminRestControllerTest { private static final Logger log = LoggerFactory.getLogger(InMemoryAdminRestControllerTest.class); From 9a6785285ffb109d038a86a58614b65028deb871 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 25 Mar 2023 20:56:14 +0700 Subject: [PATCH 064/125] apply patch 7_01 --- src/test/java/ru/javawebinar/topjava/SpringMain.java | 2 +- .../topjava/service/AbstractMealServiceTest.java | 2 ++ .../topjava/service/AbstractServiceTest.java | 11 +++++++++++ .../topjava/service/AbstractUserServiceTest.java | 10 ++++++++-- .../topjava/service/jdbc/JdbcMealServiceTest.java | 7 ------- .../topjava/service/jdbc/JdbcUserServiceTest.java | 2 -- .../user/InMemoryAdminRestControllerSpringTest.java | 4 +--- .../web/user/InMemoryAdminRestControllerTest.java | 7 ++++--- src/test/resources/spring/inmemory.xml | 2 ++ 9 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/test/java/ru/javawebinar/topjava/SpringMain.java b/src/test/java/ru/javawebinar/topjava/SpringMain.java index e41f1ae119be..1dda999c543a 100644 --- a/src/test/java/ru/javawebinar/topjava/SpringMain.java +++ b/src/test/java/ru/javawebinar/topjava/SpringMain.java @@ -17,7 +17,7 @@ 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")) { + try (ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext("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)); diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java index b19ffabe8e09..06061c1d818b 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java @@ -1,6 +1,7 @@ package ru.javawebinar.topjava.service; import org.junit.Assert; +import org.junit.Assume; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; @@ -104,6 +105,7 @@ public void getBetweenWithNullDates() { @Test public void createWithException() throws Exception { + Assume.assumeTrue("Validation not supported (JPA only)", isJpaBased()); validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), " ", 300), USER_ID)); validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, null, "Description", 300), USER_ID)); validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Description", 9), USER_ID)); diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java index 06d99de9734e..e4c1f5757696 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java @@ -5,12 +5,15 @@ import org.junit.rules.ExternalResource; import org.junit.rules.Stopwatch; import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; 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.Profiles; import ru.javawebinar.topjava.TimingRules; import static org.junit.Assert.assertThrows; @@ -28,9 +31,17 @@ public abstract class AbstractServiceTest { @ClassRule public static ExternalResource summary = TimingRules.SUMMARY; + @Autowired + private Environment env; + @Rule public Stopwatch stopwatch = TimingRules.STOPWATCH; + public boolean isJpaBased() { +// return Arrays.stream(env.getActiveProfiles()).noneMatch(Profiles.JDBC::equals); + return env.acceptsProfiles(org.springframework.core.env.Profiles.of(Profiles.JPA, Profiles.DATAJPA)); + } + // Check root cause in JUnit: https://github.com/junit-team/junit4/pull/778 protected void validateRootCause(Class rootExceptionClass, Runnable runnable) { assertThrows(rootExceptionClass, () -> { diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java index 1ebd08f87643..e802895aa95e 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -1,20 +1,22 @@ package ru.javawebinar.topjava.service; +import org.junit.Assume; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Lazy; import org.springframework.dao.DataAccessException; import ru.javawebinar.topjava.UserTestData; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.JpaUtil; import ru.javawebinar.topjava.util.exception.NotFoundException; import javax.validation.ConstraintViolationException; import java.util.Date; import java.util.List; import java.util.Set; -import ru.javawebinar.topjava.repository.JpaUtil; import static org.junit.Assert.assertThrows; import static ru.javawebinar.topjava.UserTestData.*; @@ -28,12 +30,15 @@ public abstract class AbstractUserServiceTest extends AbstractServiceTest { private CacheManager cacheManager; @Autowired + @Lazy protected JpaUtil jpaUtil; @Before public void setup() { cacheManager.getCache("users").clear(); - jpaUtil.clear2ndLevelHibernateCache(); + if (isJpaBased()) { + jpaUtil.clear2ndLevelHibernateCache(); + } } @Test @@ -95,6 +100,7 @@ public void getAll() { @Test public void createWithException() throws Exception { + Assume.assumeTrue("Validation not supported (JPA only)", isJpaBased()); validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, " ", "mail@yandex.ru", "password", Role.USER))); validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", " ", "password", Role.USER))); validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", "mail@yandex.ru", " ", Role.USER))); diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java index 996fdde1a580..9ff4ae615b2b 100644 --- a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java @@ -1,7 +1,5 @@ package ru.javawebinar.topjava.service.jdbc; -import org.junit.Ignore; -import org.junit.Test; import org.springframework.test.context.ActiveProfiles; import ru.javawebinar.topjava.service.AbstractMealServiceTest; @@ -9,9 +7,4 @@ @ActiveProfiles(JDBC) public class JdbcMealServiceTest extends AbstractMealServiceTest { - @Test - @Override - @Ignore - public void createWithException() throws Exception { - } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java index 477b92cae517..419f68ed1098 100644 --- a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java @@ -1,12 +1,10 @@ package ru.javawebinar.topjava.service.jdbc; -import org.junit.Ignore; import org.springframework.test.context.ActiveProfiles; import ru.javawebinar.topjava.service.AbstractUserServiceTest; import static ru.javawebinar.topjava.Profiles.JDBC; @ActiveProfiles(JDBC) -@Ignore public class JdbcUserServiceTest extends AbstractUserServiceTest { } \ 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 index dd39ec6611e1..626200081cc4 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java @@ -2,7 +2,6 @@ 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; @@ -14,9 +13,8 @@ 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"}) +@ContextConfiguration({"classpath:spring/inmemory.xml"}) @RunWith(SpringRunner.class) -@Ignore public class InMemoryAdminRestControllerSpringTest { @Autowired diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java index eee560577c2b..6c12a614d7af 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java @@ -13,7 +13,6 @@ import static ru.javawebinar.topjava.UserTestData.NOT_FOUND; import static ru.javawebinar.topjava.UserTestData.USER_ID; -@Ignore public class InMemoryAdminRestControllerTest { private static final Logger log = LoggerFactory.getLogger(InMemoryAdminRestControllerTest.class); @@ -23,7 +22,7 @@ public class InMemoryAdminRestControllerTest { @BeforeClass public static void beforeClass() { - appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml", "spring/inmemory.xml"); + appCtx = new ClassPathXmlApplicationContext("spring/inmemory.xml"); log.info("\n{}\n", Arrays.toString(appCtx.getBeanDefinitionNames())); controller = appCtx.getBean(AdminRestController.class); repository = appCtx.getBean(InMemoryUserRepository.class); @@ -31,7 +30,9 @@ public static void beforeClass() { @AfterClass public static void afterClass() { - appCtx.close(); + // May cause during JUnit "Cache is not alive (STATUS_SHUTDOWN)" as JUnit share Spring context for speed + // http://stackoverflow.com/questions/16281802/ehcache-shutdown-causing-an-exception-while-running-test-suite + // appCtx.close(); } @Before diff --git a/src/test/resources/spring/inmemory.xml b/src/test/resources/spring/inmemory.xml index c6a2710cbd88..0c9d0502857d 100644 --- a/src/test/resources/spring/inmemory.xml +++ b/src/test/resources/spring/inmemory.xml @@ -4,4 +4,6 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> + + \ No newline at end of file From 96d35304d13b02101906bc64bc4672fee2053e88 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 25 Mar 2023 20:56:26 +0700 Subject: [PATCH 065/125] apply patch 7_02 --- config/messages/app.properties | 19 ++++- config/messages/app_ru.properties | 17 ++++ .../javawebinar/topjava/web/MealServlet.java | 85 ------------------- .../topjava/web/RootController.java | 17 +++- .../web/meal/AbstractMealController.java | 72 ++++++++++++++++ .../topjava/web/meal/JspMealController.java | 70 +++++++++++++++ .../topjava/web/meal/MealRestController.java | 71 +--------------- src/main/webapp/WEB-INF/jsp/mealForm.jsp | 25 +++--- src/main/webapp/WEB-INF/jsp/meals.jsp | 44 +++++----- 9 files changed, 227 insertions(+), 193 deletions(-) delete mode 100644 src/main/java/ru/javawebinar/topjava/web/MealServlet.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java diff --git a/config/messages/app.properties b/config/messages/app.properties index ccd4c66b5397..6b4d2d155a47 100644 --- a/config/messages/app.properties +++ b/config/messages/app.properties @@ -2,11 +2,28 @@ app.title=Calories management app.home=Home app.footer=Internship Spring 5/JPA Enterprise (Topjava) application app.login=Login as + user.title=Users user.name=Name user.email=Email user.roles=Roles user.active=Active user.registered=Registered + meal.title=Meals -common.select=Select \ No newline at end of file +meal.edit=Edit meal +meal.add=Add meal +meal.filter=Filter +meal.startDate=From date (inclusive) +meal.endDate=To date (inclusive) +meal.startTime=From time (inclusive) +meal.endTime=To time (exclusive) +meal.description=Description +meal.dateTime=Date/Time +meal.calories=Calories + +common.select=Select +common.delete=Delete +common.update=Update +common.save=Save +common.cancel=Cancel \ No newline at end of file diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties index a9760be8d75d..c1e3d54e35a5 100644 --- a/config/messages/app_ru.properties +++ b/config/messages/app_ru.properties @@ -2,11 +2,28 @@ app.title=Подсчет калорий app.home=Главная app.footer=Приложение стажировки Spring 5/JPA Enterprise (Topjava) app.login=Зайти как + user.title=Пользователи user.name=Имя user.email=Почта user.roles=Роли user.active=Активный user.registered=Зарегистрирован + meal.title=Моя еда +meal.edit=Редактирование еды +meal.add=Добавление еды +meal.filter=Отфильтровать +meal.startDate=От даты (включая) +meal.endDate=До даты (включая) +meal.startTime=От времени (включая) +meal.endTime=До времени (исключая) +meal.description=Описание +meal.dateTime=Дата/Время +meal.calories=Калории + common.select=Выбрать +common.delete=Удалить +common.update=Обновить +common.save=Сохранить +common.cancel=Отменить diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java deleted file mode 100644 index e412a25dcc89..000000000000 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ /dev/null @@ -1,85 +0,0 @@ -package ru.javawebinar.topjava.web; - -import org.springframework.util.StringUtils; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.context.support.WebApplicationContextUtils; -import ru.javawebinar.topjava.model.Meal; -import ru.javawebinar.topjava.web.meal.MealRestController; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.temporal.ChronoUnit; -import java.util.Objects; - -import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalDate; -import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalTime; - -public class MealServlet extends HttpServlet { - - private MealRestController mealController; - - @Override - public void init() { - WebApplicationContext springContext = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); - mealController = springContext.getBean(MealRestController.class); - } - - @Override - protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { - request.setCharacterEncoding("UTF-8"); - Meal meal = new Meal( - LocalDateTime.parse(request.getParameter("dateTime")), - request.getParameter("description"), - Integer.parseInt(request.getParameter("calories"))); - - if (StringUtils.hasLength(request.getParameter("id"))) { - mealController.update(meal, getId(request)); - } else { - mealController.create(meal); - } - response.sendRedirect("meals"); - } - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String action = request.getParameter("action"); - - switch (action == null ? "all" : action) { - case "delete" -> { - int id = getId(request); - mealController.delete(id); - response.sendRedirect("meals"); - } - case "create", "update" -> { - final Meal meal = "create".equals(action) ? - new Meal(LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), "", 1000) : - mealController.get(getId(request)); - request.setAttribute("meal", meal); - request.getRequestDispatcher("/mealForm.jsp").forward(request, response); - } - case "filter" -> { - LocalDate startDate = parseLocalDate(request.getParameter("startDate")); - LocalDate endDate = parseLocalDate(request.getParameter("endDate")); - LocalTime startTime = parseLocalTime(request.getParameter("startTime")); - LocalTime endTime = parseLocalTime(request.getParameter("endTime")); - request.setAttribute("meals", mealController.getBetween(startDate, startTime, endDate, endTime)); - request.getRequestDispatcher("/meals.jsp").forward(request, response); - } - default -> { - request.setAttribute("meals", mealController.getAll()); - request.getRequestDispatcher("/meals.jsp").forward(request, response); - } - } - } - - private int getId(HttpServletRequest request) { - String paramId = Objects.requireNonNull(request.getParameter("id")); - return Integer.parseInt(paramId); - } -} diff --git a/src/main/java/ru/javawebinar/topjava/web/RootController.java b/src/main/java/ru/javawebinar/topjava/web/RootController.java index f432d93b1fdf..921462ca98cd 100644 --- a/src/main/java/ru/javawebinar/topjava/web/RootController.java +++ b/src/main/java/ru/javawebinar/topjava/web/RootController.java @@ -7,7 +7,9 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import ru.javawebinar.topjava.service.MealService; import ru.javawebinar.topjava.service.UserService; +import ru.javawebinar.topjava.util.MealsUtil; import javax.servlet.http.HttpServletRequest; @@ -16,7 +18,10 @@ public class RootController { private static final Logger log = LoggerFactory.getLogger(RootController.class); @Autowired - private UserService service; + private UserService userService; + + @Autowired + private MealService mealService; @GetMapping("/") public String root() { @@ -27,7 +32,7 @@ public String root() { @GetMapping("/users") public String getUsers(Model model) { log.info("users"); - model.addAttribute("users", service.getAll()); + model.addAttribute("users", userService.getAll()); return "users"; } @@ -38,4 +43,12 @@ public String setUser(HttpServletRequest request) { SecurityUtil.setAuthUserId(userId); return "redirect:meals"; } + + @GetMapping("/meals") + public String getMeals(Model model) { + log.info("meals"); + model.addAttribute("meals", + MealsUtil.getTos(mealService.getAll(SecurityUtil.authUserId()), SecurityUtil.authUserCaloriesPerDay())); + return "meals"; + } } diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java new file mode 100644 index 000000000000..ec601c187896 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java @@ -0,0 +1,72 @@ +package ru.javawebinar.topjava.web.meal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.Nullable; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.service.MealService; +import ru.javawebinar.topjava.to.MealTo; +import ru.javawebinar.topjava.util.MealsUtil; +import ru.javawebinar.topjava.web.SecurityUtil; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNew; + +public abstract class AbstractMealController { + private final Logger log = LoggerFactory.getLogger(getClass()); + + @Autowired + private MealService service; + + public Meal get(int id) { + int userId = SecurityUtil.authUserId(); + log.info("get meal {} for user {}", id, userId); + return service.get(id, userId); + } + + public void delete(int id) { + int userId = SecurityUtil.authUserId(); + log.info("delete meal {} for user {}", id, userId); + service.delete(id, userId); + } + + public List getAll() { + int userId = SecurityUtil.authUserId(); + log.info("getAll for user {}", userId); + return MealsUtil.getTos(service.getAll(userId), SecurityUtil.authUserCaloriesPerDay()); + } + + public Meal create(Meal meal) { + int userId = SecurityUtil.authUserId(); + log.info("create {} for user {}", meal, userId); + checkNew(meal); + return service.create(meal, userId); + } + + public void update(Meal meal, int id) { + int userId = SecurityUtil.authUserId(); + log.info("update {} for user {}", meal, userId); + assureIdConsistent(meal, id); + service.update(meal, userId); + } + + /** + *
      Filter separately + *
    1. by date
    2. + *
    3. by time for every date
    4. + *
    + */ + public List getBetween(@Nullable LocalDate startDate, @Nullable LocalTime startTime, + @Nullable LocalDate endDate, @Nullable LocalTime endTime) { + int userId = SecurityUtil.authUserId(); + log.info("getBetween dates({} - {}) time({} - {}) for user {}", startDate, endDate, startTime, endTime, userId); + + List mealsDateFiltered = service.getBetweenInclusive(startDate, endDate, userId); + return MealsUtil.getFilteredTos(mealsDateFiltered, SecurityUtil.authUserCaloriesPerDay(), startTime, endTime); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java b/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java new file mode 100644 index 000000000000..5ba8a2c046f6 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java @@ -0,0 +1,70 @@ +package ru.javawebinar.topjava.web.meal; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import ru.javawebinar.topjava.model.Meal; + +import javax.servlet.http.HttpServletRequest; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.temporal.ChronoUnit; +import java.util.Objects; + +import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalDate; +import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalTime; + +@Controller +@RequestMapping("/meals") +public class JspMealController extends AbstractMealController { + + @GetMapping("/delete") + public String delete(HttpServletRequest request) { + super.delete(getId(request)); + return "redirect:meals"; + } + + @GetMapping("/update") + public String update(HttpServletRequest request, Model model) { + model.addAttribute("meal", super.get(getId(request))); + return "mealForm"; + } + + @GetMapping("/create") + public String create(Model model) { + model.addAttribute("meal", new Meal(LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), "", 1000)); + return "mealForm"; + } + + @PostMapping + public String updateOrCreate(HttpServletRequest request) { + Meal meal = new Meal(LocalDateTime.parse(request.getParameter("dateTime")), + request.getParameter("description"), + Integer.parseInt(request.getParameter("calories"))); + + if (request.getParameter("id").isEmpty()) { + super.create(meal); + } else { + super.update(meal, getId(request)); + } + return "redirect:meals"; + } + + @GetMapping("/filter") + public String getBetween(HttpServletRequest request, Model model) { + LocalDate startDate = parseLocalDate(request.getParameter("startDate")); + LocalDate endDate = parseLocalDate(request.getParameter("endDate")); + LocalTime startTime = parseLocalTime(request.getParameter("startTime")); + LocalTime endTime = parseLocalTime(request.getParameter("endTime")); + model.addAttribute("meals", super.getBetween(startDate, startTime, endDate, endTime)); + return "meals"; + } + + private int getId(HttpServletRequest request) { + String paramId = Objects.requireNonNull(request.getParameter("id")); + return Integer.parseInt(paramId); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java index bbfe35e3f0fa..c3daf68535d7 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java @@ -1,76 +1,7 @@ package ru.javawebinar.topjava.web.meal; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.lang.Nullable; import org.springframework.stereotype.Controller; -import ru.javawebinar.topjava.model.Meal; -import ru.javawebinar.topjava.service.MealService; -import ru.javawebinar.topjava.to.MealTo; -import ru.javawebinar.topjava.util.MealsUtil; -import ru.javawebinar.topjava.web.SecurityUtil; - -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.List; - -import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent; -import static ru.javawebinar.topjava.util.ValidationUtil.checkNew; @Controller -public class MealRestController { - private static final Logger log = LoggerFactory.getLogger(MealRestController.class); - - private final MealService service; - - public MealRestController(MealService service) { - this.service = service; - } - - public Meal get(int id) { - int userId = SecurityUtil.authUserId(); - log.info("get meal {} for user {}", id, userId); - return service.get(id, userId); - } - - public void delete(int id) { - int userId = SecurityUtil.authUserId(); - log.info("delete meal {} for user {}", id, userId); - service.delete(id, userId); - } - - public List getAll() { - int userId = SecurityUtil.authUserId(); - log.info("getAll for user {}", userId); - return MealsUtil.getTos(service.getAll(userId), SecurityUtil.authUserCaloriesPerDay()); - } - - public Meal create(Meal meal) { - int userId = SecurityUtil.authUserId(); - checkNew(meal); - log.info("create {} for user {}", meal, userId); - return service.create(meal, userId); - } - - public void update(Meal meal, int id) { - int userId = SecurityUtil.authUserId(); - assureIdConsistent(meal, id); - log.info("update {} for user {}", meal, userId); - service.update(meal, userId); - } - - /** - *
      Filter separately - *
    1. by date
    2. - *
    3. by time for every date
    4. - *
    - */ - public List getBetween(@Nullable LocalDate startDate, @Nullable LocalTime startTime, - @Nullable LocalDate endDate, @Nullable LocalTime endTime) { - int userId = SecurityUtil.authUserId(); - log.info("getBetween dates({} - {}) time({} - {}) for user {}", startDate, endDate, startTime, endTime, userId); - - List mealsDateFiltered = service.getBetweenInclusive(startDate, endDate, userId); - return MealsUtil.getFilteredTos(mealsDateFiltered, SecurityUtil.authUserCaloriesPerDay(), startTime, endTime); - } +public class MealRestController extends AbstractMealController { } \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/mealForm.jsp b/src/main/webapp/WEB-INF/jsp/mealForm.jsp index 98a6f48735dc..af6d7880ec98 100644 --- a/src/main/webapp/WEB-INF/jsp/mealForm.jsp +++ b/src/main/webapp/WEB-INF/jsp/mealForm.jsp @@ -1,34 +1,35 @@ <%@ page contentType="text/html;charset=UTF-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - - Meal - - + + +
    -

    Home

    -
    -

    ${param.action == 'create' ? 'Create meal' : 'Edit meal'}

    +<%-- `meal.new` cause javax.el.ELException - bug tomcat --%> +

    +
    -
    DateTime:
    +
    :
    -
    Description:
    +
    :
    -
    Calories:
    +
    :
    - - + +
    + diff --git a/src/main/webapp/WEB-INF/jsp/meals.jsp b/src/main/webapp/WEB-INF/jsp/meals.jsp index 0074532217bf..b42230b902bd 100644 --- a/src/main/webapp/WEB-INF/jsp/meals.jsp +++ b/src/main/webapp/WEB-INF/jsp/meals.jsp @@ -1,46 +1,43 @@ <%@ 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="spring" uri="http://www.springframework.org/tags" %> <%@ taglib prefix="fn" uri="http://topjava.javawebinar.ru/functions" %> - - Meals - - + + +
    -

    Home

    -
    -

    Meals

    -
    - +

    + +
    -
    From Date (inclusive):
    +
    :
    -
    To Date (inclusive):
    +
    :
    -
    From Time (inclusive):
    +
    :
    -
    To Time (exclusive):
    +
    :
    - + -
    - Add Meal -

    +
    + +
    - - - + + + @@ -56,11 +53,12 @@ - - + +
    DateDescriptionCalories
    ${meal.description} ${meal.calories}UpdateDelete
    + \ No newline at end of file From c5db18350ba9278c6c86f53433886bf184ec64c2 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 25 Mar 2023 20:56:39 +0700 Subject: [PATCH 066/125] apply patch 7_03 --- .../topjava/web/meal/JspMealController.java | 4 ++-- .../webapp/WEB-INF/jsp/fragments/headTag.jsp | 1 + src/main/webapp/WEB-INF/web.xml | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java b/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java index 5ba8a2c046f6..7e800f683d8a 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java @@ -24,7 +24,7 @@ public class JspMealController extends AbstractMealController { @GetMapping("/delete") public String delete(HttpServletRequest request) { super.delete(getId(request)); - return "redirect:meals"; + return "redirect:/meals"; } @GetMapping("/update") @@ -50,7 +50,7 @@ public String updateOrCreate(HttpServletRequest request) { } else { super.update(meal, getId(request)); } - return "redirect:meals"; + return "redirect:/meals"; } @GetMapping("/filter") diff --git a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp index 6d77694e3406..0c77f10859f8 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp @@ -5,5 +5,6 @@ <spring:message code="app.title"/> + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index a6a4f622b4f7..65af7c831300 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -36,4 +36,21 @@ mvc-dispatcher / + + + encodingFilter + org.springframework.web.filter.CharacterEncodingFilter + + encoding + UTF-8 + + + forceEncoding + true + + + + encodingFilter + /* + From fc92b5fb12f90b38de9ca99abd569fddc78f15fe Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 25 Mar 2023 21:20:45 +0700 Subject: [PATCH 067/125] apply patch 7_04 --- src/main/java/ru/javawebinar/topjava/model/User.java | 2 +- src/main/resources/db/populateDB.sql | 3 ++- src/test/java/ru/javawebinar/topjava/UserTestData.java | 4 ++-- .../topjava/service/AbstractUserServiceTest.java | 5 ++--- .../topjava/service/datajpa/DataJpaUserServiceTest.java | 6 +++--- .../topjava/service/jdbc/JdbcUserServiceTest.java | 2 ++ 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index 0b4d40a8f2e2..c24386a9462d 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -22,7 +22,7 @@ @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @NamedQueries({ @NamedQuery(name = User.DELETE, query = "DELETE FROM User u WHERE u.id=:id"), - @NamedQuery(name = User.BY_EMAIL, query = "SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.email=?1"), + @NamedQuery(name = User.BY_EMAIL, query = "SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.roles WHERE u.email=?1"), @NamedQuery(name = User.ALL_SORTED, query = "SELECT u FROM User u ORDER BY u.name, u.email"), }) @Entity diff --git a/src/main/resources/db/populateDB.sql b/src/main/resources/db/populateDB.sql index 5021dc6f13b8..9e9bd828babe 100644 --- a/src/main/resources/db/populateDB.sql +++ b/src/main/resources/db/populateDB.sql @@ -10,7 +10,8 @@ VALUES ('User', 'user@yandex.ru', 'password'), INSERT INTO user_role (role, user_id) VALUES ('USER', 100000), - ('ADMIN', 100001); + ('ADMIN', 100001), + ('USER', 100001); INSERT INTO meal (date_time, description, calories, user_id) VALUES ('2020-01-30 10:00:00', 'Завтрак', 500, 100000), diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java index 3419c521532a..380672440ba8 100644 --- a/src/test/java/ru/javawebinar/topjava/UserTestData.java +++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java @@ -9,7 +9,7 @@ import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; public class UserTestData { - public static final MatcherFactory.Matcher USER_MATCHER = MatcherFactory.usingIgnoringFieldsComparator("registered", "roles", "meals"); + public static final MatcherFactory.Matcher USER_MATCHER = MatcherFactory.usingIgnoringFieldsComparator("registered", "meals"); public static final int USER_ID = START_SEQ; public static final int ADMIN_ID = START_SEQ + 1; @@ -17,7 +17,7 @@ public class UserTestData { 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 admin = new User(ADMIN_ID, "Admin", "admin@gmail.com", "admin", Role.ADMIN, Role.USER); public static final User guest = new User(GUEST_ID, "Guest", "guest@gmail.com", "guest"); public static User getNew() { diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java index e802895aa95e..54ee6a611d84 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -7,7 +7,6 @@ import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Lazy; import org.springframework.dao.DataAccessException; -import ru.javawebinar.topjava.UserTestData; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.JpaUtil; @@ -70,8 +69,8 @@ public void deletedNotFound() { @Test public void get() { - User user = service.get(USER_ID); - USER_MATCHER.assertMatch(user, UserTestData.user); + User user = service.get(ADMIN_ID); + USER_MATCHER.assertMatch(user, admin); } @Test diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java index 7733b7bfc784..dd7ad4ed087a 100644 --- a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java @@ -17,9 +17,9 @@ public class DataJpaUserServiceTest extends AbstractUserServiceTest { @Test public void getWithMeals() { - User user = service.getWithMeals(USER_ID); - USER_MATCHER.assertMatch(user, UserTestData.user); - MEAL_MATCHER.assertMatch(user.getMeals(), MealTestData.meals); + User admin = service.getWithMeals(ADMIN_ID); + USER_MATCHER.assertMatch(admin, UserTestData.admin); + MEAL_MATCHER.assertMatch(admin.getMeals(), MealTestData.adminMeal2, MealTestData.adminMeal1); } @Test diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java index 419f68ed1098..477b92cae517 100644 --- a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java @@ -1,10 +1,12 @@ package ru.javawebinar.topjava.service.jdbc; +import org.junit.Ignore; import org.springframework.test.context.ActiveProfiles; import ru.javawebinar.topjava.service.AbstractUserServiceTest; import static ru.javawebinar.topjava.Profiles.JDBC; @ActiveProfiles(JDBC) +@Ignore public class JdbcUserServiceTest extends AbstractUserServiceTest { } \ No newline at end of file From 3d5fe1008dbc15882bdb983194f5a2d2bf0a3636 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 25 Mar 2023 21:21:21 +0700 Subject: [PATCH 068/125] apply patch 7_05 --- .../repository/datajpa/CrudUserRepository.java | 14 +++++++++----- .../topjava/repository/jpa/JpaUserRepository.java | 2 ++ src/main/resources/db/postgres.properties | 3 ++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java index 806884a9c90b..f3f362bdfaf0 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java @@ -1,13 +1,12 @@ package ru.javawebinar.topjava.repository.datajpa; -import org.springframework.data.jpa.repository.EntityGraph; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.*; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; import ru.javawebinar.topjava.model.User; +import javax.persistence.QueryHint; + @Transactional(readOnly = true) public interface CrudUserRepository extends JpaRepository { @Transactional @@ -15,9 +14,14 @@ public interface CrudUserRepository extends JpaRepository { @Query("DELETE FROM User u WHERE u.id=:id") int delete(@Param("id") int id); + // https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#hql-distinct + @QueryHints({ + @QueryHint(name = org.hibernate.jpa.QueryHints.HINT_PASS_DISTINCT_THROUGH, value = "false") + }) User getByEmail(String email); - @EntityGraph(attributePaths = {"meals", "roles"}) + // https://stackoverflow.com/a/46013654/548473 + @EntityGraph(attributePaths = {"meals"}, type = EntityGraph.EntityGraphType.LOAD) @Query("SELECT u FROM User u WHERE u.id=?1") User getWithMeals(int id); } diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java index 3a1bbddc4cf2..22fa8f4e35e1 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java @@ -1,5 +1,6 @@ package ru.javawebinar.topjava.repository.jpa; +import org.hibernate.jpa.QueryHints; import org.springframework.dao.support.DataAccessUtils; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -61,6 +62,7 @@ public boolean delete(int id) { public User getByEmail(String email) { List users = em.createNamedQuery(User.BY_EMAIL, User.class) .setParameter(1, email) + .setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false) .getResultList(); return DataAccessUtils.singleResult(users); } diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties index 75d7d68730ad..c56854a9b452 100644 --- a/src/main/resources/db/postgres.properties +++ b/src/main/resources/db/postgres.properties @@ -6,4 +6,5 @@ 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 +#https://hibernate.atlassian.net/browse/HHH-13280 +hibernate.use_sql_comments=false \ No newline at end of file From 12cc914e1a49f005a8d78b576a3adee0cbf982d7 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 25 Mar 2023 21:21:40 +0700 Subject: [PATCH 069/125] apply patch 7_06 --- .../repository/jdbc/JdbcMealRepository.java | 4 ++ .../repository/jdbc/JdbcUserRepository.java | 63 ++++++++++++++++--- src/main/resources/spring/spring-db.xml | 9 ++- .../service/jdbc/JdbcUserServiceTest.java | 2 - 4 files changed, 65 insertions(+), 13 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java index b7f268f3a5ee..61ad25b2a090 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java @@ -8,6 +8,7 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; @@ -15,6 +16,7 @@ import java.util.List; @Repository +@Transactional(readOnly = true) public class JdbcMealRepository implements MealRepository { private static final RowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(Meal.class); @@ -35,6 +37,7 @@ public JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate } @Override + @Transactional public Meal save(Meal meal, int userId) { MapSqlParameterSource map = new MapSqlParameterSource() .addValue("id", meal.getId()) @@ -58,6 +61,7 @@ public Meal save(Meal meal, int userId) { } @Override + @Transactional public boolean delete(int id, int userId) { return jdbcTemplate.update("DELETE FROM meal WHERE id=? AND user_id=?", id, userId) != 0; } diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java index 0d4baa50eef2..7ea9570d24aa 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java @@ -8,12 +8,16 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.UserRepository; -import java.util.List; +import java.util.*; @Repository +@Transactional(readOnly = true) public class JdbcUserRepository implements UserRepository { private static final BeanPropertyRowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(User.class); @@ -35,22 +39,32 @@ public JdbcUserRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate } @Override + @Transactional public User save(User user) { BeanPropertySqlParameterSource parameterSource = new BeanPropertySqlParameterSource(user); if (user.isNew()) { Number newKey = insertUser.executeAndReturnKey(parameterSource); user.setId(newKey.intValue()); - } else if (namedParameterJdbcTemplate.update(""" - UPDATE users SET name=:name, email=:email, password=:password, - registered=:registered, enabled=:enabled, calories_per_day=:caloriesPerDay WHERE id=:id - """, parameterSource) == 0) { - return null; + insertRoles(user); + } else { + if (namedParameterJdbcTemplate.update(""" + UPDATE users SET name=:name, email=:email, password=:password, + registered=:registered, enabled=:enabled, calories_per_day=:caloriesPerDay WHERE id=:id + """, parameterSource) == 0) { + return null; + } + // Simplest implementation. + // More complicated : get user roles from DB and compare them with user.roles (assume that roles are changed rarely). + // If roles are changed, calculate difference in java and delete/insert them. + deleteRoles(user); + insertRoles(user); } return user; } @Override + @Transactional public boolean delete(int id) { return jdbcTemplate.update("DELETE FROM users WHERE id=?", id) != 0; } @@ -58,18 +72,49 @@ public boolean delete(int id) { @Override public User get(int id) { List users = jdbcTemplate.query("SELECT * FROM users WHERE id=?", ROW_MAPPER, id); - return DataAccessUtils.singleResult(users); + return setRoles(DataAccessUtils.singleResult(users)); } @Override public User getByEmail(String email) { // return jdbcTemplate.queryForObject("SELECT * FROM users WHERE email=?", ROW_MAPPER, email); List users = jdbcTemplate.query("SELECT * FROM users WHERE email=?", ROW_MAPPER, email); - return DataAccessUtils.singleResult(users); + return setRoles(DataAccessUtils.singleResult(users)); } @Override public List getAll() { - return jdbcTemplate.query("SELECT * FROM users ORDER BY name, email", ROW_MAPPER); + List users = jdbcTemplate.query("SELECT * FROM users ORDER BY name, email", ROW_MAPPER); + + Map> map = new HashMap<>(); + jdbcTemplate.query("SELECT * FROM user_role", rs -> { + map.computeIfAbsent(rs.getInt("user_id"), userId -> EnumSet.noneOf(Role.class)) + .add(Role.valueOf(rs.getString("role"))); + }); + users.forEach(u -> u.setRoles(map.get(u.getId()))); + return users; + } + + private void insertRoles(User u) { + Set roles = u.getRoles(); + if (!CollectionUtils.isEmpty(roles)) { + jdbcTemplate.batchUpdate("INSERT INTO user_role (user_id, role) VALUES (?, ?)", roles, roles.size(), + (ps, role) -> { + ps.setInt(1, u.id()); + ps.setString(2, role.name()); + }); + } + } + + private void deleteRoles(User u) { + jdbcTemplate.update("DELETE FROM user_role WHERE user_id=?", u.getId()); + } + + private User setRoles(User u) { + if (u != null) { + List roles = jdbcTemplate.queryForList("SELECT role FROM user_role WHERE user_id=?", Role.class, u.getId()); + u.setRoles(roles); + } + return u; } } diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 2b62f074828b..bf849688d059 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -20,6 +20,8 @@ + + @@ -66,6 +68,11 @@ + + + + @@ -101,8 +108,6 @@
    - - diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java index 477b92cae517..419f68ed1098 100644 --- a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java @@ -1,12 +1,10 @@ package ru.javawebinar.topjava.service.jdbc; -import org.junit.Ignore; import org.springframework.test.context.ActiveProfiles; import ru.javawebinar.topjava.service.AbstractUserServiceTest; import static ru.javawebinar.topjava.Profiles.JDBC; @ActiveProfiles(JDBC) -@Ignore public class JdbcUserServiceTest extends AbstractUserServiceTest { } \ No newline at end of file From aafba6af7c8f55b07d28ed4565a16b16bcf1275a Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 25 Mar 2023 21:21:50 +0700 Subject: [PATCH 070/125] apply patch 7_07 --- .../ru/javawebinar/topjava/model/Meal.java | 2 +- .../repository/jdbc/JdbcMealRepository.java | 3 +++ .../repository/jdbc/JdbcUserRepository.java | 3 +++ .../topjava/util/ValidationUtil.java | 20 +++++++++++++++++++ .../service/AbstractMealServiceTest.java | 2 -- .../service/AbstractUserServiceTest.java | 2 -- 6 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index c579bf169b6e..e74f32939c7c 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -45,7 +45,7 @@ public class Meal extends AbstractBaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) @OnDelete(action = OnDeleteAction.CASCADE) - @NotNull +// @NotNull private User user; public Meal() { diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java index 61ad25b2a090..775d314ed961 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java @@ -11,6 +11,7 @@ import org.springframework.transaction.annotation.Transactional; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; +import ru.javawebinar.topjava.util.ValidationUtil; import java.time.LocalDateTime; import java.util.List; @@ -39,6 +40,8 @@ public JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate @Override @Transactional public Meal save(Meal meal, int userId) { + ValidationUtil.validate(meal); + MapSqlParameterSource map = new MapSqlParameterSource() .addValue("id", meal.getId()) .addValue("description", meal.getDescription()) diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java index 7ea9570d24aa..ca5dcc185cd8 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java @@ -13,6 +13,7 @@ import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.UserRepository; +import ru.javawebinar.topjava.util.ValidationUtil; import java.util.*; @@ -41,6 +42,8 @@ public JdbcUserRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate @Override @Transactional public User save(User user) { + ValidationUtil.validate(user); + BeanPropertySqlParameterSource parameterSource = new BeanPropertySqlParameterSource(user); if (user.isNew()) { diff --git a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java index 986c7d97df5f..fa79c4b95dee 100644 --- a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java @@ -6,11 +6,31 @@ import ru.javawebinar.topjava.model.AbstractBaseEntity; import ru.javawebinar.topjava.util.exception.NotFoundException; +import javax.validation.*; +import java.util.Set; + public class ValidationUtil { + private static final Validator validator; + + static { + // From Javadoc: implementations are thread-safe and instances are typically cached and reused. + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + // From Javadoc: implementations of this interface must be thread-safe + validator = factory.getValidator(); + } + private ValidationUtil() { } + public static void validate(T bean) { + // https://alexkosarev.name/2018/07/30/bean-validation-api/ + Set> violations = validator.validate(bean); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + } + public static T checkNotFoundWithId(T object, int id) { checkNotFoundWithId(object != null, id); return object; diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java index 06061c1d818b..b19ffabe8e09 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java @@ -1,7 +1,6 @@ package ru.javawebinar.topjava.service; import org.junit.Assert; -import org.junit.Assume; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; @@ -105,7 +104,6 @@ public void getBetweenWithNullDates() { @Test public void createWithException() throws Exception { - Assume.assumeTrue("Validation not supported (JPA only)", isJpaBased()); validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), " ", 300), USER_ID)); validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, null, "Description", 300), USER_ID)); validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Description", 9), USER_ID)); diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java index 54ee6a611d84..4252ee3d59d9 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -1,6 +1,5 @@ package ru.javawebinar.topjava.service; -import org.junit.Assume; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -99,7 +98,6 @@ public void getAll() { @Test public void createWithException() throws Exception { - Assume.assumeTrue("Validation not supported (JPA only)", isJpaBased()); validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, " ", "mail@yandex.ru", "password", Role.USER))); validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", " ", "password", Role.USER))); validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", "mail@yandex.ru", " ", Role.USER))); From 291275bc101e764e7cce95fee40114a1a43a0624 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 25 Mar 2023 21:22:01 +0700 Subject: [PATCH 071/125] apply patch 7_08 --- .../topjava/repository/JpaUtil.java | 21 --------------- src/main/resources/spring/spring-db.xml | 2 -- .../topjava/service/AbstractServiceTest.java | 11 -------- .../service/AbstractUserServiceTest.java | 19 ------------- src/test/resources/spring/spring-cache.xml | 27 +++++++++++++++++++ 5 files changed, 27 insertions(+), 53 deletions(-) delete mode 100644 src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java create mode 100644 src/test/resources/spring/spring-cache.xml diff --git a/src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java b/src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java deleted file mode 100644 index f3e51ad50fbb..000000000000 --- a/src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java +++ /dev/null @@ -1,21 +0,0 @@ -package ru.javawebinar.topjava.repository; - -import org.hibernate.Session; -import org.hibernate.SessionFactory; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; - -public class JpaUtil { - - @PersistenceContext - private EntityManager em; - - public void clear2ndLevelHibernateCache() { - Session s = (Session) em.getDelegate(); - SessionFactory sf = s.getSessionFactory(); -// sf.getCache().evictEntityData(User.class, AbstractBaseEntity.START_SEQ); -// sf.getCache().evictEntityData(User.class); - sf.getCache().evictAllRegions(); - } -} diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index bf849688d059..48afdb11a749 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -111,8 +111,6 @@ - - diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java index e4c1f5757696..06d99de9734e 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java @@ -5,15 +5,12 @@ import org.junit.rules.ExternalResource; import org.junit.rules.Stopwatch; import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.env.Environment; 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.Profiles; import ru.javawebinar.topjava.TimingRules; import static org.junit.Assert.assertThrows; @@ -31,17 +28,9 @@ public abstract class AbstractServiceTest { @ClassRule public static ExternalResource summary = TimingRules.SUMMARY; - @Autowired - private Environment env; - @Rule public Stopwatch stopwatch = TimingRules.STOPWATCH; - public boolean isJpaBased() { -// return Arrays.stream(env.getActiveProfiles()).noneMatch(Profiles.JDBC::equals); - return env.acceptsProfiles(org.springframework.core.env.Profiles.of(Profiles.JPA, Profiles.DATAJPA)); - } - // Check root cause in JUnit: https://github.com/junit-team/junit4/pull/778 protected void validateRootCause(Class rootExceptionClass, Runnable runnable) { assertThrows(rootExceptionClass, () -> { diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java index 4252ee3d59d9..6bd0772a9be8 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -1,14 +1,10 @@ package ru.javawebinar.topjava.service; -import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cache.CacheManager; -import org.springframework.context.annotation.Lazy; import org.springframework.dao.DataAccessException; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; -import ru.javawebinar.topjava.repository.JpaUtil; import ru.javawebinar.topjava.util.exception.NotFoundException; import javax.validation.ConstraintViolationException; @@ -24,21 +20,6 @@ public abstract class AbstractUserServiceTest extends AbstractServiceTest { @Autowired protected UserService service; - @Autowired - private CacheManager cacheManager; - - @Autowired - @Lazy - protected JpaUtil jpaUtil; - - @Before - public void setup() { - cacheManager.getCache("users").clear(); - if (isJpaBased()) { - jpaUtil.clear2ndLevelHibernateCache(); - } - } - @Test public void create() { User created = service.create(getNew()); diff --git a/src/test/resources/spring/spring-cache.xml b/src/test/resources/spring/spring-cache.xml new file mode 100644 index 000000000000..7c9dfda9a9f3 --- /dev/null +++ b/src/test/resources/spring/spring-cache.xml @@ -0,0 +1,27 @@ + + + + + + + + + false + + + + + + \ No newline at end of file From 5bba79f3526ecad1038212509812ece11ea4be02 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 25 Mar 2023 21:36:56 +0700 Subject: [PATCH 072/125] apply patch 7_09 --- pom.xml | 13 +++++ .../topjava/web/AbstractControllerTest.java | 55 +++++++++++++++++++ .../topjava/web/RootControllerTest.java | 32 +++++++++++ 3 files changed, 100 insertions(+) create mode 100644 src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java create mode 100644 src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java diff --git a/pom.xml b/pom.xml index 1c2a4f78da9c..c5736d38a662 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,7 @@ 4.13.2 3.24.2 + 1.3 @@ -217,6 +218,18 @@ junit ${junit.version} test + + + hamcrest-core + org.hamcrest + + + + + org.hamcrest + hamcrest-all + ${hamcrest.version} + test org.springframework diff --git a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java new file mode 100644 index 000000000000..3ec192baa614 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java @@ -0,0 +1,55 @@ +package ru.javawebinar.topjava.web; + +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; +import ru.javawebinar.topjava.ActiveDbProfileResolver; +import ru.javawebinar.topjava.Profiles; + +import javax.annotation.PostConstruct; + +@ContextConfiguration({ + "classpath:spring/spring-app.xml", + "classpath:spring/spring-mvc.xml", + "classpath:spring/spring-db.xml" +}) +@WebAppConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +@Transactional +@ActiveProfiles(resolver = ActiveDbProfileResolver.class, profiles = Profiles.REPOSITORY_IMPLEMENTATION) +public abstract class AbstractControllerTest { + + private static final CharacterEncodingFilter CHARACTER_ENCODING_FILTER = new CharacterEncodingFilter(); + + static { + CHARACTER_ENCODING_FILTER.setEncoding("UTF-8"); + CHARACTER_ENCODING_FILTER.setForceEncoding(true); + } + + private MockMvc mockMvc; + + @Autowired + private WebApplicationContext webApplicationContext; + + @PostConstruct + private void postConstruct() { + mockMvc = MockMvcBuilders + .webAppContextSetup(webApplicationContext) + .addFilter(CHARACTER_ENCODING_FILTER) + .build(); + } + + protected ResultActions perform(MockHttpServletRequestBuilder builder) throws Exception { + return mockMvc.perform(builder); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java new file mode 100644 index 000000000000..cca2827e887f --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java @@ -0,0 +1,32 @@ +package ru.javawebinar.topjava.web; + +import org.junit.Test; +import ru.javawebinar.topjava.UserTestData; + +import static org.hamcrest.Matchers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; +import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; + +public class RootControllerTest extends AbstractControllerTest { + + @Test + public void getUsers() throws Exception { + perform(get("/users")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("users")) + .andExpect(forwardedUrl("/WEB-INF/jsp/users.jsp")) + .andExpect(model().attribute("users", hasSize(3))) + .andExpect(model().attribute("users", hasItem( + allOf( + hasProperty("id", is(START_SEQ)), + hasProperty("name", is(UserTestData.user.getName())) + ) + ))); + } +} \ No newline at end of file From 0cdab7abe433a69a399943052a316b849f976038 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 25 Mar 2023 22:40:47 +0700 Subject: [PATCH 073/125] apply patch 7_10 --- pom.xml | 23 +++++----- .../javawebinar/topjava/TimingExtension.java | 36 ++++++++++++++++ .../ru/javawebinar/topjava/TimingRules.java | 42 ------------------- .../service/AbstractMealServiceTest.java | 36 ++++++++-------- .../topjava/service/AbstractServiceTest.java | 26 ++++-------- .../service/AbstractUserServiceTest.java | 24 +++++------ .../datajpa/DataJpaMealServiceTest.java | 12 +++--- .../datajpa/DataJpaUserServiceTest.java | 12 +++--- .../service/jdbc/JdbcMealServiceTest.java | 2 +- .../service/jdbc/JdbcUserServiceTest.java | 2 +- .../service/jpa/JpaMealServiceTest.java | 2 +- .../service/jpa/JpaUserServiceTest.java | 2 +- .../topjava/web/AbstractControllerTest.java | 11 ++--- .../topjava/web/RootControllerTest.java | 11 ++--- ...InMemoryAdminRestControllerSpringTest.java | 27 ++++++------ .../user/InMemoryAdminRestControllerTest.java | 24 +++++------ 16 files changed, 135 insertions(+), 157 deletions(-) create mode 100644 src/test/java/ru/javawebinar/topjava/TimingExtension.java delete mode 100644 src/test/java/ru/javawebinar/topjava/TimingRules.java diff --git a/pom.xml b/pom.xml index c5736d38a662..718d407a07ec 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,7 @@ 42.5.3 - 4.13.2 + 5.9.2 3.24.2 1.3 @@ -58,6 +58,7 @@ + org.apache.maven.plugins maven-surefire-plugin 2.22.2 @@ -214,16 +215,10 @@ - junit - junit - ${junit.version} + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} test - - - hamcrest-core - org.hamcrest - - org.hamcrest @@ -242,6 +237,14 @@ ${assertj.version} test + + + + org.junit.platform + junit-platform-launcher + 1.9.2 + test + diff --git a/src/test/java/ru/javawebinar/topjava/TimingExtension.java b/src/test/java/ru/javawebinar/topjava/TimingExtension.java new file mode 100644 index 000000000000..cee6ae92c6b8 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/TimingExtension.java @@ -0,0 +1,36 @@ +package ru.javawebinar.topjava; + +import org.junit.jupiter.api.extension.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StopWatch; + +public class TimingExtension implements + BeforeTestExecutionCallback, AfterTestExecutionCallback, BeforeAllCallback, AfterAllCallback { + + private static final Logger log = LoggerFactory.getLogger("result"); + + private StopWatch stopWatch; + + @Override + public void beforeAll(ExtensionContext extensionContext) { + stopWatch = new StopWatch("Execution time of " + extensionContext.getRequiredTestClass().getSimpleName()); + } + + @Override + public void beforeTestExecution(ExtensionContext extensionContext) { + String testName = extensionContext.getDisplayName(); + log.info("\nStart " + testName); + stopWatch.start(testName); + } + + @Override + public void afterTestExecution(ExtensionContext extensionContext) { + stopWatch.stop(); + } + + @Override + public void afterAll(ExtensionContext extensionContext) { + log.info('\n' + stopWatch.prettyPrint() + '\n'); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/TimingRules.java b/src/test/java/ru/javawebinar/topjava/TimingRules.java deleted file mode 100644 index fdd3d8779dbc..000000000000 --- a/src/test/java/ru/javawebinar/topjava/TimingRules.java +++ /dev/null @@ -1,42 +0,0 @@ -package ru.javawebinar.topjava; - -import org.junit.rules.ExternalResource; -import org.junit.rules.Stopwatch; -import org.junit.runner.Description; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.concurrent.TimeUnit; - -public class TimingRules { - private static final Logger log = LoggerFactory.getLogger("result"); - - private static final StringBuilder results = new StringBuilder(); - - // http://stackoverflow.com/questions/14892125/what-is-the-best-practice-to-determine-the-execution-time-of-the-bussiness-relev - public static final Stopwatch STOPWATCH = new Stopwatch() { - @Override - protected void finished(long nanos, Description description) { - String result = String.format("%-95s %7d", description.getDisplayName(), TimeUnit.NANOSECONDS.toMillis(nanos)); - results.append(result).append('\n'); - log.info(result + " ms\n"); - } - }; - - // https://dzone.com/articles/applying-new-jdk-11-string-methods - private static final String DELIM = "-".repeat(103); - - public static final ExternalResource SUMMARY = new ExternalResource() { - @Override - protected void before() throws Throwable { - results.setLength(0); - } - - @Override - protected void after() { - log.info("\n" + DELIM + - "\nTest Duration, ms" + - "\n" + DELIM + "\n" + results + DELIM + "\n"); - } - }; -} diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java index b19ffabe8e09..6b00e448152b 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java @@ -1,7 +1,7 @@ package ru.javawebinar.topjava.service; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import ru.javawebinar.topjava.model.Meal; @@ -12,7 +12,7 @@ import java.time.Month; import static java.time.LocalDateTime.of; -import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertThrows; import static ru.javawebinar.topjava.MealTestData.*; import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; import static ru.javawebinar.topjava.UserTestData.USER_ID; @@ -23,23 +23,23 @@ public abstract class AbstractMealServiceTest extends AbstractServiceTest { protected MealService service; @Test - public void delete() { + void delete() { service.delete(MEAL1_ID, USER_ID); assertThrows(NotFoundException.class, () -> service.get(MEAL1_ID, USER_ID)); } @Test - public void deleteNotFound() { + void deleteNotFound() { assertThrows(NotFoundException.class, () -> service.delete(NOT_FOUND, USER_ID)); } @Test - public void deleteNotOwn() { + void deleteNotOwn() { assertThrows(NotFoundException.class, () -> service.delete(MEAL1_ID, ADMIN_ID)); } @Test - public void create() { + void create() { Meal created = service.create(getNew(), USER_ID); int newId = created.id(); Meal newMeal = getNew(); @@ -49,48 +49,48 @@ public void create() { } @Test - public void duplicateDateTimeCreate() { + void duplicateDateTimeCreate() { assertThrows(DataAccessException.class, () -> service.create(new Meal(null, meal1.getDateTime(), "duplicate", 100), USER_ID)); } @Test - public void get() { + void get() { Meal actual = service.get(ADMIN_MEAL_ID, ADMIN_ID); MEAL_MATCHER.assertMatch(actual, adminMeal1); } @Test - public void getNotFound() { + void getNotFound() { assertThrows(NotFoundException.class, () -> service.get(NOT_FOUND, USER_ID)); } @Test - public void getNotOwn() { + void getNotOwn() { assertThrows(NotFoundException.class, () -> service.get(MEAL1_ID, ADMIN_ID)); } @Test - public void update() { + void update() { Meal updated = getUpdated(); service.update(updated, USER_ID); MEAL_MATCHER.assertMatch(service.get(MEAL1_ID, USER_ID), getUpdated()); } @Test - public void updateNotOwn() { + void updateNotOwn() { NotFoundException exception = assertThrows(NotFoundException.class, () -> service.update(getUpdated(), ADMIN_ID)); - Assert.assertEquals("Not found entity with id=" + MEAL1_ID, exception.getMessage()); + Assertions.assertEquals("Not found entity with id=" + MEAL1_ID, exception.getMessage()); MEAL_MATCHER.assertMatch(service.get(MEAL1_ID, USER_ID), meal1); } @Test - public void getAll() { + void getAll() { MEAL_MATCHER.assertMatch(service.getAll(USER_ID), meals); } @Test - public void getBetweenInclusive() { + void getBetweenInclusive() { MEAL_MATCHER.assertMatch(service.getBetweenInclusive( LocalDate.of(2020, Month.JANUARY, 30), LocalDate.of(2020, Month.JANUARY, 30), USER_ID), @@ -98,12 +98,12 @@ public void getBetweenInclusive() { } @Test - public void getBetweenWithNullDates() { + void getBetweenWithNullDates() { MEAL_MATCHER.assertMatch(service.getBetweenInclusive(null, null, USER_ID), meals); } @Test - public void createWithException() throws Exception { + void createWithException() throws Exception { validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), " ", 300), USER_ID)); validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, null, "Description", 300), USER_ID)); validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Description", 9), USER_ID)); diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java index 06d99de9734e..06f72ef86ac8 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java @@ -1,36 +1,26 @@ package ru.javawebinar.topjava.service; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.rules.ExternalResource; -import org.junit.rules.Stopwatch; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.extension.ExtendWith; 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 org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import ru.javawebinar.topjava.ActiveDbProfileResolver; -import ru.javawebinar.topjava.TimingRules; +import ru.javawebinar.topjava.TimingExtension; -import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertThrows; import static ru.javawebinar.topjava.util.ValidationUtil.getRootCause; -@ContextConfiguration({ +@SpringJUnitConfig(locations = { "classpath:spring/spring-app.xml", "classpath:spring/spring-db.xml" }) -@RunWith(SpringRunner.class) -@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) +//@ExtendWith(SpringExtension.class) @ActiveProfiles(resolver = ActiveDbProfileResolver.class) +@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) +@ExtendWith(TimingExtension.class) public abstract class AbstractServiceTest { - @ClassRule - public static ExternalResource summary = TimingRules.SUMMARY; - - @Rule - public Stopwatch stopwatch = TimingRules.STOPWATCH; - // Check root cause in JUnit: https://github.com/junit-team/junit4/pull/778 protected void validateRootCause(Class rootExceptionClass, Runnable runnable) { assertThrows(rootExceptionClass, () -> { diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java index 6bd0772a9be8..fe6838b5f26f 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -1,6 +1,6 @@ package ru.javawebinar.topjava.service; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import ru.javawebinar.topjava.model.Role; @@ -12,7 +12,7 @@ import java.util.List; import java.util.Set; -import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertThrows; import static ru.javawebinar.topjava.UserTestData.*; public abstract class AbstractUserServiceTest extends AbstractServiceTest { @@ -21,7 +21,7 @@ public abstract class AbstractUserServiceTest extends AbstractServiceTest { protected UserService service; @Test - public void create() { + void create() { User created = service.create(getNew()); int newId = created.id(); User newUser = getNew(); @@ -31,54 +31,54 @@ public void create() { } @Test - public void duplicateMailCreate() { + void duplicateMailCreate() { assertThrows(DataAccessException.class, () -> service.create(new User(null, "Duplicate", "user@yandex.ru", "newPass", Role.USER))); } @Test - public void delete() { + void delete() { service.delete(USER_ID); assertThrows(NotFoundException.class, () -> service.get(USER_ID)); } @Test - public void deletedNotFound() { + void deletedNotFound() { assertThrows(NotFoundException.class, () -> service.delete(NOT_FOUND)); } @Test - public void get() { + void get() { User user = service.get(ADMIN_ID); USER_MATCHER.assertMatch(user, admin); } @Test - public void getNotFound() { + void getNotFound() { assertThrows(NotFoundException.class, () -> service.get(NOT_FOUND)); } @Test - public void getByEmail() { + void getByEmail() { User user = service.getByEmail("admin@gmail.com"); USER_MATCHER.assertMatch(user, admin); } @Test - public void update() { + void update() { User updated = getUpdated(); service.update(updated); USER_MATCHER.assertMatch(service.get(USER_ID), getUpdated()); } @Test - public void getAll() { + void getAll() { List all = service.getAll(); USER_MATCHER.assertMatch(all, admin, guest, user); } @Test - public void createWithException() throws Exception { + void createWithException() throws Exception { validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, " ", "mail@yandex.ru", "password", Role.USER))); validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", " ", "password", Role.USER))); validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", "mail@yandex.ru", " ", Role.USER))); diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java index 83cbd7cc25e0..161c93fb5296 100644 --- a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java @@ -1,7 +1,7 @@ package ru.javawebinar.topjava.service.datajpa; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.springframework.test.context.ActiveProfiles; import ru.javawebinar.topjava.MealTestData; import ru.javawebinar.topjava.model.Meal; @@ -13,17 +13,17 @@ import static ru.javawebinar.topjava.UserTestData.*; @ActiveProfiles(DATAJPA) -public class DataJpaMealServiceTest extends AbstractMealServiceTest { +class DataJpaMealServiceTest extends AbstractMealServiceTest { @Test - public void getWithUser() { + void getWithUser() { Meal adminMeal = service.getWithUser(ADMIN_MEAL_ID, ADMIN_ID); MEAL_MATCHER.assertMatch(adminMeal, adminMeal1); USER_MATCHER.assertMatch(adminMeal.getUser(), admin); } @Test - public void getWithUserNotFound() { - Assert.assertThrows(NotFoundException.class, + void getWithUserNotFound() { + Assertions.assertThrows(NotFoundException.class, () -> service.getWithUser(MealTestData.NOT_FOUND, ADMIN_ID)); } } diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java index dd7ad4ed087a..d8a1f4106c1f 100644 --- a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java @@ -1,7 +1,7 @@ package ru.javawebinar.topjava.service.datajpa; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.springframework.test.context.ActiveProfiles; import ru.javawebinar.topjava.MealTestData; import ru.javawebinar.topjava.UserTestData; @@ -14,17 +14,17 @@ import static ru.javawebinar.topjava.UserTestData.*; @ActiveProfiles(DATAJPA) -public class DataJpaUserServiceTest extends AbstractUserServiceTest { +class DataJpaUserServiceTest extends AbstractUserServiceTest { @Test - public void getWithMeals() { + void getWithMeals() { User admin = service.getWithMeals(ADMIN_ID); USER_MATCHER.assertMatch(admin, UserTestData.admin); MEAL_MATCHER.assertMatch(admin.getMeals(), MealTestData.adminMeal2, MealTestData.adminMeal1); } @Test - public void getWithMealsNotFound() { - Assert.assertThrows(NotFoundException.class, + void getWithMealsNotFound() { + Assertions.assertThrows(NotFoundException.class, () -> service.getWithMeals(NOT_FOUND)); } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java index 9ff4ae615b2b..aef588264f71 100644 --- a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java @@ -6,5 +6,5 @@ import static ru.javawebinar.topjava.Profiles.JDBC; @ActiveProfiles(JDBC) -public class JdbcMealServiceTest extends AbstractMealServiceTest { +class JdbcMealServiceTest extends AbstractMealServiceTest { } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java index 419f68ed1098..62ca7668cf67 100644 --- a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java @@ -6,5 +6,5 @@ import static ru.javawebinar.topjava.Profiles.JDBC; @ActiveProfiles(JDBC) -public class JdbcUserServiceTest extends AbstractUserServiceTest { +class JdbcUserServiceTest extends AbstractUserServiceTest { } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java index 70e7bf865421..aaf5dcda960e 100644 --- a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java @@ -6,5 +6,5 @@ import static ru.javawebinar.topjava.Profiles.JPA; @ActiveProfiles(JPA) -public class JpaMealServiceTest extends AbstractMealServiceTest { +class JpaMealServiceTest extends AbstractMealServiceTest { } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java index d1b3e4699785..6d1cd91543fc 100644 --- a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java @@ -6,5 +6,5 @@ import static ru.javawebinar.topjava.Profiles.JPA; @ActiveProfiles(JPA) -public class JpaUserServiceTest extends AbstractUserServiceTest { +class JpaUserServiceTest extends AbstractUserServiceTest { } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java index 3ec192baa614..6fa9b1d4c81b 100644 --- a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java @@ -1,11 +1,8 @@ package ru.javawebinar.topjava.web; -import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; @@ -18,13 +15,13 @@ import javax.annotation.PostConstruct; -@ContextConfiguration({ +@SpringJUnitWebConfig(locations = { "classpath:spring/spring-app.xml", "classpath:spring/spring-mvc.xml", "classpath:spring/spring-db.xml" }) -@WebAppConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +//@WebAppConfiguration +//@ExtendWith(SpringExtension.class) @Transactional @ActiveProfiles(resolver = ActiveDbProfileResolver.class, profiles = Profiles.REPOSITORY_IMPLEMENTATION) public abstract class AbstractControllerTest { diff --git a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java index cca2827e887f..b7759340494f 100644 --- a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java @@ -1,21 +1,18 @@ package ru.javawebinar.topjava.web; -import org.junit.Test; +import org.junit.jupiter.api.Test; import ru.javawebinar.topjava.UserTestData; import static org.hamcrest.Matchers.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; -public class RootControllerTest extends AbstractControllerTest { +class RootControllerTest extends AbstractControllerTest { @Test - public void getUsers() throws Exception { + void getUsers() throws Exception { perform(get("/users")) .andDo(print()) .andExpect(status().isOk()) diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java index 626200081cc4..fc030a503144 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java @@ -1,21 +1,18 @@ package ru.javawebinar.topjava.web.user; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; 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/inmemory.xml"}) -@RunWith(SpringRunner.class) -public class InMemoryAdminRestControllerSpringTest { +@SpringJUnitConfig(locations = {"classpath:spring/inmemory.xml"}) +class InMemoryAdminRestControllerSpringTest { @Autowired private AdminRestController controller; @@ -23,19 +20,19 @@ public class InMemoryAdminRestControllerSpringTest { @Autowired private InMemoryUserRepository repository; - @Before - public void setUp() { + @BeforeEach + void setUp() { repository.init(); } @Test - public void delete() { + void delete() { controller.delete(USER_ID); - Assert.assertNull(repository.get(USER_ID)); + Assertions.assertNull(repository.get(USER_ID)); } @Test - public void deleteNotFound() { - Assert.assertThrows(NotFoundException.class, () -> controller.delete(NOT_FOUND)); + void deleteNotFound() { + Assertions.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 index 6c12a614d7af..c41fa0e6bb4d 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java @@ -1,6 +1,6 @@ package ru.javawebinar.topjava.web.user; -import org.junit.*; +import org.junit.jupiter.api.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ConfigurableApplicationContext; @@ -13,42 +13,42 @@ import static ru.javawebinar.topjava.UserTestData.NOT_FOUND; import static ru.javawebinar.topjava.UserTestData.USER_ID; -public class InMemoryAdminRestControllerTest { +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() { + @BeforeAll + static void beforeClass() { appCtx = new ClassPathXmlApplicationContext("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() { + @AfterAll + static void afterClass() { // May cause during JUnit "Cache is not alive (STATUS_SHUTDOWN)" as JUnit share Spring context for speed // http://stackoverflow.com/questions/16281802/ehcache-shutdown-causing-an-exception-while-running-test-suite // appCtx.close(); } - @Before - public void setUp() { + @BeforeEach + void setup() { // re-initialize repository.init(); } @Test - public void delete() { + void delete() { controller.delete(USER_ID); - Assert.assertNull(repository.get(USER_ID)); + Assertions.assertNull(repository.get(USER_ID)); } @Test - public void deleteNotFound() { - Assert.assertThrows(NotFoundException.class, () -> controller.delete(NOT_FOUND)); + void deleteNotFound() { + Assertions.assertThrows(NotFoundException.class, () -> controller.delete(NOT_FOUND)); } } \ No newline at end of file From 5bf6f6921ebecd23bd7f2b2e69a69ba744ea78d8 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 25 Mar 2023 22:48:33 +0700 Subject: [PATCH 074/125] apply patch 7_11 --- .../topjava/web/user/AdminRestController.java | 37 ++++++++++++++----- .../web/user/ProfileRestController.java | 15 ++++++-- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java index b37a8ed6c8a5..095ced3b0e1f 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java @@ -1,40 +1,59 @@ package ru.javawebinar.topjava.web.user; -import org.springframework.stereotype.Controller; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import ru.javawebinar.topjava.model.User; +import java.net.URI; import java.util.List; -@Controller +@RestController +@RequestMapping(value = AdminRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE) public class AdminRestController extends AbstractUserController { + static final String REST_URL = "/rest/admin/users"; + @Override + @GetMapping public List getAll() { return super.getAll(); } @Override - public User get(int id) { + @GetMapping("/{id}") + public User get(@PathVariable int id) { return super.get(id); } - @Override - public User create(User user) { - return super.create(user); + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createWithLocation(@RequestBody User user) { + User created = super.create(user); + URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() + .path(REST_URL + "/{id}") + .buildAndExpand(created.getId()).toUri(); + return ResponseEntity.created(uriOfNewResource).body(created); } @Override - public void delete(int id) { + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { super.delete(id); } @Override - public void update(User user, int id) { + @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void update(@RequestBody User user, @PathVariable int id) { super.update(user, id); } @Override - public User getByMail(String email) { + @GetMapping("/by-email") + public User getByMail(@RequestParam 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 index 7d3702c31c46..4887ff8b070c 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -1,22 +1,31 @@ package ru.javawebinar.topjava.web.user; -import org.springframework.stereotype.Controller; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; import ru.javawebinar.topjava.model.User; import static ru.javawebinar.topjava.web.SecurityUtil.authUserId; -@Controller +@RestController +@RequestMapping(value = ProfileRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE) public class ProfileRestController extends AbstractUserController { + static final String REST_URL = "/rest/profile"; + @GetMapping public User get() { return super.get(authUserId()); } + @DeleteMapping + @ResponseStatus(HttpStatus.NO_CONTENT) public void delete() { super.delete(authUserId()); } - public void update(User user) { + @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void update(@RequestBody User user) { super.update(user, authUserId()); } } \ No newline at end of file From 5f0b3f847f0205fb6ab5195c3c5a4d24f9198e83 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 25 Mar 2023 23:19:09 +0700 Subject: [PATCH 075/125] apply patch 7_12 --- pom.xml | 8 ++++++ .../topjava/model/AbstractBaseEntity.java | 5 ++++ .../ru/javawebinar/topjava/model/User.java | 2 ++ .../web/user/AdminRestControllerTest.java | 25 +++++++++++++++++++ 4 files changed, 40 insertions(+) create mode 100644 src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java diff --git a/pom.xml b/pom.xml index 718d407a07ec..3ccc8cd45a82 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,7 @@ UTF-8 5.3.25 2.7.8 + 2.14.2 9.0.72 @@ -213,6 +214,13 @@ spring-webmvc + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + org.junit.jupiter diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java index acb11612281c..901798cb753f 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -1,14 +1,19 @@ package ru.javawebinar.topjava.model; +import com.fasterxml.jackson.annotation.JsonAutoDetect; import org.hibernate.Hibernate; import org.springframework.data.domain.Persistable; import org.springframework.util.Assert; import javax.persistence.*; +import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY; +import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE; + @MappedSuperclass // http://stackoverflow.com/questions/594597/hibernate-annotations-which-is-better-field-or-property-access @Access(AccessType.FIELD) +@JsonAutoDetect(fieldVisibility = ANY, getterVisibility = NONE, isGetterVisibility = NONE, setterVisibility = NONE) public abstract class AbstractBaseEntity implements Persistable { public static final int START_SEQ = 100000; diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index c24386a9462d..c5bae71be93a 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -4,6 +4,7 @@ import org.hibernate.annotations.*; import org.hibernate.validator.constraints.Range; import org.springframework.util.CollectionUtils; +import com.fasterxml.jackson.annotation.JsonIgnore; import javax.persistence.Entity; import javax.persistence.NamedQueries; @@ -70,6 +71,7 @@ public class User extends AbstractNamedEntity { @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")//, cascade = CascadeType.REMOVE, orphanRemoval = true) @OrderBy("dateTime DESC") @OnDelete(action = OnDeleteAction.CASCADE) //https://stackoverflow.com/a/44988100/548473 + @JsonIgnore private List meals; public User() { diff --git a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java new file mode 100644 index 000000000000..0ddd38eaa240 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java @@ -0,0 +1,25 @@ +package ru.javawebinar.topjava.web.user; + +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import ru.javawebinar.topjava.web.AbstractControllerTest; + +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; + +class AdminRestControllerTest extends AbstractControllerTest { + + private static final String REST_URL = AdminRestController.REST_URL + '/'; + + @Test + void get() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + ADMIN_ID)) + .andExpect(status().isOk()) + .andDo(print()) + // https://jira.spring.io/browse/SPR-14472 + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)); + } +} \ No newline at end of file From 8e2ff92c18d400a7ac0d3b171466f6754855b8fc Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 25 Mar 2023 23:26:58 +0700 Subject: [PATCH 076/125] apply patch 7_13 --- pom.xml | 10 ++++++ .../topjava/model/AbstractBaseEntity.java | 6 +--- .../ru/javawebinar/topjava/model/User.java | 3 +- .../topjava/web/json/JacksonObjectMapper.java | 31 +++++++++++++++++++ src/main/resources/spring/spring-mvc.xml | 11 ++++++- 5 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java diff --git a/pom.xml b/pom.xml index 3ccc8cd45a82..6ef68919fea1 100644 --- a/pom.xml +++ b/pom.xml @@ -220,6 +220,16 @@ jackson-databind ${jackson.version} + + com.fasterxml.jackson.datatype + jackson-datatype-hibernate5 + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java index 901798cb753f..7cdc077bcdcf 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -1,19 +1,15 @@ package ru.javawebinar.topjava.model; -import com.fasterxml.jackson.annotation.JsonAutoDetect; import org.hibernate.Hibernate; import org.springframework.data.domain.Persistable; import org.springframework.util.Assert; import javax.persistence.*; -import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY; -import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE; - @MappedSuperclass // http://stackoverflow.com/questions/594597/hibernate-annotations-which-is-better-field-or-property-access @Access(AccessType.FIELD) -@JsonAutoDetect(fieldVisibility = ANY, getterVisibility = NONE, isGetterVisibility = NONE, setterVisibility = NONE) +//@JsonAutoDetect(fieldVisibility = ANY, getterVisibility = NONE, isGetterVisibility = NONE, setterVisibility = NONE) public abstract class AbstractBaseEntity implements Persistable { public static final int START_SEQ = 100000; diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index c5bae71be93a..3b83fd50661c 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -4,7 +4,6 @@ import org.hibernate.annotations.*; import org.hibernate.validator.constraints.Range; import org.springframework.util.CollectionUtils; -import com.fasterxml.jackson.annotation.JsonIgnore; import javax.persistence.Entity; import javax.persistence.NamedQueries; @@ -71,7 +70,7 @@ public class User extends AbstractNamedEntity { @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")//, cascade = CascadeType.REMOVE, orphanRemoval = true) @OrderBy("dateTime DESC") @OnDelete(action = OnDeleteAction.CASCADE) //https://stackoverflow.com/a/44988100/548473 - @JsonIgnore +// @JsonIgnore private List meals; public User() { diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java b/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java new file mode 100644 index 000000000000..d91b1007b012 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java @@ -0,0 +1,31 @@ +package ru.javawebinar.topjava.web.json; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +/** + *

    + * Handling Hibernate lazy-loading + * + * @link https://github.com/FasterXML/jackson + * @link https://github.com/FasterXML/jackson-datatype-hibernate + * @link https://github.com/FasterXML/jackson-docs/wiki/JacksonHowToCustomSerializers + */ +public class JacksonObjectMapper extends ObjectMapper { + + private JacksonObjectMapper() { + registerModule(new Hibernate5Module()); + + registerModule(new JavaTimeModule()); + configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + + setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); + setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + setSerializationInclusion(JsonInclude.Include.NON_NULL); + } +} \ No newline at end of file diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml index aa5599feaca4..1d0b485210f7 100644 --- a/src/main/resources/spring/spring-mvc.xml +++ b/src/main/resources/spring/spring-mvc.xml @@ -5,7 +5,16 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> - + + + + + + + + + + From 10a654412fb73aebbf5e6af4c719f7488fa944a1 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 25 Mar 2023 23:49:29 +0700 Subject: [PATCH 077/125] apply patch 7_14 --- pom.xml | 8 ++++ .../topjava/web/json/JacksonObjectMapper.java | 6 +++ .../topjava/web/json/JsonUtil.java | 46 +++++++++++++++++++ src/main/resources/spring/spring-mvc.xml | 7 ++- .../topjava/web/json/JsonUtilTest.java | 30 ++++++++++++ .../web/user/AdminRestControllerTest.java | 5 +- 6 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java create mode 100644 src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java diff --git a/pom.xml b/pom.xml index 6ef68919fea1..24ec3f759ff6 100644 --- a/pom.xml +++ b/pom.xml @@ -38,6 +38,7 @@ 5.9.2 3.24.2 1.3 + 1.5.1 @@ -244,6 +245,13 @@ ${hamcrest.version} test + + org.skyscreamer + jsonassert + ${jsonassert.version} + test + + org.springframework spring-test diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java b/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java index d91b1007b012..8237df93bffe 100644 --- a/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java +++ b/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java @@ -18,6 +18,8 @@ */ public class JacksonObjectMapper extends ObjectMapper { + private static final ObjectMapper MAPPER = new JacksonObjectMapper(); + private JacksonObjectMapper() { registerModule(new Hibernate5Module()); @@ -28,4 +30,8 @@ private JacksonObjectMapper() { setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); setSerializationInclusion(JsonInclude.Include.NON_NULL); } + + public static ObjectMapper getMapper() { + return MAPPER; + } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java new file mode 100644 index 000000000000..3bff99bb93f0 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java @@ -0,0 +1,46 @@ +package ru.javawebinar.topjava.web.json; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectReader; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static ru.javawebinar.topjava.web.json.JacksonObjectMapper.getMapper; + +public class JsonUtil { + + public static List readValues(String json, Class clazz) { + ObjectReader reader = getMapper().readerFor(clazz); + try { + return reader.readValues(json).readAll(); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid read array from JSON:\n'" + json + "'", e); + } + } + + public static T readValue(String json, Class clazz) { + try { + return getMapper().readValue(json, clazz); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid read from JSON:\n'" + json + "'", e); + } + } + + public static String writeValue(T obj) { + try { + return getMapper().writeValueAsString(obj); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Invalid write to JSON:\n'" + obj + "'", e); + } + } + + public static String writeIgnoreProps(T obj, String... ignoreProps) { + Map map = getMapper().convertValue(obj, new TypeReference<>() {}); + map.keySet().removeAll(Set.of(ignoreProps)); + return writeValue(map); + } +} \ No newline at end of file diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml index 1d0b485210f7..7b00e74310f6 100644 --- a/src/main/resources/spring/spring-mvc.xml +++ b/src/main/resources/spring/spring-mvc.xml @@ -5,17 +5,16 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> + + - - - + - diff --git a/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java b/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java new file mode 100644 index 000000000000..540586d114fd --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java @@ -0,0 +1,30 @@ +package ru.javawebinar.topjava.web.json; + +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.javawebinar.topjava.model.Meal; + +import java.util.List; + +import static ru.javawebinar.topjava.MealTestData.*; + +class JsonUtilTest { + private static final Logger log = LoggerFactory.getLogger(JsonUtilTest.class); + + @Test + void readWriteValue() { + String json = JsonUtil.writeValue(adminMeal1); + log.info(json); + Meal meal = JsonUtil.readValue(json, Meal.class); + MEAL_MATCHER.assertMatch(meal, adminMeal1); + } + + @Test + void readWriteValues() { + String json = JsonUtil.writeValue(meals); + log.info(json); + List actual = JsonUtil.readValues(json, Meal.class); + MEAL_MATCHER.assertMatch(actual, meals); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java index 0ddd38eaa240..0202bce2c526 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java @@ -9,6 +9,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; +import static ru.javawebinar.topjava.UserTestData.admin; +import static ru.javawebinar.topjava.web.json.JsonUtil.writeIgnoreProps; class AdminRestControllerTest extends AbstractControllerTest { @@ -20,6 +22,7 @@ void get() throws Exception { .andExpect(status().isOk()) .andDo(print()) // https://jira.spring.io/browse/SPR-14472 - .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)); + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(content().json(writeIgnoreProps(admin, "registered"))); } } \ No newline at end of file From 32587853112a8a794ab8dd22739320ae099ffd60 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sat, 25 Mar 2023 23:49:40 +0700 Subject: [PATCH 078/125] apply patch 7_15 --- pom.xml | 11 +-- .../topjava/web/json/JsonUtil.java | 9 --- .../javawebinar/topjava/MatcherFactory.java | 36 +++++++++- .../ru/javawebinar/topjava/MealTestData.java | 2 +- .../ru/javawebinar/topjava/UserTestData.java | 2 +- .../topjava/web/RootControllerTest.java | 23 ++++--- .../web/user/AdminRestControllerTest.java | 67 +++++++++++++++++-- .../web/user/ProfileRestControllerTest.java | 48 +++++++++++++ 8 files changed, 161 insertions(+), 37 deletions(-) create mode 100644 src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java diff --git a/pom.xml b/pom.xml index 24ec3f759ff6..047dffff52e3 100644 --- a/pom.xml +++ b/pom.xml @@ -37,8 +37,7 @@ 5.9.2 3.24.2 - 1.3 - 1.5.1 + 2.2 @@ -241,16 +240,10 @@ org.hamcrest - hamcrest-all + hamcrest-core ${hamcrest.version} test - - org.skyscreamer - jsonassert - ${jsonassert.version} - test - org.springframework diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java index 3bff99bb93f0..fda04590d618 100644 --- a/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java +++ b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java @@ -1,13 +1,10 @@ package ru.javawebinar.topjava.web.json; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectReader; import java.io.IOException; import java.util.List; -import java.util.Map; -import java.util.Set; import static ru.javawebinar.topjava.web.json.JacksonObjectMapper.getMapper; @@ -37,10 +34,4 @@ public static String writeValue(T obj) { throw new IllegalStateException("Invalid write to JSON:\n'" + obj + "'", e); } } - - public static String writeIgnoreProps(T obj, String... ignoreProps) { - Map map = getMapper().convertValue(obj, new TypeReference<>() {}); - map.keySet().removeAll(Set.of(ignoreProps)); - return writeValue(map); - } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/MatcherFactory.java b/src/test/java/ru/javawebinar/topjava/MatcherFactory.java index c5f04086a356..40f3d8c22b77 100644 --- a/src/test/java/ru/javawebinar/topjava/MatcherFactory.java +++ b/src/test/java/ru/javawebinar/topjava/MatcherFactory.java @@ -1,5 +1,11 @@ package ru.javawebinar.topjava; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.ResultMatcher; +import ru.javawebinar.topjava.web.json.JsonUtil; + +import java.io.UnsupportedEncodingException; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -8,16 +14,19 @@ * Factory for creating test matchers. *

    * Comparing actual and expected objects via AssertJ + * Support converting json MvcResult to objects for comparation. */ public class MatcherFactory { - public static Matcher usingIgnoringFieldsComparator(String... fieldsToIgnore) { - return new Matcher<>(fieldsToIgnore); + public static Matcher usingIgnoringFieldsComparator(Class clazz, String... fieldsToIgnore) { + return new Matcher<>(clazz, fieldsToIgnore); } public static class Matcher { + private final Class clazz; private final String[] fieldsToIgnore; - private Matcher(String... fieldsToIgnore) { + private Matcher(Class clazz, String... fieldsToIgnore) { + this.clazz = clazz; this.fieldsToIgnore = fieldsToIgnore; } @@ -33,5 +42,26 @@ public final void assertMatch(Iterable actual, T... expected) { public void assertMatch(Iterable actual, Iterable expected) { assertThat(actual).usingRecursiveFieldByFieldElementComparatorIgnoringFields(fieldsToIgnore).isEqualTo(expected); } + + public ResultMatcher contentJson(T expected) { + return result -> assertMatch(JsonUtil.readValue(getContent(result), clazz), expected); + } + + @SafeVarargs + public final ResultMatcher contentJson(T... expected) { + return contentJson(List.of(expected)); + } + + public ResultMatcher contentJson(Iterable expected) { + return result -> assertMatch(JsonUtil.readValues(getContent(result), clazz), expected); + } + + public T readFromJson(ResultActions action) throws UnsupportedEncodingException { + return JsonUtil.readValue(getContent(action.andReturn()), clazz); + } + + private static String getContent(MvcResult result) throws UnsupportedEncodingException { + return result.getResponse().getContentAsString(); + } } } diff --git a/src/test/java/ru/javawebinar/topjava/MealTestData.java b/src/test/java/ru/javawebinar/topjava/MealTestData.java index d044e3f90a22..c2697db04001 100644 --- a/src/test/java/ru/javawebinar/topjava/MealTestData.java +++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java @@ -10,7 +10,7 @@ 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 MatcherFactory.Matcher MEAL_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(Meal.class, "user"); public static final int NOT_FOUND = 10; public static final int MEAL1_ID = START_SEQ + 3; diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java index 380672440ba8..2859645521f0 100644 --- a/src/test/java/ru/javawebinar/topjava/UserTestData.java +++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java @@ -9,7 +9,7 @@ import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; public class UserTestData { - public static final MatcherFactory.Matcher USER_MATCHER = MatcherFactory.usingIgnoringFieldsComparator("registered", "meals"); + public static final MatcherFactory.Matcher USER_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(User.class, "registered", "meals"); public static final int USER_ID = START_SEQ; public static final int ADMIN_ID = START_SEQ + 1; diff --git a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java index b7759340494f..2bec37dda60e 100644 --- a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java @@ -1,13 +1,15 @@ package ru.javawebinar.topjava.web; +import org.assertj.core.matcher.AssertionMatcher; import org.junit.jupiter.api.Test; -import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.User; + +import java.util.List; -import static org.hamcrest.Matchers.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; +import static ru.javawebinar.topjava.UserTestData.*; class RootControllerTest extends AbstractControllerTest { @@ -18,12 +20,13 @@ void getUsers() throws Exception { .andExpect(status().isOk()) .andExpect(view().name("users")) .andExpect(forwardedUrl("/WEB-INF/jsp/users.jsp")) - .andExpect(model().attribute("users", hasSize(3))) - .andExpect(model().attribute("users", hasItem( - allOf( - hasProperty("id", is(START_SEQ)), - hasProperty("name", is(UserTestData.user.getName())) - ) - ))); + .andExpect(model().attribute("users", + new AssertionMatcher>() { + @Override + public void assertion(List actual) throws AssertionError { + USER_MATCHER.assertMatch(actual, admin, guest, user); + } + } + )); } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java index 0202bce2c526..7563fe05c089 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java @@ -1,21 +1,30 @@ package ru.javawebinar.topjava.web.user; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.service.UserService; +import ru.javawebinar.topjava.util.exception.NotFoundException; import ru.javawebinar.topjava.web.AbstractControllerTest; +import ru.javawebinar.topjava.web.json.JsonUtil; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; -import static ru.javawebinar.topjava.UserTestData.admin; -import static ru.javawebinar.topjava.web.json.JsonUtil.writeIgnoreProps; +import static ru.javawebinar.topjava.UserTestData.*; class AdminRestControllerTest extends AbstractControllerTest { private static final String REST_URL = AdminRestController.REST_URL + '/'; + @Autowired + private UserService userService; + @Test void get() throws Exception { perform(MockMvcRequestBuilders.get(REST_URL + ADMIN_ID)) @@ -23,6 +32,56 @@ void get() throws Exception { .andDo(print()) // https://jira.spring.io/browse/SPR-14472 .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) - .andExpect(content().json(writeIgnoreProps(admin, "registered"))); + .andExpect(USER_MATCHER.contentJson(admin)); + } + + @Test + void getByEmail() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + "by-email?email=" + user.getEmail())) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(USER_MATCHER.contentJson(user)); + } + + @Test + void delete() throws Exception { + perform(MockMvcRequestBuilders.delete(REST_URL + USER_ID)) + .andDo(print()) + .andExpect(status().isNoContent()); + assertThrows(NotFoundException.class, () -> userService.get(USER_ID)); + } + + @Test + void update() throws Exception { + User updated = UserTestData.getUpdated(); + perform(MockMvcRequestBuilders.put(REST_URL + USER_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(updated))) + .andExpect(status().isNoContent()); + + USER_MATCHER.assertMatch(userService.get(USER_ID), updated); + } + + @Test + void createWithLocation() throws Exception { + User newUser = UserTestData.getNew(); + ResultActions action = perform(MockMvcRequestBuilders.post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(newUser))) + .andExpect(status().isCreated()); + + User created = USER_MATCHER.readFromJson(action); + int newId = created.id(); + newUser.setId(newId); + USER_MATCHER.assertMatch(created, newUser); + USER_MATCHER.assertMatch(userService.get(newId), newUser); + } + + @Test + void getAll() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(USER_MATCHER.contentJson(admin, guest, user)); } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java new file mode 100644 index 000000000000..e8882742da6d --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java @@ -0,0 +1,48 @@ +package ru.javawebinar.topjava.web.user; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.service.UserService; +import ru.javawebinar.topjava.web.AbstractControllerTest; +import ru.javawebinar.topjava.web.json.JsonUtil; + +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static ru.javawebinar.topjava.UserTestData.*; +import static ru.javawebinar.topjava.web.user.ProfileRestController.REST_URL; + +class ProfileRestControllerTest extends AbstractControllerTest { + + @Autowired + private UserService userService; + + @Test + void get() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(USER_MATCHER.contentJson(user)); + } + + @Test + void delete() throws Exception { + perform(MockMvcRequestBuilders.delete(REST_URL)) + .andExpect(status().isNoContent()); + USER_MATCHER.assertMatch(userService.getAll(), admin, guest); + } + + @Test + void update() throws Exception { + User updated = getUpdated(); + perform(MockMvcRequestBuilders.put(REST_URL).contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(updated))) + .andDo(print()) + .andExpect(status().isNoContent()); + + USER_MATCHER.assertMatch(userService.get(USER_ID), updated); + } +} \ No newline at end of file From d5b102342d58c8c8580b369025ef4f0819d681b2 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Mon, 27 Mar 2023 11:55:53 +0700 Subject: [PATCH 079/125] apply patch 7_16 --- config/Topjava-soapui-project.xml | 395 ++++++++++++++++++ .../web/user/ProfileRestController.java | 5 + src/main/resources/spring/spring-mvc.xml | 8 + 3 files changed, 408 insertions(+) create mode 100644 config/Topjava-soapui-project.xml diff --git a/config/Topjava-soapui-project.xml b/config/Topjava-soapui-project.xml new file mode 100644 index 000000000000..f9668d765bb8 --- /dev/null +++ b/config/Topjava-soapui-project.xml @@ -0,0 +1,395 @@ + + + + + + + + http://localhost:8080 + + + + + + + + + text/html;charset=utf-8 + 500 401 + + html + + + application/json + 200 + + ns:Response + + + application/json;charset=UTF-8 + 200 + + Response + + + + <xml-fragment/> + + http://localhost:8080 + + http://localhost/topjava/rest/admin/users + + user@yandex.ru + password + No Authorization + Basic + No Authorization + + + + + + + + + + + application/json + + + + text/html;charset=utf-8 + 500 + + html + + + application/json + 201 + + user:Response + + + application/json;charset=UTF-8 + 201 + + user:Response + + + + <xml-fragment/> + + http://localhost:8080 + {"name": "New2", + "email": "new2@yandex.ru", + "password": "passwordNew", + "roles": ["USER"] + } + + http://localhost/topjava/rest/admin/users + + No Authorization + Basic + No Authorization + + + + + + + + + + + + + + + text/html;charset=utf-8 + 500 + + html + + + application/json + 200 + + ns:Response + + + application/json;charset=UTF-8 + 200 + + ns:Response + + + + <xml-fragment/> + + http://localhost:8080 + + http://localhost/topjava/rest/admin/users/100000 + + No Authorization + Basic + No Authorization + + + + + + + + + + + text/html;charset=utf-8 + 405 500 + + html + + + application/json + + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + <xml-fragment/> + + http://localhost:8080 + {"name": "UserUpdated", + "email": "user@yandex.ru", + "password": "passwordNew", + "roles": ["USER"] + } + + http://localhost/topjava/rest/admin/users/100000 + + Basic + Basic + Global HTTP Settings + + + + + + + + + + + + + + + text/html;charset=utf-8 + 500 + + html + + + application/json + 200 + + ns:Response + + + application/json;charset=UTF-8 + 200 + + prof:Response + + + + <xml-fragment/> + + http://localhost:8080 + + http://localhost/topjava/rest/profile + + Basic + Basic + Global HTTP Settings + + + + + + + + + + + application/json + + + + text/html;charset=utf-8 + 500 405 + + html + + + application/json + 201 + + user:Response + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + <xml-fragment/> + + http://localhost:8080 + {"name": "New777", + "email": "new777@yandex.ru", + "password": "passwordNew", + "roles": ["USER"] + } + + http://localhost/topjava/rest/profile + + No Authorization + Basic + No Authorization + + + + + + + + + + + + 200 + + data + + + text/html;charset=utf-8 + 500 + + html + + + application/json + + + + + 200 + + data + + + + 204 + + data + + + + <xml-fragment/> + + http://localhost:8080 + + http://localhost/topjava/rest/profile + + No Authorization + Basic + No Authorization + + + + + + + + + + + + + + \ 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 index 4887ff8b070c..14559e4cf6fd 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -28,4 +28,9 @@ public void delete() { public void update(@RequestBody User user) { super.update(user, authUserId()); } + + @GetMapping("/text") + public String testUTF() { + return "Русский текст"; + } } \ No newline at end of file diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml index 7b00e74310f6..68fe83a4063a 100644 --- a/src/main/resources/spring/spring-mvc.xml +++ b/src/main/resources/spring/spring-mvc.xml @@ -12,6 +12,14 @@ + + + + text/plain;charset=UTF-8 + text/html;charset=UTF-8 + + + From 5a0d835d8247e0f4d7808d56547b2b89b660fed6 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 2 Apr 2023 17:41:29 +0700 Subject: [PATCH 080/125] apply patch 8_0 --- .../topjava/web/user/AdminRestControllerTest.java | 5 ++--- .../web/user/InMemoryAdminRestControllerSpringTest.java | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java index 7563fe05c089..6af80902c749 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java @@ -5,7 +5,6 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import ru.javawebinar.topjava.UserTestData; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.service.UserService; import ru.javawebinar.topjava.util.exception.NotFoundException; @@ -53,7 +52,7 @@ void delete() throws Exception { @Test void update() throws Exception { - User updated = UserTestData.getUpdated(); + User updated = getUpdated(); perform(MockMvcRequestBuilders.put(REST_URL + USER_ID) .contentType(MediaType.APPLICATION_JSON) .content(JsonUtil.writeValue(updated))) @@ -64,7 +63,7 @@ void update() throws Exception { @Test void createWithLocation() throws Exception { - User newUser = UserTestData.getNew(); + User newUser = getNew(); ResultActions action = perform(MockMvcRequestBuilders.post(REST_URL) .contentType(MediaType.APPLICATION_JSON) .content(JsonUtil.writeValue(newUser))) diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java index fc030a503144..7568d0f52491 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java @@ -21,7 +21,7 @@ class InMemoryAdminRestControllerSpringTest { private InMemoryUserRepository repository; @BeforeEach - void setUp() { + void setup() { repository.init(); } From be249dadff922fa739422d373c483cc0afcd603e Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 2 Apr 2023 17:41:40 +0700 Subject: [PATCH 081/125] apply patch 8_01 --- .../ru/javawebinar/topjava/to/MealTo.java | 18 +++++++++++++++++ src/main/resources/logback.xml | 3 ++- .../topjava/web/ResourceControllerTest.java | 20 +++++++++++++++++++ .../topjava/web/RootControllerTest.java | 12 +++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 src/test/java/ru/javawebinar/topjava/web/ResourceControllerTest.java diff --git a/src/main/java/ru/javawebinar/topjava/to/MealTo.java b/src/main/java/ru/javawebinar/topjava/to/MealTo.java index d14feae792b7..191d342ef6c0 100644 --- a/src/main/java/ru/javawebinar/topjava/to/MealTo.java +++ b/src/main/java/ru/javawebinar/topjava/to/MealTo.java @@ -1,6 +1,7 @@ package ru.javawebinar.topjava.to; import java.time.LocalDateTime; +import java.util.Objects; public class MealTo { private final Integer id; @@ -41,6 +42,23 @@ public boolean isExcess() { return excess; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MealTo mealTo = (MealTo) o; + return calories == mealTo.calories && + excess == mealTo.excess && + Objects.equals(id, mealTo.id) && + Objects.equals(dateTime, mealTo.dateTime) && + Objects.equals(description, mealTo.description); + } + + @Override + public int hashCode() { + return Objects.hash(id, dateTime, description, calories, excess); + } + @Override public String toString() { return "MealTo{" + diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 1d27e3f1ea2b..63c9d5bab744 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -21,7 +21,8 @@ - + + diff --git a/src/test/java/ru/javawebinar/topjava/web/ResourceControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/ResourceControllerTest.java new file mode 100644 index 000000000000..244399662c58 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/ResourceControllerTest.java @@ -0,0 +1,20 @@ +package ru.javawebinar.topjava.web; + +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class ResourceControllerTest extends AbstractControllerTest { + + @Test + void resources() throws Exception { + perform(get("/resources/css/style.css")) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(MediaType.valueOf("text/css"))) + .andExpect(status().isOk()); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java index 2bec37dda60e..6832eaea91a7 100644 --- a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java @@ -9,7 +9,9 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static ru.javawebinar.topjava.MealTestData.meals; import static ru.javawebinar.topjava.UserTestData.*; +import static ru.javawebinar.topjava.util.MealsUtil.getTos; class RootControllerTest extends AbstractControllerTest { @@ -29,4 +31,14 @@ public void assertion(List actual) throws AssertionError { } )); } + + @Test + void getMeals() throws Exception { + perform(get("/meals")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("meals")) + .andExpect(forwardedUrl("/WEB-INF/jsp/meals.jsp")) + .andExpect(model().attribute("meals", getTos(meals, SecurityUtil.authUserCaloriesPerDay()))); + } } \ No newline at end of file From c384ed3f9925f5ad3d0908ff8968ede270cb6101 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 2 Apr 2023 17:41:51 +0700 Subject: [PATCH 082/125] apply patch 8_02 --- .../ru/javawebinar/topjava/to/MealTo.java | 2 + .../javawebinar/topjava/util/MealsUtil.java | 2 +- .../topjava/web/meal/MealRestController.java | 61 ++++++++++++- .../javawebinar/topjava/MatcherFactory.java | 28 ++++-- .../ru/javawebinar/topjava/MealTestData.java | 2 + .../web/meal/MealRestControllerTest.java | 89 +++++++++++++++++++ 6 files changed, 175 insertions(+), 9 deletions(-) create mode 100644 src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java diff --git a/src/main/java/ru/javawebinar/topjava/to/MealTo.java b/src/main/java/ru/javawebinar/topjava/to/MealTo.java index 191d342ef6c0..d0f7863827f8 100644 --- a/src/main/java/ru/javawebinar/topjava/to/MealTo.java +++ b/src/main/java/ru/javawebinar/topjava/to/MealTo.java @@ -1,5 +1,6 @@ package ru.javawebinar.topjava.to; +import java.beans.ConstructorProperties; import java.time.LocalDateTime; import java.util.Objects; @@ -14,6 +15,7 @@ public class MealTo { private final boolean excess; + @ConstructorProperties({"id", "dateTime", "description", "calories", "excess"}) public MealTo(Integer id, LocalDateTime dateTime, String description, int calories, boolean excess) { this.id = id; this.dateTime = dateTime; diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index 658671b9df10..3c74215c09d5 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -38,7 +38,7 @@ private static List filterByPredicate(Collection meals, int calori .toList(); } - private static MealTo createTo(Meal meal, boolean excess) { + public static MealTo createTo(Meal meal, boolean excess) { return new MealTo(meal.getId(), meal.getDateTime(), meal.getDescription(), meal.getCalories(), excess); } } diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java index c3daf68535d7..0a5dbbd511f4 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java @@ -1,7 +1,64 @@ package ru.javawebinar.topjava.web.meal; -import org.springframework.stereotype.Controller; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.to.MealTo; -@Controller +import java.net.URI; +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping(value = MealRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE) public class MealRestController extends AbstractMealController { + static final String REST_URL = "/rest/profile/meals"; + + @Override + @GetMapping("/{id}") + public Meal get(@PathVariable int id) { + return super.get(id); + } + + @Override + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + super.delete(id); + } + + @Override + @GetMapping + public List getAll() { + return super.getAll(); + } + + @Override + @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void update(@RequestBody Meal meal, @PathVariable int id) { + super.update(meal, id); + } + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createWithLocation(@RequestBody Meal meal) { + Meal created = super.create(meal); + + URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() + .path(REST_URL + "/{id}") + .buildAndExpand(created.getId()).toUri(); + + return ResponseEntity.created(uriOfNewResource).body(created); + } + + @GetMapping("/between") + public List getBetween( + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDateTime, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDateTime) { + return super.getBetween(startDateTime.toLocalDate(), startDateTime.toLocalTime(), endDateTime.toLocalDate(), endDateTime.toLocalTime()); + } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/MatcherFactory.java b/src/test/java/ru/javawebinar/topjava/MatcherFactory.java index 40f3d8c22b77..15e01e15535f 100644 --- a/src/test/java/ru/javawebinar/topjava/MatcherFactory.java +++ b/src/test/java/ru/javawebinar/topjava/MatcherFactory.java @@ -7,6 +7,7 @@ import java.io.UnsupportedEncodingException; import java.util.List; +import java.util.function.BiConsumer; import static org.assertj.core.api.Assertions.assertThat; @@ -17,21 +18,36 @@ * Support converting json MvcResult to objects for comparation. */ public class MatcherFactory { + + public static Matcher usingAssertions(Class clazz, BiConsumer assertion, BiConsumer, Iterable> iterableAssertion) { + return new Matcher<>(clazz, assertion, iterableAssertion); + } + + public static Matcher usingEqualsComparator(Class clazz) { + return usingAssertions(clazz, + (a, e) -> assertThat(a).isEqualTo(e), + (a, e) -> assertThat(a).isEqualTo(e)); + } + public static Matcher usingIgnoringFieldsComparator(Class clazz, String... fieldsToIgnore) { - return new Matcher<>(clazz, fieldsToIgnore); + return usingAssertions(clazz, + (a, e) -> assertThat(a).usingRecursiveComparison().ignoringFields(fieldsToIgnore).isEqualTo(e), + (a, e) -> assertThat(a).usingRecursiveFieldByFieldElementComparatorIgnoringFields(fieldsToIgnore).isEqualTo(e)); } public static class Matcher { private final Class clazz; - private final String[] fieldsToIgnore; + private final BiConsumer assertion; + private final BiConsumer, Iterable> iterableAssertion; - private Matcher(Class clazz, String... fieldsToIgnore) { + private Matcher(Class clazz, BiConsumer assertion, BiConsumer, Iterable> iterableAssertion) { this.clazz = clazz; - this.fieldsToIgnore = fieldsToIgnore; + this.assertion = assertion; + this.iterableAssertion = iterableAssertion; } public void assertMatch(T actual, T expected) { - assertThat(actual).usingRecursiveComparison().ignoringFields(fieldsToIgnore).isEqualTo(expected); + assertion.accept(actual, expected); } @SafeVarargs @@ -40,7 +56,7 @@ public final void assertMatch(Iterable actual, T... expected) { } public void assertMatch(Iterable actual, Iterable expected) { - assertThat(actual).usingRecursiveFieldByFieldElementComparatorIgnoringFields(fieldsToIgnore).isEqualTo(expected); + iterableAssertion.accept(actual, expected); } public ResultMatcher contentJson(T expected) { diff --git a/src/test/java/ru/javawebinar/topjava/MealTestData.java b/src/test/java/ru/javawebinar/topjava/MealTestData.java index c2697db04001..6ee8b66ddb1e 100644 --- a/src/test/java/ru/javawebinar/topjava/MealTestData.java +++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java @@ -1,6 +1,7 @@ package ru.javawebinar.topjava; import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.to.MealTo; import java.time.Month; import java.time.temporal.ChronoUnit; @@ -11,6 +12,7 @@ public class MealTestData { public static final MatcherFactory.Matcher MEAL_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(Meal.class, "user"); + public static MatcherFactory.Matcher TO_MATCHER = MatcherFactory.usingEqualsComparator(MealTo.class); public static final int NOT_FOUND = 10; public static final int MEAL1_ID = START_SEQ + 3; diff --git a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java new file mode 100644 index 000000000000..b5aa76805238 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java @@ -0,0 +1,89 @@ +package ru.javawebinar.topjava.web.meal; + + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.service.MealService; +import ru.javawebinar.topjava.util.exception.NotFoundException; +import ru.javawebinar.topjava.web.AbstractControllerTest; +import ru.javawebinar.topjava.web.json.JsonUtil; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static ru.javawebinar.topjava.MealTestData.*; +import static ru.javawebinar.topjava.UserTestData.USER_ID; +import static ru.javawebinar.topjava.UserTestData.user; +import static ru.javawebinar.topjava.util.MealsUtil.createTo; +import static ru.javawebinar.topjava.util.MealsUtil.getTos; + +class MealRestControllerTest extends AbstractControllerTest { + + private static final String REST_URL = MealRestController.REST_URL + '/'; + + @Autowired + private MealService mealService; + + @Test + void get() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + MEAL1_ID)) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(MEAL_MATCHER.contentJson(meal1)); + } + + @Test + void delete() throws Exception { + perform(MockMvcRequestBuilders.delete(REST_URL + MEAL1_ID)) + .andExpect(status().isNoContent()); + assertThrows(NotFoundException.class, () -> mealService.get(MEAL1_ID, USER_ID)); + } + + @Test + void update() throws Exception { + Meal updated = getUpdated(); + perform(MockMvcRequestBuilders.put(REST_URL + MEAL1_ID).contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(updated))) + .andExpect(status().isNoContent()); + + MEAL_MATCHER.assertMatch(mealService.get(MEAL1_ID, USER_ID), updated); + } + + @Test + void createWithLocation() throws Exception { + Meal newMeal = getNew(); + ResultActions action = perform(MockMvcRequestBuilders.post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(newMeal))) + .andExpect(status().isCreated()); + + Meal created = MEAL_MATCHER.readFromJson(action); + int newId = created.id(); + newMeal.setId(newId); + MEAL_MATCHER.assertMatch(created, newMeal); + MEAL_MATCHER.assertMatch(mealService.get(newId, USER_ID), newMeal); + } + + @Test + void getAll() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL)) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(TO_MATCHER.contentJson(getTos(meals, user.getCaloriesPerDay()))); + } + + @Test + void getBetween() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + "between?startDateTime=2020-01-30T07:00&endDateTime=2020-01-31T11:00:00")) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(TO_MATCHER.contentJson(createTo(meal5, true), createTo(meal1, false))); + } +} \ No newline at end of file From 3820c70a140879160b04d2f23a31ea40a3b8994a Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 2 Apr 2023 18:02:52 +0700 Subject: [PATCH 083/125] apply patch 8_03 --- .../web/converter/DateTimeFormatters.java | 36 +++++++++++++++++++ .../topjava/web/meal/MealRestController.java | 16 +++++---- src/main/resources/spring/spring-mvc.xml | 11 +++++- .../web/meal/MealRestControllerTest.java | 11 +++++- 4 files changed, 66 insertions(+), 8 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java diff --git a/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java b/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java new file mode 100644 index 000000000000..15448ce8c68a --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java @@ -0,0 +1,36 @@ +package ru.javawebinar.topjava.web.converter; + +import org.springframework.format.Formatter; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +public class DateTimeFormatters { + public static class LocalDateFormatter implements Formatter { + + @Override + public LocalDate parse(String text, Locale locale) { + return LocalDate.parse(text); + } + + @Override + public String print(LocalDate lt, Locale locale) { + return lt.format(DateTimeFormatter.ISO_LOCAL_DATE); + } + } + + public static class LocalTimeFormatter implements Formatter { + + @Override + public LocalTime parse(String text, Locale locale) { + return LocalTime.parse(text); + } + + @Override + public String print(LocalTime lt, Locale locale) { + return lt.format(DateTimeFormatter.ISO_LOCAL_TIME); + } + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java index 0a5dbbd511f4..66df6c06eb20 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java @@ -1,16 +1,17 @@ package ru.javawebinar.topjava.web.meal; -import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.lang.Nullable; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.to.MealTo; import java.net.URI; -import java.time.LocalDateTime; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.List; @RestController @@ -55,10 +56,13 @@ public ResponseEntity createWithLocation(@RequestBody Meal meal) { return ResponseEntity.created(uriOfNewResource).body(created); } - @GetMapping("/between") + @Override + @GetMapping("/filter") public List getBetween( - @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDateTime, - @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDateTime) { - return super.getBetween(startDateTime.toLocalDate(), startDateTime.toLocalTime(), endDateTime.toLocalDate(), endDateTime.toLocalTime()); + @RequestParam @Nullable LocalDate startDate, + @RequestParam @Nullable LocalTime startTime, + @RequestParam @Nullable LocalDate endDate, + @RequestParam @Nullable LocalTime endTime) { + return super.getBetween(startDate, startTime, endDate, endTime); } } \ No newline at end of file diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml index 68fe83a4063a..7ae8019f04f6 100644 --- a/src/main/resources/spring/spring-mvc.xml +++ b/src/main/resources/spring/spring-mvc.xml @@ -7,7 +7,7 @@ - + @@ -23,6 +23,15 @@ + + + + + + + + + diff --git a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java index b5aa76805238..82bb7cd719a8 100644 --- a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java @@ -81,9 +81,18 @@ void getAll() throws Exception { @Test void getBetween() throws Exception { - perform(MockMvcRequestBuilders.get(REST_URL + "between?startDateTime=2020-01-30T07:00&endDateTime=2020-01-31T11:00:00")) + perform(MockMvcRequestBuilders.get(REST_URL + "filter") + .param("startDate", "2020-01-30").param("startTime", "07:00") + .param("endDate", "2020-01-31").param("endTime", "11:00")) .andExpect(status().isOk()) .andDo(print()) .andExpect(TO_MATCHER.contentJson(createTo(meal5, true), createTo(meal1, false))); } + + @Test + void getBetweenAll() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + "filter?startDate=&endTime=")) + .andExpect(status().isOk()) + .andExpect(TO_MATCHER.contentJson(getTos(meals, user.getCaloriesPerDay()))); + } } \ No newline at end of file From 86ddce2fe3d5514fa636e94a61d53731ba1009f9 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 2 Apr 2023 18:03:07 +0700 Subject: [PATCH 084/125] apply patch 8_04 --- config/Topjava-soapui-project.xml | 214 ++++++++++++++++++++++++++++++ config/curl.md | 29 ++++ 2 files changed, 243 insertions(+) create mode 100644 config/curl.md diff --git a/config/Topjava-soapui-project.xml b/config/Topjava-soapui-project.xml index f9668d765bb8..f4b00d93669d 100644 --- a/config/Topjava-soapui-project.xml +++ b/config/Topjava-soapui-project.xml @@ -386,6 +386,220 @@ + + + + + + + + + 0 + + data + + + application/json;charset=UTF-8 + 200 + + Response + + + + <xml-fragment/> + + + http://localhost:8080 + + http://localhost/topjava/rest/profile/meals + + No Authorization + + + + + + + + + + + application/json + + + + application/json;charset=UTF-8 + 201 + + meal:Response + + + + <xml-fragment/> + + + http://localhost:8080 + { + "dateTime": "2020-02-01T10:00", + "description": "Новый завтрак", + "calories": 777 + } + + http://localhost/topjava/rest/profile/meals + + No Authorization + + + + + + + + + + + + + + + application/json;charset=UTF-8 + 200 + + ns:Response + + + + <xml-fragment/> + + http://localhost:8080 + + http://localhost/topjava/rest/profile/meals/100002 + + No Authorization + + + + + + + + + + + + + 200 + + data + + + application/json + + + + + 200 + + data + + + + 200 + + data + + + + <xml-fragment/> + + UTF-8 + http://localhost:8080 + { + "id": 100002, + "dateTime": "2020-01-30T10:00", + "description": "Обновленный завтрак", + "calories": 500 + } + + http://localhost/topjava/rest/profile/meals/100002 + + No Authorization + + + + + + + + + + + + + startDate + + QUERY + + + + + startTime + + QUERY + + + + + endDate + + QUERY + + + + + endTime + + QUERY + + + + + + + + + application/json;charset=UTF-8 + 200 + + Response + + + + <xml-fragment/> + + http://localhost:8080 + + http://localhost/topjava/rest/profile/meals/filter + + No Authorization + + + + + + + + + + + startDate + startTime + endDate + endTime + + + + diff --git a/config/curl.md b/config/curl.md new file mode 100644 index 000000000000..c001e2bb94d5 --- /dev/null +++ b/config/curl.md @@ -0,0 +1,29 @@ +### curl samples (application deployed at application context `topjava`). +> For windows use `Git Bash` + +#### get All Users +`curl -s http://localhost:8080/topjava/rest/admin/users` + +#### get Users 100001 +`curl -s http://localhost:8080/topjava/rest/admin/users/100001` + +#### get All Meals +`curl -s http://localhost:8080/topjava/rest/profile/meals` + +#### get Meals 100003 +`curl -s http://localhost:8080/topjava/rest/profile/meals/100003` + +#### filter Meals +`curl -s "http://localhost:8080/topjava/rest/profile/meals/filter?startDate=2020-01-30&startTime=07:00:00&endDate=2020-01-31&endTime=11:00:00"` + +#### get Meals not found +`curl -s -v http://localhost:8080/topjava/rest/profile/meals/100008` + +#### delete Meals +`curl -s -X DELETE http://localhost:8080/topjava/rest/profile/meals/100002` + +#### create Meals +`curl -s -X POST -d '{"dateTime":"2020-02-01T12:00","description":"Created lunch","calories":300}' -H 'Content-Type:application/json;charset=UTF-8' http://localhost:8080/topjava/rest/profile/meals` + +#### update Meals +`curl -s -X PUT -d '{"dateTime":"2020-01-30T07:00", "description":"Updated breakfast", "calories":200}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/profile/meals/100003` \ No newline at end of file From 4d14604ca2252c7477ab53e38287309b5bda5fa5 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 2 Apr 2023 18:09:42 +0700 Subject: [PATCH 085/125] apply patch 8_05 --- src/main/java/ru/javawebinar/topjava/model/Meal.java | 2 ++ src/main/java/ru/javawebinar/topjava/model/User.java | 3 ++- .../javawebinar/topjava/web/user/AbstractUserController.java | 5 +++++ .../ru/javawebinar/topjava/web/user/AdminRestController.java | 5 +++++ .../javawebinar/topjava/web/user/ProfileRestController.java | 5 +++++ 5 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index e74f32939c7c..25e8cbd25338 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -1,5 +1,6 @@ package ru.javawebinar.topjava.model; +import com.fasterxml.jackson.annotation.JsonBackReference; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; import org.hibernate.validator.constraints.Range; @@ -45,6 +46,7 @@ public class Meal extends AbstractBaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) @OnDelete(action = OnDeleteAction.CASCADE) + @JsonBackReference // @NotNull private User user; diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index 3b83fd50661c..c0675622fd63 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -4,6 +4,7 @@ import org.hibernate.annotations.*; import org.hibernate.validator.constraints.Range; import org.springframework.util.CollectionUtils; +import com.fasterxml.jackson.annotation.JsonManagedReference; import javax.persistence.Entity; import javax.persistence.NamedQueries; @@ -70,7 +71,7 @@ public class User extends AbstractNamedEntity { @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")//, cascade = CascadeType.REMOVE, orphanRemoval = true) @OrderBy("dateTime DESC") @OnDelete(action = OnDeleteAction.CASCADE) //https://stackoverflow.com/a/44988100/548473 -// @JsonIgnore + @JsonManagedReference private List meals; public User() { diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java index 0000f1c1e02f..ccd46fdf61e6 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java @@ -48,4 +48,9 @@ public User getByMail(String email) { log.info("getByEmail {}", email); return service.getByEmail(email); } + + public User getWithMeals(int id) { + log.info("getWithMeals {}", id); + return service.getWithMeals(id); + } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java index 095ced3b0e1f..66a245275592 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java @@ -56,4 +56,9 @@ public void update(@RequestBody User user, @PathVariable int id) { public User getByMail(@RequestParam String email) { return super.getByMail(email); } + + @GetMapping("/{id}/with-meals") + public User getWithMeals(@PathVariable int id) { + return super.getWithMeals(id); + } } \ 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 index 14559e4cf6fd..c659266807fd 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -33,4 +33,9 @@ public void update(@RequestBody User user) { public String testUTF() { return "Русский текст"; } + + @GetMapping("/with-meals") + public User getWithMeals() { + return super.getWithMeals(authUserId()); + } } \ No newline at end of file From 61f3dcea1380d0f79dddca66fa32684f99879d2a Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 2 Apr 2023 18:09:51 +0700 Subject: [PATCH 086/125] apply patch 8_06 --- .../java/ru/javawebinar/topjava/model/User.java | 8 ++++++-- .../java/ru/javawebinar/topjava/UserTestData.java | 15 +++++++++++++++ .../service/datajpa/DataJpaUserServiceTest.java | 8 ++------ .../topjava/web/AbstractControllerTest.java | 9 +++++++++ .../topjava/web/user/AdminRestControllerTest.java | 10 ++++++++++ .../web/user/ProfileRestControllerTest.java | 10 ++++++++++ 6 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index c0675622fd63..ca6dee5f220f 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -1,10 +1,10 @@ package ru.javawebinar.topjava.model; +import com.fasterxml.jackson.annotation.JsonManagedReference; import org.hibernate.annotations.Cache; import org.hibernate.annotations.*; import org.hibernate.validator.constraints.Range; import org.springframework.util.CollectionUtils; -import com.fasterxml.jackson.annotation.JsonManagedReference; import javax.persistence.Entity; import javax.persistence.NamedQueries; @@ -58,7 +58,7 @@ public class User extends AbstractNamedEntity { uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "role"}, name = "uk_user_role")}) @Column(name = "role") @ElementCollection(fetch = FetchType.EAGER) -// @Fetch(FetchMode.SUBSELECT) + // @Fetch(FetchMode.SUBSELECT) @BatchSize(size = 200) @JoinColumn @OnDelete(action = OnDeleteAction.CASCADE) @@ -147,6 +147,10 @@ public List getMeals() { return meals; } + public void setMeals(List meals) { + this.meals = meals; + } + @Override public String toString() { return "User{" + diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java index 2859645521f0..e271179f2bcc 100644 --- a/src/test/java/ru/javawebinar/topjava/UserTestData.java +++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java @@ -5,11 +5,21 @@ import java.util.Collections; import java.util.Date; +import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; +import static ru.javawebinar.topjava.MealTestData.*; import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; public class UserTestData { public static final MatcherFactory.Matcher USER_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(User.class, "registered", "meals"); + public static MatcherFactory.Matcher USER_WITH_MEALS_MATCHER = + MatcherFactory.usingAssertions(User.class, +// No need use ignoringAllOverriddenEquals, see https://assertj.github.io/doc/#breaking-changes + (a, e) -> assertThat(a).usingRecursiveComparison().ignoringFields("registered", "meals.user").isEqualTo(e), + (a, e) -> { + throw new UnsupportedOperationException(); + }); public static final int USER_ID = START_SEQ; public static final int ADMIN_ID = START_SEQ + 1; @@ -20,6 +30,11 @@ public class UserTestData { public static final User admin = new User(ADMIN_ID, "Admin", "admin@gmail.com", "admin", Role.ADMIN, Role.USER); public static final User guest = new User(GUEST_ID, "Guest", "guest@gmail.com", "guest"); + static { + user.setMeals(meals); + admin.setMeals(List.of(adminMeal2, adminMeal1)); + } + public static User getNew() { return new User(null, "New", "new@gmail.com", "newPass", 1555, false, new Date(), Collections.singleton(Role.USER)); } diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java index d8a1f4106c1f..3638e07e9813 100644 --- a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java @@ -3,13 +3,10 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.test.context.ActiveProfiles; -import ru.javawebinar.topjava.MealTestData; -import ru.javawebinar.topjava.UserTestData; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.service.AbstractUserServiceTest; import ru.javawebinar.topjava.util.exception.NotFoundException; -import static ru.javawebinar.topjava.MealTestData.MEAL_MATCHER; import static ru.javawebinar.topjava.Profiles.DATAJPA; import static ru.javawebinar.topjava.UserTestData.*; @@ -17,9 +14,8 @@ class DataJpaUserServiceTest extends AbstractUserServiceTest { @Test void getWithMeals() { - User admin = service.getWithMeals(ADMIN_ID); - USER_MATCHER.assertMatch(admin, UserTestData.admin); - MEAL_MATCHER.assertMatch(admin.getMeals(), MealTestData.adminMeal2, MealTestData.adminMeal1); + User actual = service.getWithMeals(ADMIN_ID); + USER_WITH_MEALS_MATCHER.assertMatch(actual, admin); } @Test diff --git a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java index 6fa9b1d4c81b..a86811cbc1b7 100644 --- a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java @@ -1,6 +1,8 @@ package ru.javawebinar.topjava.web; +import org.junit.jupiter.api.Assumptions; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; import org.springframework.test.web.servlet.MockMvc; @@ -35,9 +37,16 @@ public abstract class AbstractControllerTest { private MockMvc mockMvc; + @Autowired + private Environment env; + @Autowired private WebApplicationContext webApplicationContext; + protected void assumeDataJpa() { + Assumptions.assumeTrue(env.acceptsProfiles(org.springframework.core.env.Profiles.of(Profiles.DATAJPA)), "DATA-JPA only"); + } + @PostConstruct private void postConstruct() { mockMvc = MockMvcBuilders diff --git a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java index 6af80902c749..5b6b17ccfd7d 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java @@ -83,4 +83,14 @@ void getAll() throws Exception { .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andExpect(USER_MATCHER.contentJson(admin, guest, user)); } + + @Test + void getWithMeals() throws Exception { + assumeDataJpa(); + perform(MockMvcRequestBuilders.get(REST_URL + ADMIN_ID + "/with-meals")) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(USER_WITH_MEALS_MATCHER.contentJson(admin)); + } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java index e8882742da6d..5ae3399430f5 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java @@ -45,4 +45,14 @@ void update() throws Exception { USER_MATCHER.assertMatch(userService.get(USER_ID), updated); } + + @Test + void getWithMeals() throws Exception { + assumeDataJpa(); + perform(MockMvcRequestBuilders.get(REST_URL + "/with-meals")) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(USER_WITH_MEALS_MATCHER.contentJson(user)); + } } \ No newline at end of file From 14e2737cacf36458c89a76c12b491362167f2d8a Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 2 Apr 2023 19:54:24 +0700 Subject: [PATCH 087/125] apply patch 8_07 --- pom.xml | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/pom.xml b/pom.xml index 047dffff52e3..87f281ad33a7 100644 --- a/pom.xml +++ b/pom.xml @@ -28,6 +28,13 @@ 3.10.8 + + 4.6.2 + 3.6.4 + 2.5.20-1 + 3.1.4 + 1.13.2 + 1.2.11 1.7.36 @@ -214,6 +221,50 @@ spring-webmvc + + + org.webjars + jquery + ${webjars-jquery.version} + + + org.webjars + bootstrap + ${webjars-bootstrap.version} + + + org.webjars + jquery + + + + org.webjars + popper.js + + + + + org.webjars + datatables + ${webjars-datatables.version} + + + org.webjars + jquery + + + + + org.webjars + datetimepicker + ${webjars-datetimepicker.version} + + + org.webjars.bower + noty + ${webjars-noty.version} + + com.fasterxml.jackson.core From dc5b85b95e66994e9d4063b1ad9e24cc77e3c4c9 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 2 Apr 2023 19:54:54 +0700 Subject: [PATCH 088/125] apply patch 8_08 --- config/messages/app.properties | 6 +- config/messages/app_ru.properties | 8 ++- src/main/resources/spring/spring-mvc.xml | 3 + .../WEB-INF/jsp/fragments/bodyHeader.jsp | 14 ++++- .../webapp/WEB-INF/jsp/fragments/footer.jsp | 8 ++- .../webapp/WEB-INF/jsp/fragments/headTag.jsp | 6 +- src/main/webapp/WEB-INF/jsp/index.jsp | 22 +++---- src/main/webapp/WEB-INF/jsp/users.jsp | 55 ++++++++++-------- src/main/webapp/resources/css/style.css | 25 ++++++-- .../webapp/resources/images/icon-meal.png | Bin 0 -> 1898 bytes 10 files changed, 97 insertions(+), 50 deletions(-) create mode 100644 src/main/webapp/resources/images/icon-meal.png diff --git a/config/messages/app.properties b/config/messages/app.properties index 6b4d2d155a47..65d9fe20a818 100644 --- a/config/messages/app.properties +++ b/config/messages/app.properties @@ -1,9 +1,10 @@ app.title=Calories management -app.home=Home -app.footer=Internship Spring 5/JPA Enterprise (Topjava) application +app.footer=Spring 5/JPA Enterprise (Topjava) internship application app.login=Login as user.title=Users +user.edit=Edit user +user.add=Add user user.name=Name user.email=Email user.roles=Roles @@ -22,6 +23,7 @@ meal.description=Description meal.dateTime=Date/Time meal.calories=Calories +common.add=Add common.select=Select common.delete=Delete common.update=Update diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties index c1e3d54e35a5..5569546f8400 100644 --- a/config/messages/app_ru.properties +++ b/config/messages/app_ru.properties @@ -1,9 +1,10 @@ app.title=Подсчет калорий -app.home=Главная app.footer=Приложение стажировки Spring 5/JPA Enterprise (Topjava) app.login=Зайти как user.title=Пользователи +user.edit=Редактировать пользователя +user.add=Добавить пользователя user.name=Имя user.email=Почта user.roles=Роли @@ -11,8 +12,8 @@ user.active=Активный user.registered=Зарегистрирован meal.title=Моя еда -meal.edit=Редактирование еды -meal.add=Добавление еды +meal.edit=Редактировать еду +meal.add=Добавить еду meal.filter=Отфильтровать meal.startDate=От даты (включая) meal.endDate=До даты (включая) @@ -22,6 +23,7 @@ meal.description=Описание meal.dateTime=Дата/Время meal.calories=Калории +common.add=Добавить common.select=Выбрать common.delete=Удалить common.update=Обновить diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml index 7ae8019f04f6..1201341ab14d 100644 --- a/src/main/resources/spring/spring-mvc.xml +++ b/src/main/resources/spring/spring-mvc.xml @@ -37,6 +37,9 @@ + + + diff --git a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp index 5b5efe57e65b..bba77445f371 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp @@ -1,6 +1,14 @@ <%@page contentType="text/html" pageEncoding="UTF-8" %> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> -

    - | | -
    \ No newline at end of file + diff --git a/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp index 0935c441a36b..cf1331fd5523 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp @@ -1,4 +1,8 @@ <%@page contentType="text/html" pageEncoding="UTF-8" %> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> -
    -
    \ No newline at end of file +<%--https://getbootstrap.com/docs/4.0/examples/sticky-footer/--%> +
    +
    + +
    +
    \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp index 0c77f10859f8..3f5c31c23f36 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp @@ -6,5 +6,9 @@ <spring:message code="app.title"/> - + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/index.jsp b/src/main/webapp/WEB-INF/jsp/index.jsp index 84719196538d..cc610bd72f1a 100644 --- a/src/main/webapp/WEB-INF/jsp/index.jsp +++ b/src/main/webapp/WEB-INF/jsp/index.jsp @@ -6,16 +6,18 @@ -
    -
    -
    - : - - -
    +
    +
    +
    + + + +
    +
    +
    \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/users.jsp b/src/main/webapp/WEB-INF/jsp/users.jsp index 4d3d8678906c..95b9678c97ad 100644 --- a/src/main/webapp/WEB-INF/jsp/users.jsp +++ b/src/main/webapp/WEB-INF/jsp/users.jsp @@ -8,31 +8,40 @@ -
    -

    - - - - - - - - - - - - - +
    +
    +

    + +
    + - - - - - + + + + + + + - -
    ${user.email}${user.roles}${user.enabled}
    -
    + + + +

    ${user.email}${user.roles}checked/>
    + + \ No newline at end of file diff --git a/src/main/webapp/resources/css/style.css b/src/main/webapp/resources/css/style.css index a55147510899..067c4cb1f8f6 100644 --- a/src/main/webapp/resources/css/style.css +++ b/src/main/webapp/resources/css/style.css @@ -23,10 +23,23 @@ tr[data-meal-excess="true"] { color: red; } -header, footer { - background: none repeat scroll 0 0 #A6C9E2; - color: #2E6E9E; - font-size: 20px; - padding: 5px 20px; - margin: 6px 0; +.fa { + cursor: pointer; +} + +/*https://getbootstrap.com/docs/4.0/examples/sticky-footer/sticky-footer.css*/ +html { + position: relative; + min-height: 100%; +} +body { + margin-bottom: 60px !important; /* Margin bottom by footer height */ +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + height: 60px; /* Set the fixed height of the footer here */ + line-height: 60px; /* Vertically center the text there */ + background-color: #f5f5f5; } diff --git a/src/main/webapp/resources/images/icon-meal.png b/src/main/webapp/resources/images/icon-meal.png new file mode 100644 index 0000000000000000000000000000000000000000..b4fc54ad01294abb103d9238c12b1826a9240dfd GIT binary patch literal 1898 zcmV-w2bK7VP)PS&+k>eS5@~O7WLfN z#z>r1)zQ7@JLlXpd}L<$FKLxT1mO7i`0((sD2l48LI|y1MB+H^bUOWhKS`1&t~@IA z1b~^D88e&NXf)c{*(uAi)iCqP{SZP3!OTRY*Xx}A^;#TU>54SHnYXW#g&y6RSh99^HTs$tZ#2`udlC*h;z;d0}zWj3TB}R zFn|flWT07)I)DiT%x1GFidI)w^E?kB5K$Y-9N@(I#>U3x=4P6v0K6d;BWwP3q;Kx$ z_a^B<71hQG`=w-hwO4#}p&VpZ25mf4sA(GS{pHJ-yWQ>y2H8Vl=B=%*jg5^g%R(>^ zpsL+&Chb!3l!F8yFZyq(Lj5wj%J{QS=1FV`-RXv|gz1JiLuH$O`+tkPjc%tTC8 zFh=;G>b&~)rPnv3Ze$)5$8pm%o12^M8_WSlqmlRCIcLNj^|#;a|88fwA6w$=xgmY{ z3@O6{xx>xRAXX4z=JADR>E-A6R2)3HpZxQ}ojuAO`G7P{_xJaw(g~evn6dlC zz2#Zsoia0v$b$zDh=_^k=;$bf;GAP-3;wnBGbR$umb>)D6^v`t0WV%eKS2{4Wl=yZli zQ3XsrIx+ew0zib-1)S^QbOHd(2!?BCD8SOl{xn)BYg1tn0Wg_N*vzV`a?TO4WBc1d zR;d)!V2&CN+Nj4+MfR07QZ`!jIp;_5>jO?C-d9fIKE* zu-I^;zq+&~{XOY%# z5#czF=Rpv~PA_$85Q)&-ghCj?bBuPEs6ZGZPzDp6%=1G+xi%NO7D!e2q_RG|zaIE4tDSi`uGlo$I_gH^9nL%#7Qm zwy>};o6WrUPPl}7=~DWQ)4QejGoqQdqOrEijv55RG{6antne1h4gzPPym{gH%CI-} zh+{V%kK;IQ>CAj(647`(-rL(-UVf&o8)A;F{=@y^)=no$GbJ3?U|1qpi10xPAv8v@ zz){Gm34shDAOyfb1Qrm7fW`-5YB|H<;o)E~NYk{<`O}$Qi%}HC?(S}yru}~3 zIcGo!W-x#-pHg5VCNi_KEDsJ2h-fevsOqEEp9DC8X(Ky0I4FujRns(`7d6kT&U;_i z^>jKd%aVwCy Date: Sun, 2 Apr 2023 20:07:31 +0700 Subject: [PATCH 089/125] apply patch 8_09 --- config/messages/app.properties | 1 + config/messages/app_ru.properties | 1 + .../topjava/web/user/AdminUIController.java | 35 ++++++++++++ .../webapp/WEB-INF/jsp/fragments/headTag.jsp | 7 +++ src/main/webapp/WEB-INF/jsp/users.jsp | 54 +++++++++++++++++-- .../webapp/resources/js/topjava.common.js | 39 ++++++++++++++ src/main/webapp/resources/js/topjava.users.js | 42 +++++++++++++++ 7 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java create mode 100644 src/main/webapp/resources/js/topjava.common.js create mode 100644 src/main/webapp/resources/js/topjava.users.js diff --git a/config/messages/app.properties b/config/messages/app.properties index 65d9fe20a818..fc7665a883dc 100644 --- a/config/messages/app.properties +++ b/config/messages/app.properties @@ -10,6 +10,7 @@ user.email=Email user.roles=Roles user.active=Active user.registered=Registered +user.password=Password meal.title=Meals meal.edit=Edit meal diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties index 5569546f8400..d5ad6779fe90 100644 --- a/config/messages/app_ru.properties +++ b/config/messages/app_ru.properties @@ -10,6 +10,7 @@ user.email=Почта user.roles=Роли user.active=Активный user.registered=Зарегистрирован +user.password=Пароль meal.title=Моя еда meal.edit=Редактировать еду diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java new file mode 100644 index 000000000000..887cd47173ee --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java @@ -0,0 +1,35 @@ +package ru.javawebinar.topjava.web.user; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; + +import java.util.List; + +@RestController +@RequestMapping(value = "/admin/users", produces = MediaType.APPLICATION_JSON_VALUE) +public class AdminUIController extends AbstractUserController { + + @Override + @GetMapping + public List getAll() { + return super.getAll(); + } + + @Override + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + super.delete(id); + } + + @PostMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + public void create(@RequestParam String name, + @RequestParam String email, + @RequestParam String password) { + super.create(new User(null, name, email, password, Role.USER)); + } +} diff --git a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp index 3f5c31c23f36..5eab4738b7de 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp @@ -10,5 +10,12 @@ + + + <%--http://stackoverflow.com/a/24070373/548473--%> + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/users.jsp b/src/main/webapp/WEB-INF/jsp/users.jsp index 95b9678c97ad..344ede8b3bc1 100644 --- a/src/main/webapp/WEB-INF/jsp/users.jsp +++ b/src/main/webapp/WEB-INF/jsp/users.jsp @@ -6,16 +6,18 @@ + +

    - - +
    @@ -33,15 +35,59 @@ - + - +
    ${user.email} ${user.roles}checked/>checked id="${user.id}"/>
    + + \ No newline at end of file diff --git a/src/main/webapp/resources/js/topjava.common.js b/src/main/webapp/resources/js/topjava.common.js new file mode 100644 index 000000000000..69c0d72cb314 --- /dev/null +++ b/src/main/webapp/resources/js/topjava.common.js @@ -0,0 +1,39 @@ +function makeEditable() { + $(".delete").click(function () { + deleteRow($(this).attr("id")); + }); +} + +function add() { + $("#detailsForm").find(":input").val(""); + $("#editRow").modal(); +} + +function deleteRow(id) { + $.ajax({ + url: ajaxUrl + id, + type: "DELETE", + success: function () { + updateTable(); + } + }); +} + +function updateTable() { + $.get(ajaxUrl, function (data) { + datatableApi.clear().rows.add(data).draw(); + }); +} + +function save() { + const form = $("#detailsForm"); + $.ajax({ + type: "POST", + url: ajaxUrl, + data: form.serialize(), + success: function () { + $("#editRow").modal("hide"); + updateTable(); + } + }); +} \ No newline at end of file diff --git a/src/main/webapp/resources/js/topjava.users.js b/src/main/webapp/resources/js/topjava.users.js new file mode 100644 index 000000000000..db10e94b28ba --- /dev/null +++ b/src/main/webapp/resources/js/topjava.users.js @@ -0,0 +1,42 @@ +const ajaxUrl = "admin/users/"; +let datatableApi; + +// $(document).ready(function () { +$(function () { + datatableApi = $("#datatable").DataTable({ + "paging": false, + "info": true, + "columns": [ + { + "data": "name" + }, + { + "data": "email" + }, + { + "data": "roles" + }, + { + "data": "enabled" + }, + { + "data": "registered" + }, + { + "defaultContent": "Edit", + "orderable": false + }, + { + "defaultContent": "Delete", + "orderable": false + } + ], + "order": [ + [ + 0, + "asc" + ] + ] + }); + makeEditable(); +}); \ No newline at end of file From 98d1ede391137e76dec8c9d165dfcae4417a1c86 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 2 Apr 2023 21:54:42 +0700 Subject: [PATCH 090/125] apply patch 8_10 --- src/main/webapp/WEB-INF/jsp/users.jsp | 6 +- .../webapp/resources/js/topjava.common.js | 37 +++++---- src/main/webapp/resources/js/topjava.users.js | 79 ++++++++++--------- 3 files changed, 65 insertions(+), 57 deletions(-) diff --git a/src/main/webapp/WEB-INF/jsp/users.jsp b/src/main/webapp/WEB-INF/jsp/users.jsp index 344ede8b3bc1..cd25ba75cd8a 100644 --- a/src/main/webapp/WEB-INF/jsp/users.jsp +++ b/src/main/webapp/WEB-INF/jsp/users.jsp @@ -31,14 +31,14 @@ - + ${user.email} ${user.roles} - checked id="${user.id}"/> + checked/> - + diff --git a/src/main/webapp/resources/js/topjava.common.js b/src/main/webapp/resources/js/topjava.common.js index 69c0d72cb314..6e0540b2f65c 100644 --- a/src/main/webapp/resources/js/topjava.common.js +++ b/src/main/webapp/resources/js/topjava.common.js @@ -1,39 +1,42 @@ -function makeEditable() { +let form; + +function makeEditable(datatableApi) { + ctx.datatableApi = datatableApi; + form = $('#detailsForm'); $(".delete").click(function () { - deleteRow($(this).attr("id")); + if (confirm('Are you sure?')) { + deleteRow($(this).closest('tr').attr("id")); + } }); } function add() { - $("#detailsForm").find(":input").val(""); + form.find(":input").val(""); $("#editRow").modal(); } function deleteRow(id) { $.ajax({ - url: ajaxUrl + id, - type: "DELETE", - success: function () { - updateTable(); - } + url: ctx.ajaxUrl + id, + type: "DELETE" + }).done(function () { + updateTable(); }); } function updateTable() { - $.get(ajaxUrl, function (data) { - datatableApi.clear().rows.add(data).draw(); + $.get(ctx.ajaxUrl, function (data) { + ctx.datatableApi.clear().rows.add(data).draw(); }); } function save() { - const form = $("#detailsForm"); $.ajax({ type: "POST", - url: ajaxUrl, - data: form.serialize(), - success: function () { - $("#editRow").modal("hide"); - updateTable(); - } + url: ctx.ajaxUrl, + data: form.serialize() + }).done(function () { + $("#editRow").modal("hide"); + updateTable(); }); } \ No newline at end of file diff --git a/src/main/webapp/resources/js/topjava.users.js b/src/main/webapp/resources/js/topjava.users.js index db10e94b28ba..10a3d301c5d9 100644 --- a/src/main/webapp/resources/js/topjava.users.js +++ b/src/main/webapp/resources/js/topjava.users.js @@ -1,42 +1,47 @@ -const ajaxUrl = "admin/users/"; -let datatableApi; +const userAjaxUrl = "admin/users/"; + +// https://stackoverflow.com/a/5064235/548473 +const ctx = { + ajaxUrl: userAjaxUrl +}; // $(document).ready(function () { $(function () { - datatableApi = $("#datatable").DataTable({ - "paging": false, - "info": true, - "columns": [ - { - "data": "name" - }, - { - "data": "email" - }, - { - "data": "roles" - }, - { - "data": "enabled" - }, - { - "data": "registered" - }, - { - "defaultContent": "Edit", - "orderable": false - }, - { - "defaultContent": "Delete", - "orderable": false - } - ], - "order": [ - [ - 0, - "asc" + makeEditable( + $("#datatable").DataTable({ + "paging": false, + "info": true, + "columns": [ + { + "data": "name" + }, + { + "data": "email" + }, + { + "data": "roles" + }, + { + "data": "enabled" + }, + { + "data": "registered" + }, + { + "defaultContent": "Edit", + "orderable": false + }, + { + "defaultContent": "Delete", + "orderable": false + } + ], + "order": [ + [ + 0, + "asc" + ] ] - ] - }); - makeEditable(); + }) + ); }); \ No newline at end of file From 1a50ed2c7cab371107fac338c081d7ef26d6b259 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Sun, 2 Apr 2023 21:55:24 +0700 Subject: [PATCH 091/125] apply patch 8_11 --- .../webapp/WEB-INF/jsp/fragments/headTag.jsp | 2 + src/main/webapp/WEB-INF/jsp/users.jsp | 4 +- .../webapp/resources/js/topjava.common.js | 38 +++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp index 5eab4738b7de..1de80055bcde 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp @@ -11,6 +11,7 @@ + <%--http://stackoverflow.com/a/24070373/548473--%> @@ -18,4 +19,5 @@ + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/users.jsp b/src/main/webapp/WEB-INF/jsp/users.jsp index cd25ba75cd8a..35cf30926366 100644 --- a/src/main/webapp/WEB-INF/jsp/users.jsp +++ b/src/main/webapp/WEB-INF/jsp/users.jsp @@ -50,7 +50,7 @@ + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/users.jsp b/src/main/webapp/WEB-INF/jsp/users.jsp index 35cf30926366..35eaf0ff8102 100644 --- a/src/main/webapp/WEB-INF/jsp/users.jsp +++ b/src/main/webapp/WEB-INF/jsp/users.jsp @@ -31,14 +31,14 @@ - + ${user.email} ${user.roles} checked/> - + diff --git a/src/main/webapp/resources/css/style.css b/src/main/webapp/resources/css/style.css index 067c4cb1f8f6..7c66e5126b72 100644 --- a/src/main/webapp/resources/css/style.css +++ b/src/main/webapp/resources/css/style.css @@ -1,20 +1,3 @@ -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; } diff --git a/src/main/webapp/resources/js/topjava.common.js b/src/main/webapp/resources/js/topjava.common.js index 687924a04081..2cb4a17289b5 100644 --- a/src/main/webapp/resources/js/topjava.common.js +++ b/src/main/webapp/resources/js/topjava.common.js @@ -3,11 +3,6 @@ let form; function makeEditable(datatableApi) { ctx.datatableApi = datatableApi; form = $('#detailsForm'); - $(".delete").click(function () { - if (confirm('Are you sure?')) { - deleteRow($(this).closest('tr').attr("id")); - } - }); $(document).ajaxError(function (event, jqXHR, options, jsExc) { failNoty(jqXHR); @@ -23,19 +18,19 @@ function add() { } function deleteRow(id) { - $.ajax({ - url: ctx.ajaxUrl + id, - type: "DELETE" - }).done(function () { - updateTable(); - successNoty("Deleted"); - }); + if (confirm('Are you sure?')) { + $.ajax({ + url: ctx.ajaxUrl + id, + type: "DELETE" + }).done(function () { + ctx.updateTable(); + successNoty("Deleted"); + }); + } } -function updateTable() { - $.get(ctx.ajaxUrl, function (data) { - ctx.datatableApi.clear().rows.add(data).draw(); - }); +function updateTableByData(data) { + ctx.datatableApi.clear().rows.add(data).draw(); } function save() { @@ -45,7 +40,7 @@ function save() { data: form.serialize() }).done(function () { $("#editRow").modal("hide"); - updateTable(); + ctx.updateTable(); successNoty("Saved"); }); } diff --git a/src/main/webapp/resources/js/topjava.meals.js b/src/main/webapp/resources/js/topjava.meals.js new file mode 100644 index 000000000000..cad6d1b4e722 --- /dev/null +++ b/src/main/webapp/resources/js/topjava.meals.js @@ -0,0 +1,47 @@ +const mealAjaxUrl = "profile/meals/"; + +// https://stackoverflow.com/a/5064235/548473 +const ctx = { + ajaxUrl: mealAjaxUrl, + updateTable: function () { + $.ajax({ + type: "GET", + url: mealAjaxUrl + "filter", + data: $("#filter").serialize() + }).done(updateTableByData); + } +} + +$(function () { + makeEditable( + $("#datatable").DataTable({ + "paging": false, + "info": true, + "columns": [ + { + "data": "dateTime" + }, + { + "data": "description" + }, + { + "data": "calories" + }, + { + "defaultContent": "Edit", + "orderable": false + }, + { + "defaultContent": "Delete", + "orderable": false + } + ], + "order": [ + [ + 0, + "desc" + ] + ] + }) + ); +}); \ No newline at end of file diff --git a/src/main/webapp/resources/js/topjava.users.js b/src/main/webapp/resources/js/topjava.users.js index 10a3d301c5d9..c495042c0369 100644 --- a/src/main/webapp/resources/js/topjava.users.js +++ b/src/main/webapp/resources/js/topjava.users.js @@ -2,8 +2,11 @@ const userAjaxUrl = "admin/users/"; // https://stackoverflow.com/a/5064235/548473 const ctx = { - ajaxUrl: userAjaxUrl -}; + ajaxUrl: userAjaxUrl, + updateTable: function () { + $.get(userAjaxUrl, updateTableByData); + } +} // $(document).ready(function () { $(function () { From 07e52ba1ca361b22dcc7716b05b6a4a9cf5c5605 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Mon, 10 Apr 2023 10:17:43 +0700 Subject: [PATCH 094/125] apply patch 9_02 --- src/main/webapp/WEB-INF/jsp/meals.jsp | 4 ++++ src/main/webapp/resources/js/topjava.meals.js | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/main/webapp/WEB-INF/jsp/meals.jsp b/src/main/webapp/WEB-INF/jsp/meals.jsp index fb2438e56b7c..6d5159a9da6b 100644 --- a/src/main/webapp/WEB-INF/jsp/meals.jsp +++ b/src/main/webapp/WEB-INF/jsp/meals.jsp @@ -37,6 +37,10 @@ diff --git a/src/main/webapp/resources/js/topjava.common.js b/src/main/webapp/resources/js/topjava.common.js index b9ede8138f94..862400c787af 100644 --- a/src/main/webapp/resources/js/topjava.common.js +++ b/src/main/webapp/resources/js/topjava.common.js @@ -74,6 +74,18 @@ function successNoty(text) { }).show(); } +function renderEditBtn(data, type, row) { + if (type === "display") { + return ""; + } +} + +function renderDeleteBtn(data, type, row) { + if (type === "display") { + return ""; + } +} + function failNoty(jqXHR) { closeNoty(); failedNote = new Noty({ diff --git a/src/main/webapp/resources/js/topjava.users.js b/src/main/webapp/resources/js/topjava.users.js index dc5ccb6d3ad8..aad842af11e5 100644 --- a/src/main/webapp/resources/js/topjava.users.js +++ b/src/main/webapp/resources/js/topjava.users.js @@ -27,6 +27,10 @@ function enable(chkbox, id) { $(function () { makeEditable( $("#datatable").DataTable({ + "ajax": { + "url": userAjaxUrl, + "dataSrc": "" + }, "paging": false, "info": true, "columns": [ @@ -34,24 +38,44 @@ $(function () { "data": "name" }, { - "data": "email" + "data": "email", + "render": function (data, type, row) { + if (type === "display") { + return "" + data + ""; + } + return data; + } }, { "data": "roles" }, { - "data": "enabled" + "data": "enabled", + "render": function (data, type, row) { + if (type === "display") { + return ""; + } + return data; + } }, { - "data": "registered" + "data": "registered", + "render": function (date, type, row) { + if (type === "display") { + return date.substring(0, 10); + } + return date; + } }, { - "defaultContent": "Edit", - "orderable": false + "orderable": false, + "defaultContent": "", + "render": renderEditBtn }, { - "defaultContent": "Delete", - "orderable": false + "orderable": false, + "defaultContent": "", + "render": renderDeleteBtn } ], "order": [ @@ -59,7 +83,12 @@ $(function () { 0, "asc" ] - ] + ], + "createdRow": function (row, data, dataIndex) { + if (!data.enabled) { + $(row).attr("data-user-enabled", false); + } + } }) ); }); \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java index 6832eaea91a7..cfa0c391788b 100644 --- a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java @@ -1,16 +1,11 @@ package ru.javawebinar.topjava.web; -import org.assertj.core.matcher.AssertionMatcher; import org.junit.jupiter.api.Test; -import ru.javawebinar.topjava.model.User; - -import java.util.List; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static ru.javawebinar.topjava.MealTestData.meals; -import static ru.javawebinar.topjava.UserTestData.*; import static ru.javawebinar.topjava.util.MealsUtil.getTos; class RootControllerTest extends AbstractControllerTest { @@ -21,15 +16,7 @@ void getUsers() throws Exception { .andDo(print()) .andExpect(status().isOk()) .andExpect(view().name("users")) - .andExpect(forwardedUrl("/WEB-INF/jsp/users.jsp")) - .andExpect(model().attribute("users", - new AssertionMatcher>() { - @Override - public void assertion(List actual) throws AssertionError { - USER_MATCHER.assertMatch(actual, admin, guest, user); - } - } - )); + .andExpect(forwardedUrl("/WEB-INF/jsp/users.jsp")); } @Test From 46824373c67935ce26fbcc452755b42eb38f57c0 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Mon, 10 Apr 2023 14:27:29 +0700 Subject: [PATCH 100/125] apply patch 9_08 --- config/messages/app.properties | 6 ++++++ config/messages/app_ru.properties | 6 ++++++ src/main/webapp/WEB-INF/jsp/users.jsp | 11 ++++++++++- src/main/webapp/resources/js/topjava.common.js | 14 ++++++++------ src/main/webapp/resources/js/topjava.users.js | 2 +- 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/config/messages/app.properties b/config/messages/app.properties index 4f3f559553e2..d0df81e6ea8b 100644 --- a/config/messages/app.properties +++ b/config/messages/app.properties @@ -26,5 +26,11 @@ meal.calories=Calories common.add=Add common.select=Select +common.deleted=Record deleted +common.saved=Record saved +common.enabled=Record enabled +common.disabled=Record disabled +common.errorStatus=Error status +common.confirm=Are you sure? common.save=Save common.cancel=Cancel \ No newline at end of file diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties index da51e0f06631..1e967c76ca15 100644 --- a/config/messages/app_ru.properties +++ b/config/messages/app_ru.properties @@ -26,5 +26,11 @@ meal.calories=Калории common.add=Добавить common.select=Выбрать +common.deleted=Запись удалена +common.saved=Запись сохранена +common.enabled=Запись активирована +common.disabled=Запись деактивирована +common.errorStatus=Статус ошибки +common.confirm=Вы уверены? common.save=Сохранить common.cancel=Отменить diff --git a/src/main/webapp/WEB-INF/jsp/users.jsp b/src/main/webapp/WEB-INF/jsp/users.jsp index f603d4496797..4f1ea1d1268e 100644 --- a/src/main/webapp/WEB-INF/jsp/users.jsp +++ b/src/main/webapp/WEB-INF/jsp/users.jsp @@ -37,7 +37,7 @@ @@ -85,7 +70,7 @@

    + diff --git a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java index 67f8fc6babf7..9547ccff4094 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.service.UserService; @@ -46,6 +47,23 @@ void delete() throws Exception { USER_MATCHER.assertMatch(userService.getAll(), admin, guest); } + @Test + void register() throws Exception { + UserTo newTo = new UserTo(null, "newName", "newemail@ya.ru", "newPassword", 1500); + User newUser = UsersUtil.createNewFromTo(newTo); + ResultActions action = perform(MockMvcRequestBuilders.post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(newTo))) + .andDo(print()) + .andExpect(status().isCreated()); + + User created = USER_MATCHER.readFromJson(action); + int newId = created.id(); + newUser.setId(newId); + USER_MATCHER.assertMatch(created, newUser); + USER_MATCHER.assertMatch(userService.get(newId), newUser); + } + @Test void update() throws Exception { UserTo updatedTo = new UserTo(null, "newName", "user@yandex.ru", "newPassword", 1500); From bee41d0cf29796e8c670630059b95ef20e557425 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Fri, 14 Apr 2023 22:43:10 +0700 Subject: [PATCH 115/125] apply patch 10_10 --- .../util/exception/NotFoundException.java | 5 +++++ .../web/meal/MealRestControllerTest.java | 15 +++++++++++++++ .../web/user/AdminRestControllerTest.java | 17 +++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java index f1e9b0e46376..9ff0bbbb417f 100644 --- a/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java +++ b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java @@ -1,5 +1,10 @@ package ru.javawebinar.topjava.util.exception; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +// http://stackoverflow.com/a/22358422/548473 +@ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY, reason = "No data found") // 422 public class NotFoundException extends RuntimeException { public NotFoundException(String message) { super(message); diff --git a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java index 495f0201c1ec..784bc047917c 100644 --- a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java @@ -46,6 +46,14 @@ void getUnauth() throws Exception { .andExpect(status().isUnauthorized()); } + @Test + void getNotFound() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + ADMIN_MEAL_ID) + .with(userHttpBasic(user))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + @Test void delete() throws Exception { perform(MockMvcRequestBuilders.delete(REST_URL + MEAL1_ID) @@ -54,6 +62,13 @@ void delete() throws Exception { assertThrows(NotFoundException.class, () -> mealService.get(MEAL1_ID, USER_ID)); } + @Test + void deleteNotFound() throws Exception { + perform(MockMvcRequestBuilders.delete(REST_URL + ADMIN_MEAL_ID) + .with(userHttpBasic(user))) + .andExpect(status().isUnprocessableEntity()); + } + @Test void update() throws Exception { Meal updated = getUpdated(); diff --git a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java index ec14fff71786..3abbb52e037c 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java @@ -5,6 +5,7 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import ru.javawebinar.topjava.UserTestData; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.service.UserService; import ru.javawebinar.topjava.util.exception.NotFoundException; @@ -37,6 +38,14 @@ void get() throws Exception { .andExpect(USER_MATCHER.contentJson(admin)); } + @Test + void getNotFound() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + NOT_FOUND) + .with(userHttpBasic(admin))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + @Test void getByEmail() throws Exception { perform(MockMvcRequestBuilders.get(REST_URL + "by-email?email=" + user.getEmail()) @@ -55,6 +64,14 @@ void delete() throws Exception { assertThrows(NotFoundException.class, () -> userService.get(USER_ID)); } + @Test + void deleteNotFound() throws Exception { + perform(MockMvcRequestBuilders.delete(REST_URL + NOT_FOUND) + .with(userHttpBasic(admin))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + @Test void getUnAuth() throws Exception { perform(MockMvcRequestBuilders.get(REST_URL)) From 9f7fac4bdb3fc420b3d0cf7dcbf783e460cb945e Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Fri, 14 Apr 2023 22:43:27 +0700 Subject: [PATCH 116/125] apply patch 10_11 --- config/messages/app.properties | 1 + config/messages/app_ru.properties | 1 + .../topjava/web/GlobalExceptionHandler.java | 36 +++++++++++++++++++ src/main/webapp/WEB-INF/jsp/exception.jsp | 26 ++++++++++++++ 4 files changed, 64 insertions(+) create mode 100644 src/main/java/ru/javawebinar/topjava/web/GlobalExceptionHandler.java create mode 100644 src/main/webapp/WEB-INF/jsp/exception.jsp diff --git a/config/messages/app.properties b/config/messages/app.properties index 6a174b0d262b..979407b488bd 100644 --- a/config/messages/app.properties +++ b/config/messages/app.properties @@ -40,6 +40,7 @@ common.saved=Record saved common.enabled=Record enabled common.disabled=Record disabled common.errorStatus=Error status +common.appError=Application error common.confirm=Are you sure? common.save=Save common.cancel=Cancel \ No newline at end of file diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties index 275c9099185f..2a3ac2859934 100644 --- a/config/messages/app_ru.properties +++ b/config/messages/app_ru.properties @@ -40,6 +40,7 @@ common.saved=Запись сохранена common.enabled=Запись активирована common.disabled=Запись деактивирована common.errorStatus=Статус ошибки +common.appError=Ошибка приложения common.confirm=Вы уверены? common.save=Сохранить common.cancel=Отменить diff --git a/src/main/java/ru/javawebinar/topjava/web/GlobalExceptionHandler.java b/src/main/java/ru/javawebinar/topjava/web/GlobalExceptionHandler.java new file mode 100644 index 000000000000..bd401d1c150e --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/GlobalExceptionHandler.java @@ -0,0 +1,36 @@ +package ru.javawebinar.topjava.web; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.ModelAndView; +import ru.javawebinar.topjava.AuthorizedUser; +import ru.javawebinar.topjava.util.ValidationUtil; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +@ControllerAdvice +public class GlobalExceptionHandler { + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + @ExceptionHandler(Exception.class) + public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception { + log.error("Exception at request " + req.getRequestURL(), e); + Throwable rootCause = ValidationUtil.getRootCause(e); + + HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; + ModelAndView mav = new ModelAndView("exception", + Map.of("exception", rootCause, "message", rootCause.toString(), "status", httpStatus)); + mav.setStatus(httpStatus); + + // Interceptor is not invoked, put userTo + AuthorizedUser authorizedUser = SecurityUtil.safeGet(); + if (authorizedUser != null) { + mav.addObject("userTo", authorizedUser.getUserTo()); + } + return mav; + } +} diff --git a/src/main/webapp/WEB-INF/jsp/exception.jsp b/src/main/webapp/WEB-INF/jsp/exception.jsp new file mode 100644 index 000000000000..90f84f4b1aeb --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/exception.jsp @@ -0,0 +1,26 @@ +<%@ page isErrorPage="true" contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + +
    +
    +
    +

    ${status}

    +

    +

    ${message}

    +
    +
    + + + + \ No newline at end of file From b7053703eae38b8b515e01ddebeb3102ce06f765 Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Fri, 14 Apr 2023 22:44:03 +0700 Subject: [PATCH 117/125] apply patch 10_12 --- config/curl.md | 5 +- .../topjava/util/ValidationUtil.java | 5 +- .../topjava/util/exception/ErrorInfo.java | 13 ++++ .../topjava/util/exception/ErrorType.java | 8 +++ .../IllegalRequestDataException.java | 7 ++ .../util/exception/NotFoundException.java | 5 -- .../topjava/web/ExceptionInfoHandler.java | 65 +++++++++++++++++++ .../topjava/web/meal/MealUIController.java | 1 + .../topjava/web/user/AdminUIController.java | 1 + .../webapp/resources/js/topjava.common.js | 4 +- 10 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java create mode 100644 src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java create mode 100644 src/main/java/ru/javawebinar/topjava/util/exception/IllegalRequestDataException.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java diff --git a/config/curl.md b/config/curl.md index fcaf6b9bc9e7..fbc748aa0c58 100644 --- a/config/curl.md +++ b/config/curl.md @@ -32,4 +32,7 @@ `curl -s -X POST -d '{"dateTime":"2020-02-01T12:00","description":"Created lunch","calories":300}' -H 'Content-Type:application/json;charset=UTF-8' http://localhost:8080/topjava/rest/profile/meals --user user@yandex.ru:password` #### update Meals -`curl -s -X PUT -d '{"dateTime":"2020-01-30T07:00", "description":"Updated breakfast", "calories":200}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/profile/meals/100003 --user user@yandex.ru:password` \ No newline at end of file +`curl -s -X PUT -d '{"dateTime":"2020-01-30T07:00", "description":"Updated breakfast", "calories":200}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/profile/meals/100003 --user user@yandex.ru:password` + +#### validate with Error +`curl -s -X POST -d '{}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/admin/users --user admin@gmail.com:admin` diff --git a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java index ed5a7cd770e7..6c91f95fdf83 100644 --- a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java @@ -6,6 +6,7 @@ import org.springframework.lang.NonNull; import org.springframework.validation.BindingResult; import ru.javawebinar.topjava.HasId; +import ru.javawebinar.topjava.util.exception.IllegalRequestDataException; import ru.javawebinar.topjava.util.exception.NotFoundException; import javax.validation.*; @@ -56,7 +57,7 @@ public static void checkNotFound(boolean found, String msg) { public static void checkNew(HasId bean) { if (!bean.isNew()) { - throw new IllegalArgumentException(bean + " must be new (id=null)"); + throw new IllegalRequestDataException(bean + " must be new (id=null)"); } } @@ -65,7 +66,7 @@ public static void assureIdConsistent(HasId bean, int id) { if (bean.isNew()) { bean.setId(id); } else if (bean.id() != id) { - throw new IllegalArgumentException(bean + " must be with id=" + id); + throw new IllegalRequestDataException(bean + " must be with id=" + id); } } diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java new file mode 100644 index 000000000000..d43323590d95 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java @@ -0,0 +1,13 @@ +package ru.javawebinar.topjava.util.exception; + +public class ErrorInfo { + private final String url; + private final ErrorType type; + private final String detail; + + public ErrorInfo(CharSequence url, ErrorType type, String detail) { + this.url = url.toString(); + this.type = type; + this.detail = detail; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java new file mode 100644 index 000000000000..c53a433bc668 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java @@ -0,0 +1,8 @@ +package ru.javawebinar.topjava.util.exception; + +public enum ErrorType { + APP_ERROR, + DATA_NOT_FOUND, + DATA_ERROR, + VALIDATION_ERROR +} diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/IllegalRequestDataException.java b/src/main/java/ru/javawebinar/topjava/util/exception/IllegalRequestDataException.java new file mode 100644 index 000000000000..2b144f91c91c --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/IllegalRequestDataException.java @@ -0,0 +1,7 @@ +package ru.javawebinar.topjava.util.exception; + +public class IllegalRequestDataException extends RuntimeException { + public IllegalRequestDataException(String msg) { + super(msg); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java index 9ff0bbbb417f..f1e9b0e46376 100644 --- a/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java +++ b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java @@ -1,10 +1,5 @@ package ru.javawebinar.topjava.util.exception; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -// http://stackoverflow.com/a/22358422/548473 -@ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY, reason = "No data found") // 422 public class NotFoundException extends RuntimeException { public NotFoundException(String message) { super(message); diff --git a/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java new file mode 100644 index 000000000000..5b7e9575810c --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java @@ -0,0 +1,65 @@ +package ru.javawebinar.topjava.web; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import ru.javawebinar.topjava.util.ValidationUtil; +import ru.javawebinar.topjava.util.exception.ErrorInfo; +import ru.javawebinar.topjava.util.exception.ErrorType; +import ru.javawebinar.topjava.util.exception.IllegalRequestDataException; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import javax.servlet.http.HttpServletRequest; + +import static ru.javawebinar.topjava.util.exception.ErrorType.*; + +@RestControllerAdvice(annotations = RestController.class) +@Order(Ordered.HIGHEST_PRECEDENCE + 5) +public class ExceptionInfoHandler { + private static final Logger log = LoggerFactory.getLogger(ExceptionInfoHandler.class); + + // http://stackoverflow.com/a/22358422/548473 + @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) + @ExceptionHandler(NotFoundException.class) + public ErrorInfo notFoundError(HttpServletRequest req, NotFoundException e) { + return logAndGetErrorInfo(req, e, false, DATA_NOT_FOUND); + } + + @ResponseStatus(HttpStatus.CONFLICT) // 409 + @ExceptionHandler(DataIntegrityViolationException.class) + public ErrorInfo conflict(HttpServletRequest req, DataIntegrityViolationException e) { + return logAndGetErrorInfo(req, e, true, DATA_ERROR); + } + + @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) // 422 + @ExceptionHandler({IllegalRequestDataException.class, MethodArgumentTypeMismatchException.class, HttpMessageNotReadableException.class}) + public ErrorInfo validationError(HttpServletRequest req, Exception e) { + return logAndGetErrorInfo(req, e, false, VALIDATION_ERROR); + } + + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(Exception.class) + public ErrorInfo internalError(HttpServletRequest req, Exception e) { + return logAndGetErrorInfo(req, e, true, APP_ERROR); + } + + // https://stackoverflow.com/questions/538870/should-private-helper-methods-be-static-if-they-can-be-static + private static ErrorInfo logAndGetErrorInfo(HttpServletRequest req, Exception e, boolean logException, ErrorType errorType) { + Throwable rootCause = ValidationUtil.getRootCause(e); + if (logException) { + log.error(errorType + " at request " + req.getRequestURL(), rootCause); + } else { + log.warn("{} at request {}: {}", errorType, req.getRequestURL(), rootCause.toString()); + } + return new ErrorInfo(req.getRequestURL(), errorType, rootCause.toString()); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java index 2b4f2bdcdcd1..67715ce7b58e 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java @@ -42,6 +42,7 @@ public void delete(@PathVariable int id) { @ResponseStatus(HttpStatus.NO_CONTENT) public ResponseEntity createOrUpdate(@Valid Meal meal, BindingResult result) { if (result.hasErrors()) { + // TODO change to exception handler return ValidationUtil.getErrorResponse(result); } if (meal.isNew()) { diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java index 98084be5a5e2..0f52185605b8 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java @@ -39,6 +39,7 @@ public void delete(@PathVariable int id) { @ResponseStatus(HttpStatus.NO_CONTENT) public ResponseEntity createOrUpdate(@Valid UserTo userTo, BindingResult result) { if (result.hasErrors()) { + // TODO change to exception handler return ValidationUtil.getErrorResponse(result); } if (userTo.isNew()) { diff --git a/src/main/webapp/resources/js/topjava.common.js b/src/main/webapp/resources/js/topjava.common.js index c1269a191244..d58331ee460e 100644 --- a/src/main/webapp/resources/js/topjava.common.js +++ b/src/main/webapp/resources/js/topjava.common.js @@ -101,8 +101,10 @@ function renderDeleteBtn(data, type, row) { function failNoty(jqXHR) { closeNoty(); + var errorInfo = jqXHR.responseJSON; failedNote = new Noty({ - text: "  " + i18n["common.errorStatus"] + ": " + jqXHR.status + (jqXHR.responseJSON ? "
    " + jqXHR.responseJSON : ""), + text: "  " + i18n["common.errorStatus"] + ": " + jqXHR.status + + "
    " + errorInfo.type + "
    " + errorInfo.detail, type: "error", layout: "bottomRight" }); From 98bdba0f0aca852d6288c0a327bdd43bdef8fbde Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Fri, 14 Apr 2023 23:33:18 +0700 Subject: [PATCH 118/125] apply patch 10_13 --- .../topjava/service/UserService.java | 17 ++++++++++++----- .../ru/javawebinar/topjava/util/UsersUtil.java | 7 +++++++ src/main/resources/db/populateDB.sql | 6 +++--- src/main/resources/spring/spring-security.xml | 4 ++-- .../ru/javawebinar/topjava/UserTestData.java | 4 ++-- .../service/AbstractUserServiceTest.java | 1 - src/test/resources/spring/inmemory.xml | 1 + 7 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/service/UserService.java b/src/main/java/ru/javawebinar/topjava/service/UserService.java index 47874971c5bd..19070ed22312 100644 --- a/src/main/java/ru/javawebinar/topjava/service/UserService.java +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -6,6 +6,7 @@ import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; @@ -17,6 +18,7 @@ import java.util.List; +import static ru.javawebinar.topjava.util.UsersUtil.prepareToSave; import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFound; import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; @@ -25,15 +27,17 @@ public class UserService implements UserDetailsService { private final UserRepository repository; + private final PasswordEncoder passwordEncoder; - public UserService(UserRepository repository) { + public UserService(UserRepository repository, PasswordEncoder passwordEncoder) { this.repository = repository; + this.passwordEncoder = passwordEncoder; } @CacheEvict(value = "users", allEntries = true) public User create(User user) { Assert.notNull(user, "user must not be null"); - return repository.save(user); + return prepareAndSave(user); } @CacheEvict(value = "users", allEntries = true) @@ -59,15 +63,14 @@ public List getAll() { public void update(User user) { Assert.notNull(user, "user must not be null"); // checkNotFoundWithId : check works only for JDBC, disabled - repository.save(user); + prepareAndSave(user); } @CacheEvict(value = "users", allEntries = true) @Transactional public void update(UserTo userTo) { User user = get(userTo.id()); - User updatedUser = UsersUtil.updateFromTo(user, userTo); - repository.save(updatedUser); // !! need only for JDBC implementation + prepareAndSave(UsersUtil.updateFromTo(user, userTo)); } @CacheEvict(value = "users", allEntries = true) @@ -87,6 +90,10 @@ public AuthorizedUser loadUserByUsername(String email) throws UsernameNotFoundEx return new AuthorizedUser(user); } + private User prepareAndSave(User user) { + return repository.save(prepareToSave(user, passwordEncoder)); + } + public User getWithMeals(int id) { return checkNotFoundWithId(repository.getWithMeals(id), id); } diff --git a/src/main/java/ru/javawebinar/topjava/util/UsersUtil.java b/src/main/java/ru/javawebinar/topjava/util/UsersUtil.java index c225802ea468..bdb4291f7d65 100644 --- a/src/main/java/ru/javawebinar/topjava/util/UsersUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/UsersUtil.java @@ -1,5 +1,6 @@ package ru.javawebinar.topjava.util; +import org.springframework.security.crypto.password.PasswordEncoder; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.to.UserTo; @@ -23,4 +24,10 @@ public static User updateFromTo(User user, UserTo userTo) { user.setPassword(userTo.getPassword()); return user; } + + public static User prepareToSave(User user, PasswordEncoder passwordEncoder) { + user.setPassword(passwordEncoder.encode(user.getPassword())); + user.setEmail(user.getEmail().toLowerCase()); + return user; + } } \ No newline at end of file diff --git a/src/main/resources/db/populateDB.sql b/src/main/resources/db/populateDB.sql index 0f366e06ab71..8d66cc0e5e12 100644 --- a/src/main/resources/db/populateDB.sql +++ b/src/main/resources/db/populateDB.sql @@ -4,9 +4,9 @@ DELETE FROM users; ALTER SEQUENCE global_seq RESTART WITH 100000; INSERT INTO users (name, email, password, calories_per_day) -VALUES ('User', 'user@yandex.ru', 'password', 2005), - ('Admin', 'admin@gmail.com', 'admin', 1900), - ('Guest', 'guest@gmail.com', 'guest', 2000); +VALUES ('User', 'user@yandex.ru', '{noop}password', 2005), + ('Admin', 'admin@gmail.com', '{noop}admin', 1900), + ('Guest', 'guest@gmail.com', '{noop}guest', 2000); INSERT INTO user_role (role, user_id) VALUES ('USER', 100000), diff --git a/src/main/resources/spring/spring-security.xml b/src/main/resources/spring/spring-security.xml index 93139e502106..de7d5620df52 100644 --- a/src/main/resources/spring/spring-security.xml +++ b/src/main/resources/spring/spring-security.xml @@ -15,7 +15,7 @@ - + @@ -33,7 +33,7 @@ - + + diff --git a/src/main/resources/spring/spring-security.xml b/src/main/resources/spring/spring-security.xml index de7d5620df52..efcda19fe5ee 100644 --- a/src/main/resources/spring/spring-security.xml +++ b/src/main/resources/spring/spring-security.xml @@ -28,7 +28,6 @@ authentication-failure-url="/login?error=true" login-processing-url="/spring_security_check"/> - diff --git a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp index 6e7f3aafca7b..62f05f9a4ac3 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp @@ -1,29 +1,30 @@ <%@page contentType="text/html" pageEncoding="UTF-8" %> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> +<%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %> diff --git a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp index 8002a0dc56cf..f97a9293fe7a 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp @@ -4,6 +4,10 @@ + + + + <spring:message code="app.title"/> diff --git a/src/main/webapp/resources/js/topjava.common.js b/src/main/webapp/resources/js/topjava.common.js index d58331ee460e..261659f9fcf2 100644 --- a/src/main/webapp/resources/js/topjava.common.js +++ b/src/main/webapp/resources/js/topjava.common.js @@ -21,6 +21,12 @@ function makeEditable(datatableOpts) { // solve problem with cache in IE: https://stackoverflow.com/a/4303862/548473 $.ajaxSetup({cache: false}); + + var token = $("meta[name='_csrf']").attr("content"); + var header = $("meta[name='_csrf_header']").attr("content"); + $(document).ajaxSend(function (e, xhr, options) { + xhr.setRequestHeader(header, token); + }); } function add() { From cb65546b49f527685ed9ece0a09c253ee9e04c8c Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 4 May 2023 10:47:57 +0700 Subject: [PATCH 121/125] apply patches 0-4 --- config/curl.md | 1 + config/messages/app.properties | 4 ++- config/messages/app_ru.properties | 2 ++ pom.xml | 9 ++++- .../ru/javawebinar/topjava/model/Meal.java | 2 +- .../ru/javawebinar/topjava/to/UserTo.java | 2 +- .../topjava/util/ValidationUtil.java | 25 +++++++------ .../topjava/util/exception/ErrorInfo.java | 6 ++-- .../topjava/web/ExceptionInfoHandler.java | 23 +++++++----- .../topjava/web/GlobalExceptionHandler.java | 5 +-- .../topjava/web/meal/MealRestController.java | 5 +-- .../topjava/web/meal/MealUIController.java | 10 +----- .../web/user/AbstractUserController.java | 2 ++ .../topjava/web/user/AdminRestController.java | 5 +-- .../topjava/web/user/AdminUIController.java | 30 +++++++++------- .../web/user/ProfileRestController.java | 5 +-- .../topjava/web/user/ProfileUIController.java | 14 ++++++-- src/main/webapp/WEB-INF/web.xml | 20 +++++------ .../webapp/resources/js/topjava.common.js | 2 +- .../web/meal/MealRestControllerTest.java | 36 +++++++++++++++---- .../web/user/AdminRestControllerTest.java | 31 ++++++++++++++-- .../web/user/ProfileRestControllerTest.java | 27 ++++++++++++-- 22 files changed, 187 insertions(+), 79 deletions(-) diff --git a/config/curl.md b/config/curl.md index fbc748aa0c58..76852a8a1cfb 100644 --- a/config/curl.md +++ b/config/curl.md @@ -36,3 +36,4 @@ #### validate with Error `curl -s -X POST -d '{}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/admin/users --user admin@gmail.com:admin` +`curl -s -X PUT -d '{"dateTime":"2015-05-30T07:00"}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/profile/meals/100003 --user user@yandex.ru:password` diff --git a/config/messages/app.properties b/config/messages/app.properties index 979407b488bd..257a0699739b 100644 --- a/config/messages/app.properties +++ b/config/messages/app.properties @@ -43,4 +43,6 @@ common.errorStatus=Error status common.appError=Application error common.confirm=Are you sure? common.save=Save -common.cancel=Cancel \ No newline at end of file +common.cancel=Cancel + +exception.user.duplicateEmail=User with this email already exists \ No newline at end of file diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties index 2a3ac2859934..e508c9db54de 100644 --- a/config/messages/app_ru.properties +++ b/config/messages/app_ru.properties @@ -44,3 +44,5 @@ common.appError=Ошибка приложения common.confirm=Вы уверены? common.save=Сохранить common.cancel=Отменить + +exception.user.duplicateEmail=Пользователь с такой почтой уже есть в приложении diff --git a/pom.xml b/pom.xml index 67d193561e34..8086053da57a 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ 1.0-SNAPSHOT Calories Management - http://topjava.herokuapp.com/ + http://javaops-demo.ru/topjava 17 @@ -46,6 +46,7 @@ 5.9.2 3.24.2 2.2 + 2.8.0 @@ -313,6 +314,12 @@ ${hamcrest.version} test + + com.jayway.jsonpath + json-path + ${json-path.version} + test + org.springframework diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index d7e0c07a8b0e..00e7e6372403 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -27,7 +27,7 @@ // "m.description=:desc where m.id=:id and m.user.id=:userId") }) @Entity -@Table(name = "meal", uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "date_time"}, name = "meals_unique_user_datetime_idx")}) +@Table(name = "meal", uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "date_time"}, name = "meal_unique_user_datetime_idx")}) public class Meal extends AbstractBaseEntity { public static final String ALL_SORTED = "Meal.getAll"; public static final String DELETE = "Meal.delete"; diff --git a/src/main/java/ru/javawebinar/topjava/to/UserTo.java b/src/main/java/ru/javawebinar/topjava/to/UserTo.java index 217d7390a5b4..af19862a6f46 100644 --- a/src/main/java/ru/javawebinar/topjava/to/UserTo.java +++ b/src/main/java/ru/javawebinar/topjava/to/UserTo.java @@ -24,7 +24,7 @@ public class UserTo extends BaseTo implements Serializable { private String email; @NotBlank - @Size(min = 5, max = 32, message = "length must be between 5 and 32 characters") + @Size(min = 5, max = 32) private String password; @Range(min = 10, max = 10000) diff --git a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java index 6c91f95fdf83..855d3d6e1b08 100644 --- a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java @@ -1,17 +1,16 @@ package ru.javawebinar.topjava.util; - +import org.slf4j.Logger; import org.springframework.core.NestedExceptionUtils; -import org.springframework.http.ResponseEntity; import org.springframework.lang.NonNull; -import org.springframework.validation.BindingResult; import ru.javawebinar.topjava.HasId; +import ru.javawebinar.topjava.util.exception.ErrorType; import ru.javawebinar.topjava.util.exception.IllegalRequestDataException; import ru.javawebinar.topjava.util.exception.NotFoundException; +import javax.servlet.http.HttpServletRequest; import javax.validation.*; import java.util.Set; -import java.util.stream.Collectors; public class ValidationUtil { @@ -77,11 +76,17 @@ public static Throwable getRootCause(@NonNull Throwable t) { return rootCause != null ? rootCause : t; } - public static ResponseEntity getErrorResponse(BindingResult result) { - return ResponseEntity.unprocessableEntity().body( - result.getFieldErrors().stream() - .map(fe -> String.format("[%s] %s", fe.getField(), fe.getDefaultMessage())) - .collect(Collectors.joining("
    ")) - ); + public static String getMessage(Throwable e) { + return e.getLocalizedMessage() != null ? e.getLocalizedMessage() : e.getClass().getName(); + } + + public static Throwable logAndGetRootCause(Logger log, HttpServletRequest req, Exception e, boolean logStackTrace, ErrorType errorType) { + Throwable rootCause = ValidationUtil.getRootCause(e); + if (logStackTrace) { + log.error(errorType + " at request " + req.getRequestURL(), rootCause); + } else { + log.warn("{} at request {}: {}", errorType, req.getRequestURL(), rootCause.toString()); + } + return rootCause; } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java index d43323590d95..0c5e15a94910 100644 --- a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java @@ -3,11 +3,11 @@ public class ErrorInfo { private final String url; private final ErrorType type; - private final String detail; + private final String[] details; - public ErrorInfo(CharSequence url, ErrorType type, String detail) { + public ErrorInfo(CharSequence url, ErrorType type, String... details) { this.url = url.toString(); this.type = type; - this.detail = detail; + this.details = details; } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java index 5b7e9575810c..291815e31a37 100644 --- a/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java +++ b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java @@ -7,6 +7,7 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @@ -40,6 +41,16 @@ public ErrorInfo conflict(HttpServletRequest req, DataIntegrityViolationExceptio return logAndGetErrorInfo(req, e, true, DATA_ERROR); } + @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY) // 422 + @ExceptionHandler(BindException.class) + public ErrorInfo bindValidationError(HttpServletRequest req, BindException e) { + String[] details = e.getBindingResult().getFieldErrors().stream() + .map(fe -> String.format("[%s] %s", fe.getField(), fe.getDefaultMessage())) + .toArray(String[]::new); + + return logAndGetErrorInfo(req, e, false, VALIDATION_ERROR, details); + } + @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) // 422 @ExceptionHandler({IllegalRequestDataException.class, MethodArgumentTypeMismatchException.class, HttpMessageNotReadableException.class}) public ErrorInfo validationError(HttpServletRequest req, Exception e) { @@ -53,13 +64,9 @@ public ErrorInfo internalError(HttpServletRequest req, Exception e) { } // https://stackoverflow.com/questions/538870/should-private-helper-methods-be-static-if-they-can-be-static - private static ErrorInfo logAndGetErrorInfo(HttpServletRequest req, Exception e, boolean logException, ErrorType errorType) { - Throwable rootCause = ValidationUtil.getRootCause(e); - if (logException) { - log.error(errorType + " at request " + req.getRequestURL(), rootCause); - } else { - log.warn("{} at request {}: {}", errorType, req.getRequestURL(), rootCause.toString()); - } - return new ErrorInfo(req.getRequestURL(), errorType, rootCause.toString()); + private static ErrorInfo logAndGetErrorInfo(HttpServletRequest req, Exception e, boolean logStackTrace, ErrorType errorType, String... details) { + Throwable rootCause = ValidationUtil.logAndGetRootCause(log, req, e, logStackTrace, errorType); + return new ErrorInfo(req.getRequestURL(), errorType, + details.length != 0 ? details : new String[]{ValidationUtil.getMessage(rootCause)}); } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/GlobalExceptionHandler.java b/src/main/java/ru/javawebinar/topjava/web/GlobalExceptionHandler.java index bd401d1c150e..8e995ba21c60 100644 --- a/src/main/java/ru/javawebinar/topjava/web/GlobalExceptionHandler.java +++ b/src/main/java/ru/javawebinar/topjava/web/GlobalExceptionHandler.java @@ -8,6 +8,7 @@ import org.springframework.web.servlet.ModelAndView; import ru.javawebinar.topjava.AuthorizedUser; import ru.javawebinar.topjava.util.ValidationUtil; +import ru.javawebinar.topjava.util.exception.ErrorType; import javax.servlet.http.HttpServletRequest; import java.util.Map; @@ -19,11 +20,11 @@ public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception { log.error("Exception at request " + req.getRequestURL(), e); - Throwable rootCause = ValidationUtil.getRootCause(e); + Throwable rootCause = ValidationUtil.logAndGetRootCause(log, req, e, true, ErrorType.APP_ERROR); HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; ModelAndView mav = new ModelAndView("exception", - Map.of("exception", rootCause, "message", rootCause.toString(), "status", httpStatus)); + Map.of("exception", rootCause, "message", ValidationUtil.getMessage(rootCause), "status", httpStatus)); mav.setStatus(httpStatus); // Interceptor is not invoked, put userTo diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java index 66df6c06eb20..1e46566ddd9b 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java @@ -9,6 +9,7 @@ import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.to.MealTo; +import javax.validation.Valid; import java.net.URI; import java.time.LocalDate; import java.time.LocalTime; @@ -41,12 +42,12 @@ public List getAll() { @Override @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.NO_CONTENT) - public void update(@RequestBody Meal meal, @PathVariable int id) { + public void update(@Valid @RequestBody Meal meal, @PathVariable int id) { super.update(meal, id); } @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity createWithLocation(@RequestBody Meal meal) { + public ResponseEntity createWithLocation(@Valid @RequestBody Meal meal) { Meal created = super.create(meal); URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java index 67715ce7b58e..d287226913f1 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java @@ -2,13 +2,10 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.lang.Nullable; -import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.to.MealTo; -import ru.javawebinar.topjava.util.ValidationUtil; import javax.validation.Valid; import java.time.LocalDate; @@ -40,17 +37,12 @@ public void delete(@PathVariable int id) { @PostMapping @ResponseStatus(HttpStatus.NO_CONTENT) - public ResponseEntity createOrUpdate(@Valid Meal meal, BindingResult result) { - if (result.hasErrors()) { - // TODO change to exception handler - return ValidationUtil.getErrorResponse(result); - } + public void createOrUpdate(@Valid Meal meal) { if (meal.isNew()) { super.create(meal); } else { super.update(meal, meal.getId()); } - return ResponseEntity.ok().build(); } @Override diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java index 18e3698e8f52..211ad1d92684 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java @@ -16,6 +16,8 @@ public abstract class AbstractUserController { protected final Logger log = LoggerFactory.getLogger(getClass()); + public static final String EXCEPTION_DUPLICATE_EMAIL = "exception.user.duplicateEmail"; + @Autowired private UserService service; diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java index dfc40e6c6b03..b80e43ef1103 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java @@ -7,6 +7,7 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import ru.javawebinar.topjava.model.User; +import javax.validation.Valid; import java.net.URI; import java.util.List; @@ -29,7 +30,7 @@ public User get(@PathVariable int id) { } @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity createWithLocation(@RequestBody User user) { + public ResponseEntity createWithLocation(@Valid @RequestBody User user) { User created = super.create(user); URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() .path(REST_URL + "/{id}") @@ -47,7 +48,7 @@ public void delete(@PathVariable int id) { @Override @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.NO_CONTENT) - public void update(@RequestBody User user, @PathVariable int id) { + public void update(@Valid @RequestBody User user, @PathVariable int id) { super.update(user, id); } diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java index 0f52185605b8..7013bf831943 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java @@ -1,13 +1,15 @@ package ru.javawebinar.topjava.web.user; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.to.UserTo; -import ru.javawebinar.topjava.util.ValidationUtil; +import ru.javawebinar.topjava.util.exception.IllegalRequestDataException; import javax.validation.Valid; import java.util.List; @@ -16,6 +18,9 @@ @RequestMapping(value = "/admin/users", produces = MediaType.APPLICATION_JSON_VALUE) public class AdminUIController extends AbstractUserController { + @Autowired + private MessageSource messageSource; + @Override @GetMapping public List getAll() { @@ -37,17 +42,16 @@ public void delete(@PathVariable int id) { @PostMapping @ResponseStatus(HttpStatus.NO_CONTENT) - public ResponseEntity createOrUpdate(@Valid UserTo userTo, BindingResult result) { - if (result.hasErrors()) { - // TODO change to exception handler - return ValidationUtil.getErrorResponse(result); - } - if (userTo.isNew()) { - super.create(userTo); - } else { - super.update(userTo, userTo.id()); + public void createOrUpdate(@Valid UserTo userTo) { + try { + if (userTo.isNew()) { + super.create(userTo); + } else { + super.update(userTo, userTo.id()); + } + } catch (DataIntegrityViolationException e) { + throw new IllegalRequestDataException(messageSource.getMessage(EXCEPTION_DUPLICATE_EMAIL, null, LocaleContextHolder.getLocale())); } - return ResponseEntity.ok().build(); } @Override diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java index c99f2ccaa689..16dd3134a843 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -8,6 +8,7 @@ import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.to.UserTo; +import javax.validation.Valid; import java.net.URI; import static ru.javawebinar.topjava.web.SecurityUtil.authUserId; @@ -30,7 +31,7 @@ public void delete() { @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.CREATED) - public ResponseEntity register(@RequestBody UserTo userTo) { + public ResponseEntity register(@Valid @RequestBody UserTo userTo) { User created = super.create(userTo); URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() .path(REST_URL).build().toUri(); @@ -39,7 +40,7 @@ public ResponseEntity register(@RequestBody UserTo userTo) { @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.NO_CONTENT) - public void update(@RequestBody UserTo userTo) { + public void update(@Valid @RequestBody UserTo userTo) { super.update(userTo, authUserId()); } diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileUIController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileUIController.java index 657396a7dc18..bd1a4238e501 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/ProfileUIController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileUIController.java @@ -1,5 +1,6 @@ package ru.javawebinar.topjava.web.user; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.validation.BindingResult; @@ -25,11 +26,15 @@ public String profile() { public String updateProfile(@Valid UserTo userTo, BindingResult result, SessionStatus status) { if (result.hasErrors()) { return "profile"; - } else { + } + try { super.update(userTo, SecurityUtil.authUserId()); SecurityUtil.get().setTo(userTo); status.setComplete(); return "redirect:/meals"; + } catch (DataIntegrityViolationException ex) { + result.rejectValue("email", EXCEPTION_DUPLICATE_EMAIL); + return "profile"; } } @@ -45,10 +50,15 @@ public String saveRegister(@Valid UserTo userTo, BindingResult result, SessionSt if (result.hasErrors()) { model.addAttribute("register", true); return "profile"; - } else { + } + try { super.create(userTo); status.setComplete(); return "redirect:/login?message=app.registered&username=" + userTo.getEmail(); + } catch (DataIntegrityViolationException ex) { + result.rejectValue("email", EXCEPTION_DUPLICATE_EMAIL); + model.addAttribute("register", true); + return "profile"; } } } \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 31dcf26765fd..e71b4d4a0341 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -37,16 +37,6 @@ / - - - springSecurityFilterChain - org.springframework.web.filter.DelegatingFilterProxy - - - springSecurityFilterChain - /* - - encodingFilter org.springframework.web.filter.CharacterEncodingFilter @@ -63,4 +53,14 @@ encodingFilter /* + + + + springSecurityFilterChain + org.springframework.web.filter.DelegatingFilterProxy + + + springSecurityFilterChain + /* + diff --git a/src/main/webapp/resources/js/topjava.common.js b/src/main/webapp/resources/js/topjava.common.js index 261659f9fcf2..70acef27a5ad 100644 --- a/src/main/webapp/resources/js/topjava.common.js +++ b/src/main/webapp/resources/js/topjava.common.js @@ -110,7 +110,7 @@ function failNoty(jqXHR) { var errorInfo = jqXHR.responseJSON; failedNote = new Noty({ text: "  " + i18n["common.errorStatus"] + ": " + jqXHR.status + - "
    " + errorInfo.type + "
    " + errorInfo.detail, + "
    " + errorInfo.type + "
    " + errorInfo.details.join("
    "), type: "error", layout: "bottomRight" }); diff --git a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java index 784bc047917c..851d7b5bc3a7 100644 --- a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java @@ -6,20 +6,20 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import ru.javawebinar.topjava.MealTestData; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.service.MealService; +import ru.javawebinar.topjava.util.exception.ErrorType; import ru.javawebinar.topjava.util.exception.NotFoundException; import ru.javawebinar.topjava.web.AbstractControllerTest; import ru.javawebinar.topjava.web.json.JsonUtil; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static ru.javawebinar.topjava.MealTestData.*; import static ru.javawebinar.topjava.TestUtil.userHttpBasic; -import static ru.javawebinar.topjava.UserTestData.USER_ID; -import static ru.javawebinar.topjava.UserTestData.user; +import static ru.javawebinar.topjava.UserTestData.*; import static ru.javawebinar.topjava.util.MealsUtil.createTo; import static ru.javawebinar.topjava.util.MealsUtil.getTos; @@ -71,7 +71,7 @@ void deleteNotFound() throws Exception { @Test void update() throws Exception { - Meal updated = getUpdated(); + Meal updated = MealTestData.getUpdated(); perform(MockMvcRequestBuilders.put(REST_URL + MEAL1_ID).contentType(MediaType.APPLICATION_JSON) .with(userHttpBasic(user)) .content(JsonUtil.writeValue(updated))) @@ -82,7 +82,7 @@ void update() throws Exception { @Test void createWithLocation() throws Exception { - Meal newMeal = getNew(); + Meal newMeal = MealTestData.getNew(); ResultActions action = perform(MockMvcRequestBuilders.post(REST_URL) .contentType(MediaType.APPLICATION_JSON) .with(userHttpBasic(user)) @@ -124,4 +124,28 @@ void getBetweenAll() throws Exception { .andExpect(status().isOk()) .andExpect(TO_MATCHER.contentJson(getTos(meals, user.getCaloriesPerDay()))); } + + @Test + void createInvalid() throws Exception { + Meal invalid = new Meal(null, null, "Dummy", 200); + perform(MockMvcRequestBuilders.post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(invalid)) + .with(userHttpBasic(admin))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()) + .andExpect(jsonPath("$.type").value(ErrorType.VALIDATION_ERROR.name())); + } + + @Test + void updateInvalid() throws Exception { + Meal invalid = new Meal(MEAL1_ID, null, null, 6000); + perform(MockMvcRequestBuilders.put(REST_URL + MEAL1_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(invalid)) + .with(userHttpBasic(user))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()) + .andExpect(jsonPath("$.type").value(ErrorType.VALIDATION_ERROR.name())); + } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java index dc52b0083bde..63cb9db8491a 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java @@ -5,17 +5,17 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.service.UserService; +import ru.javawebinar.topjava.util.exception.ErrorType; import ru.javawebinar.topjava.util.exception.NotFoundException; import ru.javawebinar.topjava.web.AbstractControllerTest; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static ru.javawebinar.topjava.TestUtil.userHttpBasic; import static ru.javawebinar.topjava.UserTestData.*; @@ -143,4 +143,29 @@ void enable() throws Exception { assertFalse(userService.get(USER_ID).isEnabled()); } + + @Test + void createInvalid() throws Exception { + User invalid = new User(null, null, "", "newPass", 7300, Role.USER, Role.ADMIN); + perform(MockMvcRequestBuilders.post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(admin)) + .content(jsonWithPassword(invalid, "newPass"))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()) + .andExpect(jsonPath("$.type").value(ErrorType.VALIDATION_ERROR.name())); + } + + @Test + void updateInvalid() throws Exception { + User invalid = new User(user); + invalid.setName(""); + perform(MockMvcRequestBuilders.put(REST_URL + USER_ID) + .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(admin)) + .content(jsonWithPassword(invalid, "password"))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()) + .andExpect(jsonPath("$.type").value(ErrorType.VALIDATION_ERROR.name())); + } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java index 9547ccff4094..822256b24be3 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java @@ -9,12 +9,12 @@ import ru.javawebinar.topjava.service.UserService; import ru.javawebinar.topjava.to.UserTo; import ru.javawebinar.topjava.util.UsersUtil; +import ru.javawebinar.topjava.util.exception.ErrorType; import ru.javawebinar.topjava.web.AbstractControllerTest; import ru.javawebinar.topjava.web.json.JsonUtil; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static ru.javawebinar.topjava.TestUtil.userHttpBasic; import static ru.javawebinar.topjava.UserTestData.*; import static ru.javawebinar.topjava.web.user.ProfileRestController.REST_URL; @@ -86,4 +86,27 @@ void getWithMeals() throws Exception { .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andExpect(USER_WITH_MEALS_MATCHER.contentJson(user)); } + + @Test + void registerInvalid() throws Exception { + UserTo newTo = new UserTo(null, null, null, null, 1); + perform(MockMvcRequestBuilders.post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(newTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()) + .andExpect(jsonPath("$.type").value(ErrorType.VALIDATION_ERROR.name())); + } + + @Test + void updateInvalid() throws Exception { + UserTo updatedTo = new UserTo(null, null, "password", null, 1500); + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(user)) + .content(JsonUtil.writeValue(updatedTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()) + .andExpect(jsonPath("$.type").value(ErrorType.VALIDATION_ERROR.name())); + } } \ No newline at end of file From cdc168d28ce07db80ba7938e36b4c69fbebcad4a Mon Sep 17 00:00:00 2001 From: Vadim Polosov Date: Thu, 4 May 2023 10:58:48 +0700 Subject: [PATCH 122/125] apply patches HW10 --- config/messages/app.properties | 3 +- config/messages/app_ru.properties | 1 + .../ru/javawebinar/topjava/HasIdAndEmail.java | 5 ++ .../ru/javawebinar/topjava/model/User.java | 4 +- .../ru/javawebinar/topjava/to/UserTo.java | 4 +- .../topjava/web/ExceptionInfoHandler.java | 26 +++++++- .../web/user/AbstractUserController.java | 12 +++- .../topjava/web/user/AdminUIController.java | 20 ++---- .../topjava/web/user/ProfileUIController.java | 26 +++----- .../topjava/web/user/UniqueMailValidator.java | 54 ++++++++++++++++ src/main/resources/spring/spring-mvc.xml | 12 ++++ .../WEB-INF/jsp/fragments/bodyHeader.jsp | 63 ++++++++++++------- src/main/webapp/WEB-INF/jsp/profile.jsp | 1 + src/main/webapp/resources/js/topjava.meals.js | 2 + .../ru/javawebinar/topjava/UserTestData.java | 5 +- .../topjava/web/AbstractControllerTest.java | 22 ++++++- .../web/meal/MealRestControllerTest.java | 41 ++++++++++-- .../web/user/AdminRestControllerTest.java | 45 +++++++++++-- .../web/user/ProfileRestControllerTest.java | 27 ++++++-- src/test/resources/spring/inmemory.xml | 3 +- 20 files changed, 298 insertions(+), 78 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/HasIdAndEmail.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/user/UniqueMailValidator.java diff --git a/config/messages/app.properties b/config/messages/app.properties index 257a0699739b..52ed9707ec66 100644 --- a/config/messages/app.properties +++ b/config/messages/app.properties @@ -45,4 +45,5 @@ common.confirm=Are you sure? common.save=Save common.cancel=Cancel -exception.user.duplicateEmail=User with this email already exists \ No newline at end of file +exception.user.duplicateEmail=User with this email already exists +exception.meal.duplicateDateTime=You already have meal with this date/time \ No newline at end of file diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties index e508c9db54de..32752ddd8444 100644 --- a/config/messages/app_ru.properties +++ b/config/messages/app_ru.properties @@ -46,3 +46,4 @@ common.save=Сохранить common.cancel=Отменить exception.user.duplicateEmail=Пользователь с такой почтой уже есть в приложении +exception.meal.duplicateDateTime=У вас уже есть еда с такой датой/временем diff --git a/src/main/java/ru/javawebinar/topjava/HasIdAndEmail.java b/src/main/java/ru/javawebinar/topjava/HasIdAndEmail.java new file mode 100644 index 000000000000..6389876b5dc1 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/HasIdAndEmail.java @@ -0,0 +1,5 @@ +package ru.javawebinar.topjava; + +public interface HasIdAndEmail extends HasId { + String getEmail(); +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index 12c608d0cf27..28fecb3f8388 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -6,6 +6,7 @@ import org.hibernate.annotations.*; import org.hibernate.validator.constraints.Range; import org.springframework.util.CollectionUtils; +import ru.javawebinar.topjava.HasIdAndEmail; import javax.persistence.Entity; import javax.persistence.NamedQueries; @@ -29,7 +30,7 @@ }) @Entity @Table(name = "users") -public class User extends AbstractNamedEntity { +public class User extends AbstractNamedEntity implements HasIdAndEmail { public static final String DELETE = "User.delete"; public static final String BY_EMAIL = "User.getByEmail"; @@ -99,6 +100,7 @@ public User(Integer id, String name, String email, String password, int calories setRoles(roles); } + @Override public String getEmail() { return email; } diff --git a/src/main/java/ru/javawebinar/topjava/to/UserTo.java b/src/main/java/ru/javawebinar/topjava/to/UserTo.java index af19862a6f46..7ffd9c99be16 100644 --- a/src/main/java/ru/javawebinar/topjava/to/UserTo.java +++ b/src/main/java/ru/javawebinar/topjava/to/UserTo.java @@ -1,6 +1,7 @@ package ru.javawebinar.topjava.to; import org.hibernate.validator.constraints.Range; +import ru.javawebinar.topjava.HasIdAndEmail; import ru.javawebinar.topjava.util.UsersUtil; import javax.validation.constraints.Email; @@ -10,7 +11,7 @@ import java.io.Serial; import java.io.Serializable; -public class UserTo extends BaseTo implements Serializable { +public class UserTo extends BaseTo implements HasIdAndEmail, Serializable { @Serial private static final long serialVersionUID = 1L; @@ -58,6 +59,7 @@ public void setName(String name) { this.name = name; } + @Override public String getEmail() { return email; } diff --git a/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java index 291815e31a37..cff20b6f0f53 100644 --- a/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java +++ b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java @@ -2,6 +2,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.support.MessageSourceAccessor; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.dao.DataIntegrityViolationException; @@ -20,6 +21,7 @@ import ru.javawebinar.topjava.util.exception.NotFoundException; import javax.servlet.http.HttpServletRequest; +import java.util.Map; import static ru.javawebinar.topjava.util.exception.ErrorType.*; @@ -28,6 +30,19 @@ public class ExceptionInfoHandler { private static final Logger log = LoggerFactory.getLogger(ExceptionInfoHandler.class); + public static final String EXCEPTION_DUPLICATE_EMAIL = "exception.user.duplicateEmail"; + public static final String EXCEPTION_DUPLICATE_DATETIME = "exception.meal.duplicateDateTime"; + + private static final Map CONSTRAINTS_I18N_MAP = Map.of( + "users_unique_email_idx", EXCEPTION_DUPLICATE_EMAIL, + "meals_unique_user_datetime_idx", EXCEPTION_DUPLICATE_DATETIME); + + private final MessageSourceAccessor messageSourceAccessor; + + public ExceptionInfoHandler(MessageSourceAccessor messageSourceAccessor) { + this.messageSourceAccessor = messageSourceAccessor; + } + // http://stackoverflow.com/a/22358422/548473 @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) @ExceptionHandler(NotFoundException.class) @@ -38,6 +53,15 @@ public ErrorInfo notFoundError(HttpServletRequest req, NotFoundException e) { @ResponseStatus(HttpStatus.CONFLICT) // 409 @ExceptionHandler(DataIntegrityViolationException.class) public ErrorInfo conflict(HttpServletRequest req, DataIntegrityViolationException e) { + String rootMsg = ValidationUtil.getRootCause(e).getMessage(); + if (rootMsg != null) { + String lowerCaseMsg = rootMsg.toLowerCase(); + for (Map.Entry entry : CONSTRAINTS_I18N_MAP.entrySet()) { + if (lowerCaseMsg.contains(entry.getKey())) { + return logAndGetErrorInfo(req, e, false, VALIDATION_ERROR, messageSourceAccessor.getMessage(entry.getValue())); + } + } + } return logAndGetErrorInfo(req, e, true, DATA_ERROR); } @@ -45,7 +69,7 @@ public ErrorInfo conflict(HttpServletRequest req, DataIntegrityViolationExceptio @ExceptionHandler(BindException.class) public ErrorInfo bindValidationError(HttpServletRequest req, BindException e) { String[] details = e.getBindingResult().getFieldErrors().stream() - .map(fe -> String.format("[%s] %s", fe.getField(), fe.getDefaultMessage())) + .map(messageSourceAccessor::getMessage) .toArray(String[]::new); return logAndGetErrorInfo(req, e, false, VALIDATION_ERROR, details); diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java index 211ad1d92684..79cd7b5db38e 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java @@ -3,6 +3,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.service.UserService; import ru.javawebinar.topjava.to.UserTo; @@ -16,11 +18,17 @@ public abstract class AbstractUserController { protected final Logger log = LoggerFactory.getLogger(getClass()); - public static final String EXCEPTION_DUPLICATE_EMAIL = "exception.user.duplicateEmail"; - @Autowired private UserService service; + @Autowired + private UniqueMailValidator emailValidator; + + @InitBinder + protected void initBinder(WebDataBinder binder) { + binder.addValidators(emailValidator); + } + public List getAll() { log.info("getAll"); return service.getAll(); diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java index 7013bf831943..96f0fbc8358f 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java @@ -1,15 +1,10 @@ package ru.javawebinar.topjava.web.user; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.MessageSource; -import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.to.UserTo; -import ru.javawebinar.topjava.util.exception.IllegalRequestDataException; import javax.validation.Valid; import java.util.List; @@ -18,9 +13,6 @@ @RequestMapping(value = "/admin/users", produces = MediaType.APPLICATION_JSON_VALUE) public class AdminUIController extends AbstractUserController { - @Autowired - private MessageSource messageSource; - @Override @GetMapping public List getAll() { @@ -43,14 +35,10 @@ public void delete(@PathVariable int id) { @PostMapping @ResponseStatus(HttpStatus.NO_CONTENT) public void createOrUpdate(@Valid UserTo userTo) { - try { - if (userTo.isNew()) { - super.create(userTo); - } else { - super.update(userTo, userTo.id()); - } - } catch (DataIntegrityViolationException e) { - throw new IllegalRequestDataException(messageSource.getMessage(EXCEPTION_DUPLICATE_EMAIL, null, LocaleContextHolder.getLocale())); + if (userTo.isNew()) { + super.create(userTo); + } else { + super.update(userTo, userTo.id()); } } diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileUIController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileUIController.java index bd1a4238e501..8091371babd4 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/ProfileUIController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileUIController.java @@ -1,6 +1,5 @@ package ru.javawebinar.topjava.web.user; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.validation.BindingResult; @@ -27,15 +26,10 @@ public String updateProfile(@Valid UserTo userTo, BindingResult result, SessionS if (result.hasErrors()) { return "profile"; } - try { - super.update(userTo, SecurityUtil.authUserId()); - SecurityUtil.get().setTo(userTo); - status.setComplete(); - return "redirect:/meals"; - } catch (DataIntegrityViolationException ex) { - result.rejectValue("email", EXCEPTION_DUPLICATE_EMAIL); - return "profile"; - } + super.update(userTo, SecurityUtil.authUserId()); + SecurityUtil.get().setTo(userTo); + status.setComplete(); + return "redirect:/meals"; } @GetMapping("/register") @@ -51,14 +45,8 @@ public String saveRegister(@Valid UserTo userTo, BindingResult result, SessionSt model.addAttribute("register", true); return "profile"; } - try { - super.create(userTo); - status.setComplete(); - return "redirect:/login?message=app.registered&username=" + userTo.getEmail(); - } catch (DataIntegrityViolationException ex) { - result.rejectValue("email", EXCEPTION_DUPLICATE_EMAIL); - model.addAttribute("register", true); - return "profile"; - } + super.create(userTo); + status.setComplete(); + return "redirect:/login?message=app.registered&username=" + userTo.getEmail(); } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/UniqueMailValidator.java b/src/main/java/ru/javawebinar/topjava/web/user/UniqueMailValidator.java new file mode 100644 index 000000000000..d72b8aa5c465 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/UniqueMailValidator.java @@ -0,0 +1,54 @@ +package ru.javawebinar.topjava.web.user; + + +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import ru.javawebinar.topjava.HasIdAndEmail; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; +import ru.javawebinar.topjava.web.ExceptionInfoHandler; +import ru.javawebinar.topjava.web.SecurityUtil; + +import javax.servlet.http.HttpServletRequest; + +@Component +public class UniqueMailValidator implements org.springframework.validation.Validator { + + private final UserRepository repository; + private final HttpServletRequest request; + + public UniqueMailValidator(UserRepository repository, @Nullable HttpServletRequest request) { + this.repository = repository; + this.request = request; + } + + @Override + public boolean supports(Class clazz) { + return HasIdAndEmail.class.isAssignableFrom(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + HasIdAndEmail user = ((HasIdAndEmail) target); + if (StringUtils.hasText(user.getEmail())) { + User dbUser = repository.getByEmail(user.getEmail().toLowerCase()); + if (dbUser != null) { + Assert.notNull(request, "HttpServletRequest missed"); + if (request.getMethod().equals("PUT") || (request.getMethod().equals("POST") && user.getId() != null)) { // update for REST(PUT) and UI(POST) + int dbId = dbUser.id(); + // it is ok, if update ourselves + if (user.getId() != null && dbId == user.id()) return; + + // workaround for update with user.id=null in request body + // ValidationUtil.assureIdConsistent (id setter) called after this validation + String requestURI = request.getRequestURI(); + if (requestURI.endsWith("/" + dbId) || (dbId == SecurityUtil.get().getId() && requestURI.contains("/profile"))) return; + } + errors.rejectValue("email", ExceptionInfoHandler.EXCEPTION_DUPLICATE_EMAIL); + } + } + } +} diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml index 971ae4df9337..62b8ff4b9bcb 100644 --- a/src/main/resources/spring/spring-mvc.xml +++ b/src/main/resources/spring/spring-mvc.xml @@ -70,7 +70,19 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp index 62f05f9a4ac3..73616fce1bf5 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp @@ -3,28 +3,49 @@ <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %> -