From d8c5d2869b08067c2069545a5b4555a4e570c06d Mon Sep 17 00:00:00 2001 From: marchstein Date: Tue, 4 Jun 2024 15:58:39 +0600 Subject: [PATCH 01/30] prepare HW0 --- .../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 f3f0ea6dc78a224a0613fa18852217960cccc59e Mon Sep 17 00:00:00 2001 From: marchstein Date: Mon, 10 Jun 2024 20:02:04 +0600 Subject: [PATCH 02/30] 1 rename --- pom.xml | 4 +- .../java/ru/javawebinar/topjava/Main.java | 2 +- .../model/{UserMeal.java => Meal.java} | 4 +- .../{UserMealWithExcess.java => MealTo.java} | 6 +-- .../javawebinar/topjava/util/MealsUtil.java | 39 +++++++++++++++++++ .../topjava/util/UserMealsUtil.java | 39 ------------------- 6 files changed, 47 insertions(+), 47 deletions(-) rename src/main/java/ru/javawebinar/topjava/model/{UserMeal.java => Meal.java} (83%) 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 diff --git a/pom.xml b/pom.xml index 0b1c2896da5b..e05612eb5f8e 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ 1.0-SNAPSHOT Calories Management - http://topjava.herokuapp.com/ + https://javaops-demo.ru/topjava 1.8 @@ -24,7 +24,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.13.0 ${java.version} ${java.version} diff --git a/src/main/java/ru/javawebinar/topjava/Main.java b/src/main/java/ru/javawebinar/topjava/Main.java index c2f9cc618f7c..723742bacade 100644 --- a/src/main/java/ru/javawebinar/topjava/Main.java +++ b/src/main/java/ru/javawebinar/topjava/Main.java @@ -1,7 +1,7 @@ package ru.javawebinar.topjava; /** - * @see Demo application + * @see Demo application * @see Initial project */ public class Main { diff --git a/src/main/java/ru/javawebinar/topjava/model/UserMeal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java similarity index 83% rename from src/main/java/ru/javawebinar/topjava/model/UserMeal.java rename to src/main/java/ru/javawebinar/topjava/model/Meal.java index d8f91b127f6a..f546cef0f74a 100644 --- a/src/main/java/ru/javawebinar/topjava/model/UserMeal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -2,14 +2,14 @@ import java.time.LocalDateTime; -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; 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..bb5ddbf5ccc5 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -0,0 +1,39 @@ +package ru.javawebinar.topjava.util; + +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.model.MealTo; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.util.Arrays; +import java.util.List; + +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 = 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/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; - } -} From 303206d5dd898b8bcd4c57d5da587a605156db9a Mon Sep 17 00:00:00 2001 From: marchstein Date: Mon, 10 Jun 2024 20:04:14 +0600 Subject: [PATCH 03/30] 1 streams --- .../ru/javawebinar/topjava/model/Meal.java | 10 +++++++ .../javawebinar/topjava/util/MealsUtil.java | 26 ++++++++++++------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index f546cef0f74a..943ff5cd59fa 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -1,6 +1,8 @@ package ru.javawebinar.topjava.model; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; public class Meal { private final LocalDateTime dateTime; @@ -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/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index bb5ddbf5ccc5..c29e1fbbb077 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -3,11 +3,14 @@ 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) { @@ -21,19 +24,24 @@ public static void main(String[] args) { new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 20, 0), "Ужин", 410) ); - List mealsTo = filteredByCycles(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000); + List mealsTo = filteredByStreams(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) { + 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()); } - public static List filteredByStreams(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { - // TODO Implement by streams - return null; + private static MealTo createTo(Meal meal, boolean excess) { + return new MealTo(meal.getDateTime(), meal.getDescription(), meal.getCalories(), excess); } } From 5a912aa1de8ec4ef425d412119e317865e4cefe1 Mon Sep 17 00:00:00 2001 From: marchstein Date: Tue, 11 Jun 2024 17:06:34 +0600 Subject: [PATCH 04/30] 1_2 war --- pom.xml | 9 +++++++-- src/main/webapp/WEB-INF/web.xml | 8 ++++++++ src/main/webapp/index.html | 9 +++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 src/main/webapp/WEB-INF/web.xml create mode 100644 src/main/webapp/index.html diff --git a/pom.xml b/pom.xml index e05612eb5f8e..4e279395c3c8 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ ru.javawebinar topjava - jar + war 1.0-SNAPSHOT @@ -19,8 +19,13 @@ topjava - install + package + + org.apache.maven.plugins + maven-war-plugin + 3.4.0 + org.apache.maven.plugins maven-compiler-plugin diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000000..abe697960203 --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,8 @@ + + + TopJava + diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html new file mode 100644 index 000000000000..615f6db51dc8 --- /dev/null +++ b/src/main/webapp/index.html @@ -0,0 +1,9 @@ + + + + Java Enterprise (Topjava) + + +

Проект Java Enterprise (Topjava)

+ + From 7ea26673d3172ea5922406ed47a475b93d92821b Mon Sep 17 00:00:00 2001 From: marchstein Date: Tue, 11 Jun 2024 18:58:43 +0600 Subject: [PATCH 05/30] 1_3 servlet api --- pom.xml | 7 +++++++ .../ru/javawebinar/topjava/web/UserServlet.java | 16 ++++++++++++++++ src/main/webapp/WEB-INF/web.xml | 10 ++++++++++ src/main/webapp/index.html | 4 ++++ src/main/webapp/users.jsp | 11 +++++++++++ 5 files changed, 48 insertions(+) create mode 100644 src/main/java/ru/javawebinar/topjava/web/UserServlet.java create mode 100644 src/main/webapp/users.jsp diff --git a/pom.xml b/pom.xml index 4e279395c3c8..1b48f0d0eaaa 100644 --- a/pom.xml +++ b/pom.xml @@ -39,6 +39,13 @@
+ + + javax.servlet + javax.servlet-api + 4.0.1 + provided + 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..dcbb1c294f06 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java @@ -0,0 +1,16 @@ +package ru.javawebinar.topjava.web; + +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 UserServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + 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 abe697960203..e576c30a3642 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -5,4 +5,14 @@ version="4.0"> TopJava + + + userServlet + ru.javawebinar.topjava.web.UserServlet + 0 + + + userServlet + /users + diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index 615f6db51dc8..921d3b47281d 100644 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -5,5 +5,9 @@

Проект 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 ce0fd095e3121825cb65ae1d937bcfdaa2e7c58d Mon Sep 17 00:00:00 2001 From: marchstein Date: Thu, 13 Jun 2024 15:27:44 +0600 Subject: [PATCH 06/30] 1_4 forward to redirect --- src/main/java/ru/javawebinar/topjava/web/UserServlet.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java index dcbb1c294f06..eac924a3841a 100644 --- a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java @@ -11,6 +11,7 @@ public class UserServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - request.getRequestDispatcher("/users.jsp").forward(request, response); +// request.getRequestDispatcher("/users.jsp").forward(request, response); + response.sendRedirect("users.jsp"); } } From 6eede8534670aa6d8449b3afbba936969cafce61 Mon Sep 17 00:00:00 2001 From: marchstein Date: Thu, 13 Jun 2024 15:52:47 +0600 Subject: [PATCH 07/30] 1_5 logger --- pom.xml | 8 ++++++++ src/main/java/ru/javawebinar/topjava/web/UserServlet.java | 8 +++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1b48f0d0eaaa..bb355fb05669 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,8 @@ 1.8 UTF-8 UTF-8 + + 1.2.13
@@ -39,6 +41,12 @@ + + ch.qos.logback + logback-classic + ${logback.version} + + javax.servlet diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java index eac924a3841a..ef52d67576c0 100644 --- a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java @@ -1,16 +1,22 @@ 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"); } From 548eec1199723fc08e6cf408d0703402dcb9a6ae Mon Sep 17 00:00:00 2001 From: marchstein Date: Thu, 13 Jun 2024 16:00:08 +0600 Subject: [PATCH 08/30] 1_6 log config --- pom.xml | 9 +++++++++ src/main/resources/logback.xml | 29 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 src/main/resources/logback.xml diff --git a/pom.xml b/pom.xml index bb355fb05669..5cc0fbec7b70 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,7 @@ UTF-8 1.2.13 + 1.7.36 @@ -41,10 +42,18 @@ + + + org.slf4j + slf4j-api + ${slf4j.version} + + ch.qos.logback logback-classic ${logback.version} + runtime 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 From e28af931f87c3361aaeb9f167936abc362edc590 Mon Sep 17 00:00:00 2001 From: marchstein Date: Mon, 8 Jul 2024 21:47:18 +0600 Subject: [PATCH 09/30] 2_0 fix time util --- src/main/java/ru/javawebinar/topjava/util/TimeUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java index 0ebfdb5fcdcb..550b590c97e2 100644 --- a/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java @@ -4,6 +4,6 @@ public class TimeUtil { public static boolean isBetweenHalfOpen(LocalTime lt, LocalTime startTime, LocalTime endTime) { - return lt.compareTo(startTime) >= 0 && lt.compareTo(endTime) < 0; + return !lt.isBefore(startTime) && lt.isBefore(endTime); } } From 5eaa8f0f5db216a79d1885977e6d4c245478ff2f Mon Sep 17 00:00:00 2001 From: marchstein Date: Mon, 8 Jul 2024 22:24:54 +0600 Subject: [PATCH 10/30] HW1 patch1 --- 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 +++++++++++++++++++ 12 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/pom.xml b/pom.xml index 5cc0fbec7b70..0bc434df00ee 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 723742bacade..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 550b590c97e2..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.isBefore(startTime) && lt.isBefore(endTime); - } -} 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 64184dc1c483213a05e8a0d21b5c78442a49808b Mon Sep 17 00:00:00 2001 From: marchstein Date: Tue, 9 Jul 2024 17:58:05 +0600 Subject: [PATCH 11/30] 2_2 HW1 optional --- .../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 b25c7044e3242562751980c7910bd20064c005c5 Mon Sep 17 00:00:00 2001 From: marchstein Date: Mon, 15 Jul 2024 16:54:39 +0600 Subject: [PATCH 12/30] 2_3_app_layers --- .../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..abafde5db5e1 --- /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; + + private Date registered = new Date(); + + private Set roles; + + private int caloriesPerDay; + + 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 c79ec23a2a200446cd60c437035817d3b99544f0 Mon Sep 17 00:00:00 2001 From: marchstein Date: Mon, 15 Jul 2024 21:33:29 +0600 Subject: [PATCH 13/30] 2_4_add_spring --- pom.xml | 9 +++++++++ .../ru/javawebinar/topjava/model/User.java | 18 +++++++++++------- .../topjava/service/MealService.java | 2 +- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 0bc434df00ee..9ac2e04debe3 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,8 @@ 1.8 UTF-8 UTF-8 + 5.3.37 + 1.2.13 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 abafde5db5e1..0ec0257d923d 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; - 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,6 +73,10 @@ 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; } diff --git a/src/main/java/ru/javawebinar/topjava/service/MealService.java b/src/main/java/ru/javawebinar/topjava/service/MealService.java index 0dc4a43c394f..3447b462d957 100644 --- a/src/main/java/ru/javawebinar/topjava/service/MealService.java +++ b/src/main/java/ru/javawebinar/topjava/service/MealService.java @@ -2,7 +2,7 @@ import ru.javawebinar.topjava.repository.MealRepository; -public class MealService { +public class MealService { private MealRepository repository; From dfab54fad03daa10010311704686c592e242f40b Mon Sep 17 00:00:00 2001 From: marchstein Date: Wed, 17 Jul 2024 18:43:53 +0600 Subject: [PATCH 14/30] 2_5_add_spring_context --- .../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 763137a38707df77e9dc4ca852cb3f07aec2a9d6 Mon Sep 17 00:00:00 2001 From: marchstein Date: Wed, 17 Jul 2024 18:56:29 +0600 Subject: [PATCH 15/30] 2_6_dependency_injection --- 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 44b49f4d47e46b49d04892f5ba4c8d076ba824f0 Mon Sep 17 00:00:00 2001 From: marchstein Date: Wed, 17 Jul 2024 18:57:27 +0600 Subject: [PATCH 16/30] 2_7_annotation_processing --- .../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 66fc21eecb5ceb62475692f13692760b7947f5e7 Mon Sep 17 00:00:00 2001 From: marchstein Date: Thu, 25 Jul 2024 15:31:43 +0600 Subject: [PATCH 17/30] 3_0_fix_TimeUtil --- src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java index 3f23f83fde65..e7b638eaeca7 100644 --- a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -8,7 +8,7 @@ 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; + return !lt.isBefore(startTime) && lt.isBefore(endTime); } public static String toString(LocalDateTime ldt) { From a943be2cabbe2ed5a70be8090e7b138a6fc83de8 Mon Sep 17 00:00:00 2001 From: marchstein Date: Fri, 26 Jul 2024 00:26:38 +0600 Subject: [PATCH 18/30] 3_01_HW2_repositories --- .../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 50654923a595e885fbe5a312a1be2dc82cc159aa Mon Sep 17 00:00:00 2001 From: marchstein Date: Fri, 26 Jul 2024 00:39:36 +0600 Subject: [PATCH 19/30] 3_02_HW2_repo_filters --- .../topjava/repository/MealRepository.java | 4 ++++ .../repository/inmemory/InMemoryMealRepository.java | 12 ++++++++++++ .../ru/javawebinar/topjava/util/DateTimeUtil.java | 5 ----- .../java/ru/javawebinar/topjava/util/MealsUtil.java | 2 +- src/main/java/ru/javawebinar/topjava/util/Util.java | 9 +++++++++ 5 files changed, 26 insertions(+), 6 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 e7b638eaeca7..e68cc74f1b33 100644 --- a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -1,16 +1,11 @@ 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.isBefore(startTime) && lt.isBefore(endTime); - } - 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 77ab8a50b4c7a21c2c7e95598ea657e9da28630c Mon Sep 17 00:00:00 2001 From: marchstein Date: Fri, 26 Jul 2024 01:27:02 +0600 Subject: [PATCH 20/30] 3_03_HW2_meal_layers --- .../ru/javawebinar/topjava/SpringMain.java | 16 +++++ .../ru/javawebinar/topjava/model/Meal.java | 18 +---- .../topjava/service/MealService.java | 42 ++++++++++- .../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, 143 insertions(+), 22 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 3447b462d957..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; -public class MealService { +import java.time.LocalDate; +import java.util.List; - private MealRepository repository; +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 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 e68cc74f1b33..3cdf046e0f4b 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 1734456bb3c45f5e9c75c3329440e934d7b6490a Mon Sep 17 00:00:00 2001 From: marchstein Date: Wed, 31 Jul 2024 18:37:20 +0600 Subject: [PATCH 21/30] 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 b602b60aa8da897d0943448c72f4eda65b265f58 Mon Sep 17 00:00:00 2001 From: marchstein Date: Mon, 12 Aug 2024 19:20:17 +0600 Subject: [PATCH 22/30] 3_05_HW2_optional_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 80479a0d6241b0fdd7aef06c1c3dfcdc47c95f63 Mon Sep 17 00:00:00 2001 From: marchstein Date: Mon, 12 Aug 2024 19:22:41 +0600 Subject: [PATCH 23/30] 3_06_HW2_optional_filter --- .../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 3cdf046e0f4b..0d3cfc389890 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,5 +26,15 @@ 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 13c2a1de11e284f443035d819825709a3bd9c656 Mon Sep 17 00:00:00 2001 From: marchstein Date: Mon, 12 Aug 2024 19:34:23 +0600 Subject: [PATCH 24/30] 3_07_HW2_optional_select_user --- .../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 f62ed46cbed91c493887192b586c3cc9a0c2624a Mon Sep 17 00:00:00 2001 From: marchstein Date: Thu, 12 Sep 2024 18:18:56 +0600 Subject: [PATCH 25/30] 3_08_bean_life_cycle.patch --- pom.xml | 6 ++++++ .../inmemory/InMemoryMealRepository.java | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/pom.xml b/pom.xml index 9ac2e04debe3..1d7c9762dfae 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 f0b08623afd6ef6287a8832e67f3971b8f8522de Mon Sep 17 00:00:00 2001 From: marchstein Date: Thu, 12 Sep 2024 18:22:11 +0600 Subject: [PATCH 26/30] 3_09_add_junit.patch --- 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 1d7c9762dfae..af2afd9006f2 100644 --- a/pom.xml +++ b/pom.xml @@ -20,6 +20,9 @@ 1.2.13 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..c5629388f679 --- /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 58dd39ae56d5346b041ca4c80fadf316ed0f33e8 Mon Sep 17 00:00:00 2001 From: marchstein Date: Fri, 13 Sep 2024 14:17:42 +0600 Subject: [PATCH 27/30] 3_10_add_spring_test --- 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 af2afd9006f2..514bc10b8fbb 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..8b95cd4e1486 --- /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 5548d36928f7b8197d4f99a14f9e4149c7d51b90 Mon Sep 17 00:00:00 2001 From: marchstein Date: Mon, 16 Sep 2024 19:06:19 +0600 Subject: [PATCH 28/30] 3_11_add_postgresql --- pom.xml | 9 +++++++++ src/main/resources/db/postgres.properties | 3 +++ 2 files changed, 12 insertions(+) create mode 100644 src/main/resources/db/postgres.properties diff --git a/pom.xml b/pom.xml index 514bc10b8fbb..026b440c6bbd 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,8 @@ 1.2.13 1.7.36 + + 42.7.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 cef506360fa7ec10188fd4529c576c18c5cbe86c Mon Sep 17 00:00:00 2001 From: marchstein Date: Fri, 20 Sep 2024 21:07:26 +0600 Subject: [PATCH 29/30] 3_12_db_implementation --- 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/spring/spring-db.xml | 25 ++++++ 5 files changed, 148 insertions(+) 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/pom.xml b/pom.xml index 026b440c6bbd..10ef5da628b1 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/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 bfe5d9bf3c062c89b2276a110bf99587c2aaacba Mon Sep 17 00:00:00 2001 From: marchstein Date: Tue, 8 Oct 2024 23:25:24 +0600 Subject: [PATCH 30/30] 3_12_delete video --- doc/lesson07.md | 566 ------------------------------------------ doc/video1.md | 124 --------- doc/video2.1.md | 154 ------------ doc/video2.2.md | 161 ------------ doc/video2.3.md | 115 --------- doc/video2.4.md | 24 -- doc/video3.md | 168 ------------- doc/video4.md | 110 -------- doc/video5-vcs-git.md | 180 -------------- 9 files changed, 1602 deletions(-) delete mode 100644 doc/lesson07.md delete mode 100644 doc/video1.md delete mode 100644 doc/video2.1.md delete mode 100644 doc/video2.2.md delete mode 100644 doc/video2.3.md delete mode 100644 doc/video2.4.md delete mode 100644 doc/video3.md delete mode 100644 doc/video4.md delete mode 100644 doc/video5-vcs-git.md diff --git a/doc/lesson07.md b/doc/lesson07.md deleted file mode 100644 index b958f9d2a74a..000000000000 --- a/doc/lesson07.md +++ /dev/null @@ -1,566 +0,0 @@ -# [Онлайн стажировка Spring 5/JPA Enterprise (TopJava)](http://javaops.ru/view/topjava) -## [Почему мы?](http://javaops.ru/#why) - -## REST, REST-контроллеры, тестирование контроллеров Spring MVC -# Для просмотра открыты видео [4](#--4-миграция-на-junit-5), [5](#-5-принципы-rest-rest-контроллеры), [6](#-6-тестирование-rest-контроллеров-jackson), [7](#-7-кастомизация-jackson-object-mapper), [8](#user-content--8-тестирование-rest-контроллеров-через-jsonassert-и-матчеры) -- Не стоит стремиться прочитать все ссылки урока, их можно использовать как справочник. Гораздо важнее пройти основной материал урока и сделать домашнее задание -- Обязательно посмотри правила работы с патчами на проекте -- Делать Apply Patch лучше по одному непосредственно перед видео на эту тему, а при просмотре видео сразу отслеживать все изменения кода проекта по изменению в патче (`Version Control -> Local Changes -> Ctrl+D`) -- При первом Apply удобнее выбрать имя локального ченджлиста Name: Default. Далее все остальные патчи также будут в него попадать. -- Код проекта обновляется и не всегда совпадает с видео (можно увидеть, как развивался проект). Изменения в проекте указываю после соответствующего патча. - -## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW6 - -### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. HW6 -#### Apply 7_01_HW6_fix_tests.patch - -#### Apply 7_02_HW6_meals.patch - -> сделал фильтрацию еды через `get`: операция идемпотентная, можно делать в браузере обновление по F5 - -### Внимание: чиним пути в следующем патче - -#### Apply 7_03_HW6_fix_relative_url_utf8.patch - -- - Relative paths in JSP -- - Spring redirect: prefix - -### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. HW6 Optional - -#### Apply 7_04_HW6_optional_add_role.patch - -#### `JdbcUserServiceTest` отвалились. Будем чинить в `7_06_HW6_jdbc_transaction_roles.patch` - -#### Apply 7_05_fix_hint_graph.patch - -- В `JpaUserRepositoryImpl.getByEmail` DISTINCT попадает в запрос, хотя он там не нужен. Это просто указание Hibernate - не дублировать данные. Для оптимизации можно указать Hibernate делать запрос без - distinct: [15.16.2. Using DISTINCT with entity queries](https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#hql-distinct) -- Бага [HINT_PASS_DISTINCT_THROUGH does not work if 'hibernate.use_sql_comments=true'](https://hibernate.atlassian.net/browse/HHH-13280). При `hibernate.use_sql_comments=false` все работает - в SELECT нет DISTINCT. -- Тест `DataJpaUserServiceTest.getWithMeals()` не работает для admin (у админа 2 роли, и еда при JOIN дублируется). ... - -#### Apply 7_06_HW6_jdbc_transaction_roles.patch - -Еще интересные JDBC реализации: ... - -### Валидация для `JdbcUserRepository` через Bean Validation API - -#### Apply 7_07_HW6_optional_jdbc_validation.patch - -- [Валидация данных при помощи Bean Validation API](https://alexkosarev.name/2018/07/30/bean-validation-api/). - -На данный момент у нас реализована валидация сущностей только для jpa- и dataJpa-репозиториев. При работе -через JDBC-репозиторий может произойти попытка записи в БД некорректных данных, что приведет к `SQLException` из-за нарушения -ограничений, наложенных на столбцы базы данных. Для того чтобы перехватить невалидные данные еще до -обращения в базу, воспользуемся API *javax.validation* (ее реализация `hibernate-validator` используется для проверки данных в Hibernate и будет использоваться в Spring Validation, которую подключим позже). -В `ValidationUtil` создадим один потокобезопасный валидатор, который можно переиспользовать (см. *javadoc*). -С его помощью в методах сохранения и обновления сущности в jdbc-репозиториях мы можем производить валидацию этой сущности: `ValidationUtil.validate(object);` -Чтобы проверка не падала, `@NotNull Meal.user` пришлось пока закомментировать. Починим в 10-м занятии через `@JsonView`. - -### Отключение кэша в тестах: - -Вместо наших приседаний с `JpaUtil` и проверкой профилей мы можем ... - -#### Apply 7_08_HW06_optional2_disable_tests_cache.patch - -- [Example of PropertyOverrideConfigurer](https://www.concretepage.com/spring/example_propertyoverrideconfigurer_spring) -- [Spring util schema](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#xsd-schemas-util) - -## Занятие 7: - -### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Тестирование Spring MVC - -
    - Краткое содержание - -#### Тестирование Spring MVC - -Для более удобного сравнения объектов в тестах мы будем использовать библиотеку *Harmcrest* с Matcher'ами, которая -позволяет делать сложные проверки. С *JUnit* по умолчанию подтягивается *Harmcrest core*, но нам потребуется расширенная версия: -в `pom.xml` из зависимости JUnit исключим дочернюю `hamcrest-core` и добавим `hamcrest-all`. - -Для тестирования web создадим вспомогательный класс `AbstractControllerTest`, от которого будут наследоваться все -тесты контроллеров. Его особенностью будет наличие `MockMvc` - эмуляции Spring MVC для тестирования web-компонентов. -Инициализируем ее в методе, отмеченном `@PostConstruct`: - - ``` -mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).addFilter(CHARACTER_ENCODING_FILTER).build(); - ``` - -Для того чтобы в тестах контроллеров не популировать базу перед каждым тестом, пометим этот базовый тестовый класс аннотацией `@Transactional`. -Теперь каждый тестовый метод будет выполняться в транзакции, которая будет откатываться после окончания метода и возвращать базу данных в исходное -состояние. Однако теперь в работе тестов могут возникнуть нюансы, связанные с пропагацией транзакций: все -транзакции репозиториев станут вложенными во внешнюю транзакцию теста. При этом, например, кэш первого уровня станет работать не -так, как ожидается. Т. е. при таком подходе нужно быть готовыми к ошибкам: мы их увидим и поборем в тестах на обработку ошибок на последних занятиях TopJava. - -#### UserControllerTest - -Создадим тестовый класс для контроллера юзеров, он должен наследоваться от `AbstractControllerTest`. -В `MockMvc` используется [паттерн проектирования Builder](https://refactoring.guru/ru/design-patterns/builder). - - ``` - mockMvc.perform(get("/users")) // выполнить HTTP метод GET к "/users" - .andDo(print()) // распечатать содержимое ответа - .andExpect(status().isOk()) // от контроллера ожидается ответ со статусом HTTP 200(ok) - .andExpect(view().name("users")) // контроллер должен вернуть view с именем "users" - .andExpect(forwardedUrl("/WEB-INF/jsp/users.jsp")) // ожидается, что клиент должен быть перенаправлен на "/WEB-INF/jsp/users.jsp" - .andExpect(model().attribute("users", hasSize(2))) // в модели должен быть атрибут "users" размером = 2 ... - .andExpect(model().attribute("users", hasItem( // ... внутри которого есть элемент ... - allOf( - hasProperty("id", is(START_SEQ)), // ... с аттрибутом id = START_SEQ - hasProperty("name", is(USER.getName())) //... и name = user - ) - ))); -} - ``` - -В параметры метода `andExpect()` передается реализация `ResultMatcher`, в которой мы определяем, как должен быть обработан ответ контроллера. - -
    - -#### Apply 7_09_controller_test.patch - -> - в `MockMvc` добавился `CharacterEncodingFilter` -> - добавил [`AllActiveProfileResolver`](//http://stackoverflow.com/questions/23871255/spring-profiles-simple-example-of-activeprofilesresolver) для возвращения массива профилей -> - сделал вспомогательный метод `AbstractControllerTest.perform()` - -- Hamcrest -- Unit Testing of Spring MVC Controllers - -### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. [Миграция на JUnit 5](https://drive.google.com/open?id=16wi0AJLelso-dPuDj6xaGL7yJPmiO71e) - -
    - Краткое содержание - -Для миграции на 5-ю версию JUnit в файле `pom.xml` поменяем зависимость `junit` на `junit-jupiter-engine` ([No need `junit-platform-surefire-provider` dependency in `maven-surefire-plugin`](https://junit.org/junit5/docs/current/user-guide/#running-tests-build-maven)). -Актуальную версию всегда можно посмотреть [в центральном maven-репозитории](https://search.maven.org/search?q=junit-jupiter-engine), берем только релизы (..-Mx означают предварительные milestone версии) -Изменять конфигурацию плагина `maven-sureface-plugin` в новых версиях JUnit уже не требуется. -JUnit5 не содержит в себе зависимости от *Harmcrest* (которую нам приходилось вручную -отключать для JUnit4 в предыдущих шагах), поэтому исключение `hamcrest-core` просто удаляем. -В итоге у нас останутся зависимости JUnit5 и расширенный Harmcrest. -Теперь мы можем применить все нововведения пятой версии в наших тестах: - 1. Для всех тестов теперь мы можем удалить `public`. - 2. Аннотацию `@Before` исправим на `@BeforeEach` - теперь метод, который будет выполняться перед -каждым тестом, помечается именно так. - 3. В JUnit5 работа с исключениями похожа на JUnit4 версии 4.13: вместо ожидаемых исключений в параметрах аннотации `@Test(expected = Exception.class)` используется метод `assertThrows()`, -в который первым аргументом мы передаем ожидаемое исключение, а вторым аргументом — реализацию функционального интерфейса `Executable` (код, -в котором ожидается возникновение исключения). - 4. Метод `assertThrows()` возвращает исключение, которое было выброшено в переданном ему коде. Теперь мы можем получить это исключение, извлечь из него сообщение с помощью - `e.getMessage()` и сравнить с ожидаемым. - 5. Для теста на валидацию при проверке предусловия, только при выполнении которого -будет выполняться следующий участок кода (например, в нашем случае тесты на валидацию выполнялись -только в jpa профиле), теперь нужно пользоваться утильным методом `Assumptions` (нам уже не требуется). - 6. Проверку Root Cause - причины, из-за которой было выброшено пойманное исключение, мы будем делать позднее, при тестах на ошибки. - 7. Из JUnit5 исключена функциональность `@Rule`, вместо них теперь нужно использовать `Extensions`, которые -могут встраиваться в любую фазу тестов. Чтобы добавить их в тесты, пометим базовый тестовый класс аннотацией `@ExtendWith`. - -JUnit предоставляет нам набор коллбэков — интерфейсов, которые будут исполняться в определенный момент тестирования. -Создадим класс `TimingExtension`, который будет засекать время выполнения тестовых методов. -Этот класс будет имплементировать маркерные интерфейсы — коллбэки JUnit: - - `BeforeTestExecutionCallback` - коллбэк, который будет вызывать методы этого интерфейса перед каждым тестовым методом. - - `AfterTestExecutionCallback` - методы этого интерфейса будут вызываться после каждого тестового метода; - - `BeforeAllCallback` - методы перед выполнением тестового класса; - - `AfterAllCallback` - методы после выполнения тестового класса; - -Осталось реализовать соответствующие методы, которые описываются в каждом из этих интерфейсов, они и будут вызываться JUnit в нужный момент: - - в методе `beforeAll` (который будет вызван перед запуском тестового класса) создадим спринговый утильный секундомер `StopWatch` для текущего тестового класса; - - в методе `beforeTestExecution` (будет вызван перед тестовым методом) - запустим секундомер; - - в методе `afterTestExecution` (будет вызван после тестового метода) - остановим секундомер. - - в методе `afterAll` (который будет вызван по окончанию работы тестового класса) - выведем результат работы этого секундомера в лог; - -8. Аннотации `@ContextConfiguration` и `@ExtendWith(SpringExtension.class)` (замена `@RunWith`) мы можем заменить одной `@SpringJUnitConfiguration` (старые версии IDEA ее не понимают) - -
    - -#### Apply 7_10_JUnit5.patch - -> - [No need `junit-platform-surefire-provider` dependency in `maven-surefire-plugin`](https://junit.org/junit5/docs/current/user-guide/#running-tests-build-maven) -> - [Наконец пофиксили баг с `@SpringJUnitConfig`](https://youtrack.jetbrains.com/issue/IDEA-166549) - -- [JUnit 5 homepage](https://junit.org/junit5) -- [Overview](https://junit.org/junit5/docs/snapshot/user-guide/#overview) -- [10 интересных нововведений](https://habr.com/post/337700) -- Дополнительно: - - [Extension Model](https://junit.org/junit5/docs/current/user-guide/#extensions) - - [A Guide to JUnit 5](http://www.baeldung.com/junit-5) - - [Migrating from JUnit 4](http://www.baeldung.com/junit-5-migration) - - [Before and After Test Execution Callbacks](https://junit.org/junit5/docs/snapshot/user-guide/#extensions-lifecycle-callbacks-before-after-execution) - - [Conditional Test Execution](https://junit.org/junit5/docs/snapshot/user-guide/#writing-tests-conditional-execution) - - [Third party Extensions](https://github.com/junit-team/junit5/wiki/Third-party-Extensions) - - [Реализация assertThat](https://stackoverflow.com/questions/43280250) - -### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. [Принципы REST. REST контроллеры](https://drive.google.com/open?id=1e4ySjV15ZbswqzL29UkRSdGb4lcxXFm1) - -
    - Краткое содержание - -#### Принципы REST, REST-контроллеры - -> [REST](http://spring-projects.ru/understanding/rest/) - архитектурный стиль проектирования распределенных систем (типа клиент-сервер). - -Чаще всего в REST-сервер и клиент общаются посредством обмена JSON-объектами через HTTP-методы GET/POST/PUT/DELETE/PATCH. -Особенностью REST является отсутствие состояния (контекста) взаимодействий клиента и сервера. - -В нашем приложении есть контроллеры для Admin и для User. Чтобы сделать их REST-контроллерами, -заменим аннотацию `@Controller` на `@RestController` - -> Не поленитесь зайти через Ctrl+click в `@RestController`: к аннотации `@Controller` добавлена `@ResponseBody`. Т. е. ответ от нашего приложения будет не View, а данные в теле ответа. - -В `@RequestMapping`, кроме пути для методов контроллера (`value`), добавляем параметр `produces = MediaType.APPLICATION_JSON_VALUE`. -Это означает, что в заголовки ответа будет добавлен тип `ContentType="application/json"` - в ответе от контроллера будет приходить JSON-объект. - -> Чтобы было удобно использовать путь к этому контроллеру в приложении и в тестах, -> выделим путь к нему в константу REST_URL, к которой можно будет обращаться из других классов - -1. Метод `AdminRestController.getAll` пометим аннотацией `@GetMapping` - маршрутизация к методу по HTTP GET. - -2. Метод `AdminRestController.get` пометим аннотацией `@GetMapping("/{id}")`. -В скобках аннотации указано, что к основному URL контроллера будет добавляться `id` пользователя - переменная, которая передается в запросе непосредственно в URL. - Соответствующий параметр метода нужно пометить аннотацией `@PathVariable` (если имя в URL и имя аргумента метода не совпадают, в параметрах аннотации дополнительно нужно будет уточнить - имя в URL. Если они совпадают, [этого не требуется](https://habr.com/ru/post/440214/). - -3. Метод создания пользователя `create` отметим аннотацией `@PostMapping` - маршрутизация к методу по HTTP POST. - В метод мы передаем объект `User` в теле запроса (аннотация `@RequestBody`) в формате JSON (`consumes = MediaType.APPLICATION_JSON_VALUE`). - При создании нового ресурса правило хорошего тона - вернуть в заголовке ответа URL созданного ресурса. - Для этого возвращаем не `User`, а `ResponseEntity`, который мы можем с помощью билдера `ServletUriComponentsBuilder` дополнить заголовком ответа `Location` и вернуть статус `CREATED(201)` - (если пойти в код `ResponseEntity.created` можно докопаться до сути, очень рекомендую смотреть в исходники кода). - -4. Метод `delete` помечаем `@DeleteMapping("/{id}")` - HTTP DELETE. - Он ничего не возвращает, поэтому помечаем его аннотацией `@ResponseStatus(HttpStatus.NO_CONTENT)`. Статус ответа будет HTTP.204; - -5. Над методом обновления ставим `@PutMapping` (HTTP PUT). В аргументах метод принимает `@RequestBody User user` и `@PathVariable int id`. - -6. Метод поиска по `email` также помечаем `@GetMapping` и, чтобы не было конфликта маршрутизации с методом `get()`, - указываем в URL добавку `/by`. В этот метод `email` передается как параметр запроса (аннотация `@RequestParam`). - -> **Все это СТАНДАРТ архитектурного стиля REST. НЕ придумывайте ничего своего в своих выпускных проектах! Это очень большая ошибка - не придерживаться стандартов API.** - -7. `ProfileRestController` выполняем аналогичным способом с учетом того, что пользователь имеет доступ только к своим данным. - -Если на данном этапе попытаться запустить приложение и обратиться к какому-либо методу контроллера, сервер ответит нам ошибкой со статусом 406, -так как Spring не знает, как преобразовать объект User в JSON... - -
    - -#### Apply 7_11_rest_controller.patch - -- Понимание REST -- JSON (JavaScript Object Notation) -- [15 тривиальных фактов о правильной работе с протоколом HTTP](https://habrahabr.ru/company/yandex/blog/265569/) -- [10 Best Practices for Better RESTful](https://medium.com/@mwaysolutions/10-best-practices-for-better-restful-api-cbe81b06f291) -- [Best practices for rest nested resources](https://stackoverflow.com/questions/20951419/what-are-best-practices-for-rest-nested-resources) -- - Request mapping -- [Лучшие практики разработки REST API: правила 1-7,15-17](https://tproger.ru/translations/luchshie-praktiki-razrabotki-rest-api-20-sovetov/) -- Дополнительно: - - [Подборка практик REST](https://gist.github.com/Londeren/838c8a223b92aa4017d3734d663a0ba3) - - JAX-RS vs Spring MVC - - RESTful API для сервера – делаем правильно (Часть 1) - - RESTful API для сервера – делаем правильно (Часть 2) - - И. Головач. - RestAPI - - [value/name в аннотациях @PathVariable и @RequestParam](https://habr.com/ru/post/440214/) - -### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. [Тестирование REST контроллеров. Jackson.](https://drive.google.com/open?id=1aZm2qoMh4yL_-i3HhRoyZFjRAQx-15lO) - -
    - Краткое содержание - -Для работы с JSON добавляем в `pom.xml` зависимость `jackson-databind`. -Актуальную версию библиотеки можно посмотреть в [центральном maven-репозитории](https://search.maven.org/artifact/com.fasterxml.jackson.core/jackson-databind). -Теперь Spring будет автоматически использовать эту библиотеку для сериализации/десериализации объектов в JSON (найдя ее в *classpath*). -Если сейчас запустить приложение и обратиться к методам REST-контроллера, то оно выбросит `LazyInitializationException`. -Оно возникает из-за того, что у наших сущностей есть лениво загружаемые поля, отмеченные `FetchType.LAZY` - при загрузке сущности из базы вместо такого поля подставится Proxy, который и должен вернуть -реальный экземпляр этого поля при первом же обращении. Jackson при сериализации в JSON использует все поля сущности, -и при обращении к *Lazy*-полям возникает исключение, так как сессия работы с БД в этот момент уже закрыта, и нужный объект -не может быть инициализирован. Чтобы Jackson игнорировал эти поля, пометим их аннотацией `@JsonIgnore`. - -Теперь при запуске приложения REST-контроллер будет работать. Но при получении JSON-объектов мы можем увидеть, что Jackson сериализовал объект -через геттеры (например, в ответе есть поле `new` от метода `Persistable.isNew()`). -Чтобы учитывались только поля объектов, добавим над `AbstractBaseEntity`: -````java -@JsonAutoDetect(fieldVisibility = ANY, // jackson видит все поля - getterVisibility = NONE, // ... но не видит геттеров - isGetterVisibility = NONE, //... не видит геттеров boolean-полей - setterVisibility = NONE) // ... не видит сеттеров -```` -Теперь все сущности, унаследованные от базового класса, будут сериализоваться/десериализоваться через поля. - -
    - -#### Apply 7_12_rest_test_jackson.patch - -- [Jackson databind github](https://github.com/FasterXML/jackson-databind) -- [Jackson Annotation Examples](https://www.baeldung.com/jackson-annotations) - -### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. [Кастомизация Jackson Object Mapper](https://drive.google.com/open?id=1CM6y1JhKG_yeLQE_iCDONnI7Agi4pBks) - -
    - Краткое содержание - -Сейчас, чтобы не сериализовать *Lazy*-поля, мы должны пройтись по каждой сущности и -вручную пометить их аннотацией `@JsonIgnore`. Это неудобно, засоряет код и допускает возможные ошибки. К тому же, -при некоторых условиях, нам иногда нужно загрузить и в ответе передать эти *Lazy*-поля. -Чтобы запретить сериализацию Lazy-полей для всего проекта, подключим в `pom.xml` библиотеку `jackson-datatype-hibernate`. -Также изменим сериализацию/десериализацию полей объектов в JSON: не через аннотацию `@JsonAutoDetect`, а в классе `JacksonObjectMapper`, который -унаследуем от `ObjectMapper` (стандартный Mapper, который использует Jackson) и сделаем в нем другие настройки. -В конструкторе: -- регистрируем `Hibernate5Module` - модуль `jackson-datatype-hibernate`, который не делает сериализацию ленивых полей. -- модуль для корректной сериализации `LocalDateTime` в поля JSON - `JavaTimeModule` модуль библиотеки `jackson-datatype-jsr310` -- запрещаем доступ ко всем полям и методам класса и потом разрешаем доступ только к полям -- не сериализуем null-поля (`setSerializationInclusion(JsonInclude.Include.NON_NULL)`) - -Чтобы подключить наш кастомный `JacksonObjectMapper` в проект, в конфигурации `spring-mvc.xml` к -настройке `` добавим `MappingJackson2HttpMessageConverter`, который будет использовать наш маппер. - -
    - - -#### Apply 7_13_jackson_object_mapper.patch - -- Сериализация hibernate lazy-loading с помощью - jackson-datatype-hibernate -- Handle Java 8 dates with Jackson -- Дополнительно: - - Jackson JSON - Serializer & Deserializer - -### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 8. [Тестирование REST-контроллеров через JSONassert и Матчеры](https://drive.google.com/open?id=1oa3e0_tG57E71g6PW7_tfb3B61Qldctl) - -
    - Краткое содержание - -Сейчас в тестах REST-контроллера мы проводим проверку только на статус ответа и тип возвращаемого контента. Добавим проверку содержимого ответа. - -#### 7_14_json_assert_tests - -Чтобы сравнивать содержимое ответа контроллера в виде JSON и сущность, воспользуемся библиотекой -`jsonassert`, которую подключим в `pom.xml` со scope *test*. - -Эта библиотека при сравнении в тестах в качестве ожидаемого значения ожидает от -нас объект в виде JSON-строки. Чтобы вручную не преобразовывать объекты в JSON и не -хардкодить их в виде строк в наши тесты, воспользуемся Jackson. -Для преобразования объектов в JSON и обратно создадим утильный класс `JsonUtil`, в котором -с помощью нашего `JacksonObjectMapper` и будет конвертировать объекты. -И мы сталкиваемся с проблемой: `JsonUtil` - утильный класс и не является -бином Spring, а для его работы требуется наш кастомный маппер, который находится под управлением -Spring и расположен в контейнере зависимостей. Поэтому, чтобы была возможность получить -наш маппер из других классов, сделаем его синглтоном и сделаем в нем статический -метод, который будет возвращать его экземпляр. Теперь `JsonUtil` сможет его получить. -И нам нужно указать Spring, чтобы он не создавал второй экземпляр этого объекта, а клал в свой контекст существующий. -Для этого в конфигурации `spring-mvc.xml` определим factory-метод, с помощью которого Spring должен -получить экземпляр (instance) этого класса: -```xml - -``` -а в конфигурации `message-converter` вместо создания бина просто сошлемся на сконфигурированный `objectMapper`. - -Метод `ContentResultMatchers.json()` из `spring-test` использует библиотеку `jsonassert` для сравнения 2-х JSON строк: одну из ответа контроллера и вторую - -JSON-сериализация `admin` без поля `registered` (это поле инициализируется в момент создания и отличается). -В методе `JsonUtil.writeIgnoreProps` мы преобразуем объект `admin` в мапу, удаляем из нее игнорируемые поля и снова сериализуем в JSON. - -Также сделаем тесты для утильного класса `JsonUtil`. В тестах мы записываем -объект в JSON-строку, затем конвертируем эту строку обратно в объект и сравниваем с исходным. И то же самое делаем со списком объектов. - -#### 7_15_tests_refactoring - -**`RootControllerTest`** - -Сделаем рефакторинг `RootControllerTest`. Ранее мы в тесте получали модель, доставали из нее сущности и с помощью `hamcrest-all` -производили по одному параметру их сравнение с ожидаемыми значениями. -Метод `ResultActions.andExpect()` позволяет передавать реализацию интерфейса `Matcher`, в котором можно делать любые сравнения. -Функциональность сравнения списка юзеров по ВСЕМ полям у нас уже есть - мы просто делегируем сравнение объектов в `UserTestData.MATCHER`. -При этом нам больше не нужен `harmcrest-all`, нам достаточно только `harmcrest-core`. - -**`MatcherFactory`** - -Теперь вместо `jsonassert` и сравнения JSON-строк в тестах контроллеров сделаем сравнения JSON-объектов через `MatcherFactory`. -Преобразуем ответ контроллера из JSON в объект и сравним с эталоном через уже имеющийся у нас матчер. -Вместо сравнения JSON-строк в метод `andExpect()` мы будем передавать реализации интерфейса `ResultMatcher` из `MATCHER.contentJson(..)`. - -`MATCHER.contentJson(..)` принимают ожидаемый объект и возвращают для него `ResultMatcher` с реализацией единственного метода `match(MvcResult result)`, -в котором делегируем сравнение уже существующим у нас матчерам. -Мы берем JSON-тело ответа (`MatcherFactory.getContent`), десериализуем его в объект (`JsonUtil.readValue/readValues`) и сравниваем через имеющийся `MATCHER.assertMatch` -десериализованный из тела контроллера объект и ожидаемое значение. - -> Методы из класса `TestUtil` перенес в `MatcherFactory`, лишние удалил. - -**`AdminRestControllerTest`** - -- `getByEmail()` - сделан по аналогии с тестом `get()`. Дополнительно нужно добавить в строку URL параметры запроса. -- `delete()` - выполняем HTTP.DELETE. Проверяем статус ответа 204. Проверяем, что пользователь удален. - -> Раньше я получал всех users из базы и проверял, что среди них нет удаленного. При этом тесты становятся чувствительными ко всем users в базе и ломаются при добавлении/удалении новых тестовых данных. - -- `update()` - выполняем HTTP.PUT. В тело запроса подаем сериализованный `JsonUtil.writeValue(updated)`. После выполнения проверяем, что объект в базе обновился. -- `create()` - выполняем HTTP.POST аналогично `update()`. Но сравнить результат мы сразу не можем, т. к. при создании объекта ему присваивается `id`. - Поэтому мы извлекаем созданного пользователя из ответа (`MATCHER.readFromJson(action)`), получаем его `id`, и уже с этим `id` эталонный объект мы можем сравнить с объектом в ответе контроллера и со - значением в базе. -- `getAll()` - аналогично `get()`. Список пользователей из ответа в формате JSON сравниваем с эталонным списком (`MATCHER.contentJson(admin, user)`). - -Тесты для `ProfileRestController` выполнены аналогично. - -
    - -#### Apply 7_14_json_assert_tests.patch - -> - В `JsonUtil.writeIgnoreProps` вместо цикла по мапе сделал `map.keySet().removeAll` - -- [JSONassert](https://github.com/skyscreamer/JSONassert) -- [Java Code Examples for ObjectMapper](https://www.programcreek.com/java-api-examples/index.php?api=com.fasterxml.jackson.databind.ObjectMapper) - -#### Apply 7_15_tests_refactoring.patch - -> - Методы из класса `TestUtil` перенес в `MatcherFactory`, лишние удалил. -> - Раньше в тестах я для проверок получал всех users из базы и сравнивал с эталонным списком. При этом тесты становятся чувствительными ко всем users в базе и ломаются при добавлении/удалении новых тестовых данных. - -- [Java @SafeVarargs Annotation](https://www.baeldung.com/java-safevarargs) - -### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 9. Тестирование через SoapUI. UTF-8 - -
    - Краткое содержание - -SoapUI - это один из инструментов для тестирования API приложений, которые работают по REST и по SOAP. -Он позволяет нам по протоколу HTTP дернуть методы нашего API и увидеть ответ контроллеров. - -Если в контроллер мы добавим метод, который в теле ответа будет возвращать текст на кириллице, то увидим, что кодировка теряется. -Для сохранения кодировки используем `StringHttpMessageConverter`, который конфигурируем в `spring-mvc.xml`. -При этом мы должны явно указать, что конвертор будет работать только с текстом в кодировке *UTF-8*. - -
    - -#### Apply 7_16_soapui_utf8_converter.patch - -- Инструменты тестирования REST: - - SoapUI - - Написание HTTP-запросов с помощью - Curl. - Для Windows 7 можно использовать Git Bash, с Windows 10 v1803 можно прямо из консоли. Возможны проблемы с UTF-8: - - [CURL doesn't encode UTF-8](https://stackoverflow.com/a/41384903/548473) - - [Нстройка кодировки в Windows](https://support.socialkit.ru/ru/knowledge-bases/4/articles/11110-preduprezhdenie-obnaruzhenyi-problemyi-svyazannyie-s-raspoznavaniem-russkih-simvolov) - - **[IDEA: Tools->HTTP Client->...](https://www.jetbrains.com/help/idea/rest-client-tool-window.html)** - - Postman - - [Insomnia REST client](https://insomnia.rest/) - -**Импортировать проект в SoapUI из `config\Topjava-soapui-project.xml`. Response смотреть в формате JSON.** - -> Проверка UTF-8: http://localhost:8080/topjava/rest/profile/text - -[ResponseBody and UTF-8](http://web.archive.org/web/20190102203042/http://forum.spring.io/forum/spring-projects/web/74209-responsebody-and-utf-8) - -## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы - -> Зачем у нас и UI-контроллеры, и REST-контроллеры? То есть в общем случае backend-разработчику недостаточно предоставить REST API и RestController? - -В общем случае нужны и те, и другие. REST обычно используют для отдельного UI (например, на React или Angular) или для -интеграции / мобильного приложения. У нас REST-контроллеры используются только для тестирования. UI-контроллеры используем для -нашего приложения на JSP шаблонах. Таких сайтов без богатой UI логики тоже немало. Например https://javaops.ru/ :) -Разница в запросах: - -- для UI используются только GET и POST -- при создании/обновлении в UI мы принимаем данные из формы `application/x-www-form-urlencoded` (посмотрите - вкладку `Network`, не в формате JSON) -- для REST API запросы GET, POST, PUT, DELETE, PATCH и возвращают только данные (обычно JSON) - -...и в способе авторизации: - -- для REST API у нас будет базовая авторизация -- для UI - через cookies - -Также часто бывают смешанные сайты, где есть и отдельное JS приложение, и шаблоны. - -> При выполнении тестов через MockMvc никаких изменений на базе не видно, почему оно не сохраняет? - -`AbstractControllerTest` аннотируется `@Transactional` - это означает, что тесты идут в транзакции, и после каждого -теста JUnit делает rollback базы. - -> Что получается в результате выполнения запроса `SELECT DISTINCT(u) FROM User u LEFT JOIN FETCH u.roles ORDER BY u.name, u.email`? В чем разница в SQL без `DISTINCT`. - -Запросы SQL можно посмотреть в логах. Т. е. `DISTINCT` в `JPQL` влияет на то, как Hibernate обрабатывает дублирующиеся -записи (с `DISTINCT` их исключает). Результат можно посмотреть в тестах или приложении, поставив брекпойнт. По -поводу `SQL DISTINCT` не стесняйтесь пользоваться google, -например, [оператор SQL DISTINCT](http://2sql.ru/novosti/sql-distinct/) - -> В чем заключается расширение функциональности hamcrest в нашем тесте, что нам пришлось его отдельно от JUnit прописывать? - -hamcrest-all используется в проверках `RootControllerTest`: `org.hamcrest.Matchers.*` - -> Jackson мы просто подключаем в помнике, и Spring будет с ним работать без любых других настроек? - -Да, Spring смотрит в classpath и если видит там Jackson, то подключает интеграцию с ним. - -> Где-то слышал, что любой ресурс по REST должен однозначно идентифицироваться через url без параметров. Правильно ли задавать URL для фильтрации в виде `http://localhost/topjava/rest/meals/filter/{startDate}/{startTime}/{endDate}/{endTime}` ? - -Так делают только при -отношении -агрегация, например, если давать админу право смотреть еду любого юзера, URL мог бы быть похож -на `http://localhost/topjava/rest/users/{userId}/meals/{mealId}` (не рекомендуется, см. ссылку ниже). В случае критериев -поиска или страничных данных они передаются как параметр. Смотри также: - -- [15 тривиальных фактов о правильной работе с протоколом HTTP](https://habrahabr.ru/company/yandex/blog/265569/) -- 10 Best Practices - for Better RESTful -- [REST resource hierarchy (если кратко: не рекомендуется)](https://stackoverflow.com/questions/15259843/how-to-structure-rest-resource-hierarchy) - -> Что означает конструкция в `JsonUtil`: `reader.readValues(json)`; - -См. Generic Methods. Когда компилятор -не может вывести тип, можно его уточнить при вызове generic метода. Неважно, static или нет. - -## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW07 - -- 1: Добавить тесты контроллеров: - - 1.1 `RootControllerTest.getMeals` для `meals.jsp` - - 1.2 Сделать `ResourceControllerTest` для `style.css` (проверить `status` и `ContentType`) -- 2: Реализовать `MealRestController` и протестировать его через `MealRestControllerTest` - - 2.1 следите, чтобы url в тестах совпадал с параметрами в методе контроллера. Можно добавить - логирование `` для проверки маршрутизации. - - 2.2 в параметрах `getBetween` принимать `LocalDateTime` (конвертировать - через @DateTimeFormat with Java - 8 Date-Time API), пока без проверки на `null` (используя `toLocalDate()/toLocalTime()`, см. Optional п. 3). В - тестах передавать в формате `ISO_LOCAL_DATE_TIME` ( - например `'2011-12-03T10:15:30'`). - -### Optional - -- 3: Переделать `MealRestController.getBetween` на параметры `LocalDate/LocalTime` c раздельной фильтрацией по - времени/дате, работающий при `null` значениях (см. демо и `JspMealController.getBetween`) - . Заменить `@DateTimeFormat` на свои LocalDate/LocalTime конверторы или форматтеры. - - Spring Type - Conversion - - Spring Field - Formatting - - - Difference between Spring MVC formatters and converters -- 4: Протестировать `MealRestController` (SoapUI, Curl, IDEA Test RESTful Web Service, Postman). Запросы `curl` занести - в отдельный `md` файл (или `README.md`) -- 5: Добавить в `AdminRestController` и `ProfileRestController` методы получения пользователя вместе с - едой (`getWithMeals`, `/with-meals`). - - [Jackson – Bidirectional Relationships](https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion) - -### Optional 2 - -- 6: Сделать тесты на методы контроллеров `getWithMeals()` (п. 5) - -**На следующем занятии используется JavaScript/jQuery. Если у вас там -пробелы, пройдите его основы** ---------------------- - -## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Типичные ошибки и подсказки по реализации - -- 1: Ошибка в тесте _Invalid read array from JSON_ обычно расшифровывается немного ниже: читайте внимательно. -- 2: Jackson и неизменяемые объекты (для - сериализации `MealTo`) -- 3: Если у meal, приходящий в контроллер, поля `null`, проверьте `@RequestBody` перед параметром (данные приходят в - формате JSON) -- 4: При проблемах с собственным форматтером убедитесь, что в конфигурации `[JVM Ecosystem Report 2021](https://snyk.io/jvm-ecosystem-report-2021/) ->показывает, что Spring является абсолютным ->лидером среди фреймворков. - -На первом месте упоминается **Spring Boot**, а на втором - **Spring MVC**. - -Здесь хотелось бы внести некоторую ясность. -"Сердцем" Spring является Spring Core, который реализует Dependency -Injection. -Этот компонент, как правило, используется во всех остальных -проектах Spring Framework. - -Spring Boot - это надстройка над Spring Framework, -которая позволяет быстро создавать приложения разных типов, -использовать автоматическую конфигурацию для некоторых -компонентов и использовать некоторые удобные встроенные -инструменты, например, готовые инструменты для мониторинга -работы приложения (Spring Actuator). - -Если мы создаем web-приложение, то мы можем создавать его с -помощью Spring Boot, но Spring MVC, который является именно -web-фреймворком, также будет присутствовать в таком приложении -и играть ключевую роль. - -Spring Boot позволяет создавать очень простые приложения без -единой строчки конфигурации. Но как только мы пытаемся -создать что-то более сложное, нам все равно приходится -создавать конфигурационные файлы и классы и настраивать все вручную -точно так же, как в традиционном Spring-приложении. -По своему опыту могу сказать, что на реальных проектах -такая ситуация возникает в большинстве случаев. -Например, как только нужно настроить подключение -к нескольким базам данных, а не к одной, мы вынуждены -создавать конфигурационные классы вручную. - -В этом случае Spring Boot в какой-то степени может даже -навредить вашей продуктивности, потому что вам придется -разбираться, какие автоконфигурационные классы отключить, и т. д. - -Мы изучаем Spring довольно глубоко. -Традиционно хорошо показал себя подход, при котором -вначале изучается чистый Spring Framework, после чего -мы переходим к использованию Spring Boot. Такой подход -сохраняется и в этой версии курса. Без этого Spring Boot -будет для вас "черным ящиком", который вам будет очень -сложно понять, настраивать и отлаживать. - -### ORM frameworks -Отчеты, которые мы использовали, обычно не включают информацию -о самых популярных ORM-фреймворках. - ->Известно, ->что **Hibernate** является достаточно популярным фреймворком ->и наиболее часто упоминается в вакансиях, если сравнивать ->его с другими ORM-фреймворками и инструментами для работы Java-приложения ->с базами данных. - -Вот одна из статей, которая также говорит о -популярности Hibernate: [Top 5 Java ORM tools - 2022](https://www.knowledgefactory.net/2021/09/top-java-orm-tools-20XX.html) - -**ORM** или **Object-relational-mapping** можно перевести как -"объектно-реляционное отображение (преобразование)". -Это техника, которая позволяет создать виртуальную базу -данных с помощью объектно ориентированного языка -программирования и взаимодействовать с ней, в то время -как взаимодействие уже с реальной базой данных выполняется -фреймворком незаметно для нас. -Это облегчает работу, позволяет работать с привычными -и удобными для использования Java-объектами, вызывая -их методы, вместо того, чтобы писать SQL-запросы к базе данных и код для интерпретации полученных данных. - ->Hibernate является реализацией спецификации JPA, ->так же, как и менее популярный EclipseLink. - -Говоря простым языком, JPA (Java Persistence API) - это -набор интерфейсов без реализации для работы с ORM, -который включен в стандарт Java EE. Отдельные провайдеры -могут предоставлять свои реализации этого интерфейса. - -Недостатками JPA и его реализации Hibernate являются удар по производительности -и недостаточная гибкость. Вопрос производительности частично -может быть решен с помощью различных техник оптимизации кода, -написанного с использованием JPA, однако для некоторых -приложений тот оверхед, который несет в себе JPA, является -недопустимым, и вы можете увидеть на некоторых проектах -использование других подходов, от использования -чистого JDBC до использования MyBatis, JOOQ или каких-то -альтернативных решений. - -В реальных приложениях те запросы к базе данных, -которые создают наибольшую нагрузку на систему, -могут быть оптимизированы для улучшения производительности: для -них могут быть применены JDBC, альтернативные -JPA-фреймворки, также запросы могут быть написаны -с помощью нативных SQL-запросов в JPA. -Для остальных запросов, которые не приводят к -большой нагрузке на систему, в том же приложении может -быть применен Hibernate. -Таким образом, в одном приложении могут быть применены одновременно разные технологии. - - -На курсе вы изучите, как работать с БД с помощью нескольких -технологий: JDBC (а точнее, Spring JDBC template), -а также JPA и Spring DataJPA. -Вы сами не только увидите, но и почувствуете -при написании кода в ваших домашних заданиях плюсы и минусы каждого подхода. - -### Обзор пройденных тем -Давайте подведем промежуточные итоги по результатам этого урока. - -В этом уроке мы: - -* познакомились с тем, что такое фреймворки -и для чего они нужны; -* посмотрели статистику, которая определяет -Spring Framework как самый популярный фреймворк на рынке; -* познакомились с ORM - подходом для упрощения работы -с базами данных с использованием объектно-ориентированного подхода; -* узнали про JPA (Java Persistence API) - протокол для работы с -ORM - и про реализации JPA, среди которых Hibernate является самой популярной; -* обсудили недостатки подхода ORM и познакомились с инструментами, -которые могут быть использованы в качестве альтернативы. - -В следующем коротком уроке мы завершим обзор используемых технологий. -Увидимся в следующем видео. diff --git a/doc/video2.3.md b/doc/video2.3.md deleted file mode 100644 index af9506cb4596..000000000000 --- a/doc/video2.3.md +++ /dev/null @@ -1,115 +0,0 @@ -## Обзор наиболее востребованных технологий. Тренды - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) [Видео](https://drive.google.com/file/d/1XcTRkArj2guek9OiPuFEq_U1V4Dg0N-j) - -### Тренд на отказ от reflection - -Говоря о трендах, хотелось бы сказать, что в настоящее -время развиваются фреймворки, построенные на отказе -от использования Reflection API. -Dependency injection в Spring, сериализация с помощью -Jackson построены на использовании Reflection, что -сильно бьет по производительности, но было достаточно -удобным решение до недавнего времени. -Фреймворк Micronaut полностью построен на отказе от -Reflection, и это дает существенный прирост -производительности при измерении ряда параметров. -Micronaut использует продвинутый компилятор и -генерацию байткода, что позволяет создать все -бины (управляемые фреймворком объекты) в ходе компиляции. - -Пока доля Micronaut очень мала, но он используется -в продакшене. Я предполагаю, что в какой-то момент -Spring может также внедрить подобный подход. - -### Тренд на развитие реактивного программирования - -Продолжает развиваться реактивное программирование, -которое является в какой-то степени новой парадигмой -в программировании. - -Spring развивает свой реактивный фреймворк Spring WebFlux, -который поддерживает библиотеку Reactor. - - -Ряд современных задач не решается традиционными -методами, такими как блокирующий Input/Output, -HTTP-протокол. Одним из решений данных вопросов -является применение подходов реактивного -программирования (Reactor, RxJava), использование -новых протоколов передачи данных (RSocket). -У нас также готовится курс [ReactJava](https://javaops.ru/#inprogress) как -продолжение TopJava на реактивном стеке. - -### Тренд на микросервисы, тезисы - -ПО становиться большим и сложным. Очень большое приложение -сложно поддерживать одной командой, управлять его жизненным -циклом - разработкой, релизами. - -Если в большое монолитное приложение добавляется новая -функциональность приходиться выполнять повторного тестирование -всей большой системы, в результате срок выхода новых фич увеличивается. - -Микросервисная архитектура позволяет решить эту проблему - она -предполагает разделение большого приложения на отдельные -приложения-модули, которые взаимодействуют друг с другом. -Над одним модулем-сервисом может работать один человек или -небольшая команда. У такого модуля-сервиса будет отдельный -Git-репозиторий, он может быть задеплоен (развернут) независимо -от остальной системы. При таком подходе, команды могут вести работу -над различными сервисами параллельно, параллельно деплоить -их на серверах. При внесении изменений в микросервис “А”, -вероятность вызывать проблемы в микросервисе “B” снижается. -У разных микросервисов могут быть собственные базы данных. -Это также повышает надежность и гибкость. Например, если по каким то причинам после обновления банковского микросервиса, ответственного за выдачу кредитов возникли сбои и база данных оказалась недоступна, другие компоненты системы, имеющие отдельные базы данных, продолжат свою работу без сбоев. - -Это существенно упрощает доработку, тестирование и деплой таких систем. - -Использование микросервисов дает большую гибкость в выборе -технологий. Нам не нужно ограничивать себя в использовании -технологий для того, чтобы все стандартизировать внутри -одной большой системы. Работая с отдельными микросервисами, -мы можем выбрать идеально подходящие технологии для отдельных задач. -Например, один микросервис может использовать реляционные -базы данных, а второй эффективнее решает свою задачу -используя NoSQL базу данных или in memory базу данных. - -Еще одно важное преимущество, которое дает микросервисная -архитектура это возможность легко масштабировать систему горизонтально. - -Предположим, что банк испытывает быстрый рост обращений -об открытии новых счетов. - -Если банк использует монолитную архитектуру, -то есть одно большое приложение, ему может потребоваться -запускать отдельный экземпляр всего своего приложения -и направлять часть обращений от клиентов на этот экземпляр (instance). - -Если же банк использует микросервисную архитектуру, -он может запустить несколько экземпляров только сервиса, -ответственного за открытие новых счетов и распределять -нагрузку между этими экземплярами. -При этом микросервис, ответственный за выдачу кредитов, -который не испытывает повышенной нагрузки, может остаться -в одном экземпляре. При этом увеличение числа микросервисов, -ответственных за открытие счетов может происходить автоматически -с помощью инструментов DevOps и Kubernetes. - -Однако использование микросервисов также создает сложности. -Разработка систем, построенных на микросервисной архитектуре -на порядок сложнее. Обслуживание таких систем также -существенно сложнее. - -Компания должна обслуживать множество серверов, -обеспечивать мониторинг каждого микросервиса и так далее. -Также security таких систем существенно сложнее, -поскольку мы имеем дело с множеством приложений, -коммуницирующих друг с другом и все эти коммуникации -должны быть безопасными. - - -Следующий, готовый стать самым популярным -после TopJava курс - [Микросервисы](https://javaops.ru/view/cloudjava). - -[Первое занятие открытое](https://javaops.ru/view/cloudjava/lesson01) diff --git a/doc/video2.4.md b/doc/video2.4.md deleted file mode 100644 index 14041e795564..000000000000 --- a/doc/video2.4.md +++ /dev/null @@ -1,24 +0,0 @@ -## 2.4 Обзор наиболее востребованных технологий. Обзор разрабатываемого приложения - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) [Видео](https://drive.google.com/file/d/1LHI18LZK1MRIEBpVe3WjCE890EydN5Gz) - -### О работе с фронтендом и JavaScript -Курс включает себя минимальную практику работы с JavaScript. -Почему это нужно? -В вакансиях backend разработчиков, как правило, отсутствуют -требования глубокого знания JavaScript, но каждый разработчик -должен уметь пользовать DevTools - инструментами разработчика -в браузере, чтобы при работе над рабочими задачами по меньшей -мере иметь возможность понять, происходит ли ошибка на стороне -фронтенда или на стороне бэкенда, увидеть, какие данные уходят -на сервер с фронтенда и так далее. - -Также, конечно, минимальные знания JavaScript приятны, поскольку -они дают вам возможность написать свой несложный проект хотя -бы с минимальным фронтендом. - -Существуют также fullstack-разработчики, от которых требуется -способность полноценно решать задачи фронтенда на продакшене. -Это требует довольно глубокого знания фронтенд-технологий и -таких фреймворков, как Angular или React. Это требует много -времени, и такая подготовка не входит в курс TopJava. \ No newline at end of file diff --git a/doc/video3.md b/doc/video3.md deleted file mode 100644 index 1821aac3f3af..000000000000 --- a/doc/video3.md +++ /dev/null @@ -1,168 +0,0 @@ -## Рекомендуемые подходы обучения на курсе - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) [Видео](https://drive.google.com/file/d/1v5sVL8ivNvSXEPVlrYibFLD5byywRkmT) - -В предыдущих уроках мы сделали обзор технологий, -которые будут использоваться в нашем курсе и создаваемом проекте. -Теперь давайте поговорим о том, как начать -изучать эти технологии и практиковаться в их использовании. - ---- -В 1980 году National Training Laboratories в -США провели исследования эффективности разных -способов обучения. -Выяснилось, что у лекций и чтения книг крайне -низкая эффективность — всего 5-10%. -Дальше идет просмотр видео лекций и прослушивание аудио. - -Максимальная эффективность в 90% — это обучение -людьми других людей — менторинг и немедленное -применение полученных знаний на практике. - - - - - -Я хочу подчеркнуть этот момент. - ->Когда вы смотрите видео, вам может казаться, что вы все понимаете, но, поверьте, когда вы попытаетесь повторить это самостоятельно, у вас возникнет множество вопросов и сложностей. Практика - это важнейшая часть обучения, не пропускайте ее. - -На нашем курсе мы: -- обсуждаем занятия с коллегами и преподавателями в Slack - эффективность 50% -- выполняем практические домашние задания по каждой пройденной теме - 75% -- помогаем коллегам и разрабатываем собственный выпускной проект - 90% - -### О проверке домашних заданий -Также очень важная часть обучения - проверка -ваших домашних заданий и ревью выпускного -проекта нашими кураторами. - ->**Это самый эффективный способ научиться программировать!** - -При устройстве на работу, на собеседовании обязательно -задавай вопрос про ревью кода. -Если его нет, фирма занимается разработкой -непрофессионально? и рост там будет достаточно -медленным и ограниченным. - -В ревью укажут именно твои ошибки -в стиле, структурах данных, алгоритмах и кодировании. -До вечера вторника участники шлют ссылку -на свой GitHub-репозиторий с домашним -заданием занятия, проверка делается -ассистентами, результат пишется в Slack. -Получается эффективно и оперативно. - -После проверки можно исправить замечания -и пройти ее еще раз. - -В конце стажировки делается ревью вашего выпускного проекта. - - -### Участие на стажировке: ожидания и реальность - -Давайте кратко обсудим, чем курс TopJava является -и чем он не является, что следует от него ожидать -и чего не следует. - -#### 1-й тип ложного представления о стажировке: - ->Я увижу, как с нуля строится web-приложение A с использованием технологий B, просмотрю видео по темам, этого будет достаточно - - - -Почему такой подход не верный: -TopJava - это стажировка, поэтому НЕ рассчитывайте -пройти ее на диване с пакетом поп-корна. -Тебе придется на ней РАБОТАТЬ (выполнять ДЗ, -самостоятельно решать какие-то задачи, читать логи, -дебажить, ходить на StackOverflow и даже думать -об этом, засыпая) - -#### 2-й тип ложного представления о стажировке: ->Меня научат шаблонам работы с технологией А, и я -> смогу их применять в любой ситуации - -Почему это тоже не вполне верный подход: -Используемые на стажировке технологии представляют -собой инструменты, которые позволяет сделать -что-то проще. Мы поделимся практикой их использования, -неочевидными особенностями и т. п., покажем -"грабли", на которые вы рано или поздно наступите. -Нет гарантии, что, устроившись на работу, вы -увидите точно такие же подходы. -Все проекты и команды индивидуальны: используются -различные инструменты и различные решения. -Столкновение с технологиями, с которыми -ты ранее не был знаком, - это нормальная -часть жизни любого программиста. Нужно быть к этому готовым. - -Хорошее представление о решении проблем -дает поиск на StackOverflow, где почти -на любую проблему дается большое количество -вариантов решения. Поиск решений и выбор -лучшего - это основная работа Java-разработчика, -и мы максимально постараемся этому научить: -каждый раз в конкретной ситуации вы должны -будете САМИ думать, что применять и как. -Выполняя домашние задания, вы должны приложить -все усилия, чтобы самостоятельно найти решение. -Далее в начале следующего занятия вы также -посмотрите разбор решения, подготовленного -Григорием Кислиным. - -#### 3-й вариант ошибочного представления о стажировке TopJava связан с неверным представлением о том зачем нужна проверка домашних заданий. Например, человек может ошибочно рассуждать так: - ->я хочу проходить стажировку с проверкой ДЗ, -> чтобы мне рассказали, как нужно правильно выполнять задания - -Задача проверки не в том, чтобы общими усилиями -написать код, который ты и так увидишь в разборе. -Если у тебя что-то не получается, наша -задача - не найти ошибку/подебажить за тебя/почитать -логи и т. п., а подсказать способ самостоятельно -найти решение (хотя для этого мы сначала сами -ищем/дебажим/читаем) -Вторая важная задача, которую решает -проверяющий, - увидеть то, что не увидел ты. -Когда ты сдаешь задание на ревью, тебе -может казаться, что все почти идеально. Проверяющий -подскажет тебе, где ты что-то пропустил из-за -недостатка опыта, что позволит тебе улучшить код. - ---- - ->Любое знание стоит воспринимать как подобие семантического дерева: убедитесь в том, что понимаете фундаментальные принципы, то есть ствол и крупные ветки, прежде чем лезть в мелкие листья-детали. Иначе последним не на чем будет держаться -— Илон Маск - -Обычно в занятии дается много дополнительного -материала и ссылок. Не стоит стремиться прочитать -все ссылки урока, их можно использовать как -справочник. Гораздо важнее пройти основной -материал урока и сделать домашнее -задание - этого достаточно для усвоения -материала и получения той самой -основы - ствола и крупных веток, на -которых впоследствии можно наращивать листву. - - -Как правило, подбираются участники разного -уровня. Поэтому главное – не стеснятся -задавать вопросы (после самостоятельного -гугления и поиска решения). Всегда есть -поддержка группы (в том числе от пришедших -на бесплатный повтор участников), моя и ассистентов. - ---- -### Основные навыки программиста, которые необходимо развить на курсе -Давайте перечислим набор навыков, которые -вам необходимо развивать в ходе курса -и которые необходимы любому программисту: - -- умение и привычка искать -информацию, чтобы иметь больший выбор из -доступных вариантов технологий и подходов -к решению задачи, умение пользоваться StackOverflow; -- умение пользоваться дебаггером в Intellij Idea; -- умение пользоваться DevTools в браузере; -- определенный кругозор и опыт для того, чтобы придумывать поисковые запросы. diff --git a/doc/video4.md b/doc/video4.md deleted file mode 100644 index 7d9b00716dca..000000000000 --- a/doc/video4.md +++ /dev/null @@ -1,110 +0,0 @@ -## Структура приложения (многоуровневая архитектура) -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) [Видео](https://drive.google.com/file/d/1UHzSy9i-uonmTMFoR5v69Y-vyWLCLQWd) - -Приложение, которое мы будем разрабатывать, это [программа для подсчета калорий](http://javaops-demo.ru/topjava). - -В этом видео обсудим структуру этого приложения. - ---- -Ссылки на отчеты, которые будут использоваться в этом уроке: - -- [Многоуровневая архитектура (русскоязыная статья в Wikipedia)](https://ru.wikipedia.org/wiki/%D0%9C%D0%BD%D0%BE%D0%B3%D0%BE%D1%83%D1%80%D0%BE%D0%B2%D0%BD%D0%B5%D0%B2%D0%B0%D1%8F_%D0%B0%D1%80%D1%85%D0%B8%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0) -- [Multitier architecture (англоязычная статья в Wikipedia)](https://en.wikipedia.org/wiki/Multitier_architecture) - ---- - - - -На структурной схеме приложения вы видите, что оно условно разделено на 4 части: **Views**, -**Controller**, **Service** и **Repository**. - -Такой подход является реализацией многоуровневой архитектуры в программировании. -По-английски этот подход называется **_Multitier architecture_**. - -Его суть заключается в разделении приложения на несколько слоев, -каждый из которых ответственен за конкретную задачу. - -### Слой отображения (View) -Views соответствует слою отображения (или presentation layer). Это user interface (UI) или -фронтенд - все то, что мы видим и с чем взаимодействуем в браузере. -В качестве View могут быть HTML-страницы, созданные с использованием специальных -движков шаблонов, например, JSP (встроен в Tomcat), или -Thymeleaf (шаблоны по умолчанию в Spring Boot), или отдельное frontend-приложение, -написанное на одном из JavaScript-фреймворков. - -В случае с движками шаблонов HTML-страницы будут располагаться в одном проекте -с основным кодом приложения. Для приложений с "небогатым" UI используются именно шаблоны. -Этот подход отличается от так называемых RIA - rich internet application - приложений со сложным UI. - -Главное отличие rich internet application от приложений с фронтендом -на движках шаблонов заключается в том, что фронтенд, созданный на движке -шаблонов, работает на сервере, и это что-то простое. -В случае с rich internet application фронтенд-приложение загружается -через Интернет к вам на компьютер и запускается в браузере. -Оно может быть максимально сложным и выполнять функции традиционных -десктоп-приложений. -Иногда в одном приложении смешиваются оба способа: например, страница -логина-пароля в RIA делаются на шаблонах. - -Создание простого фронтенда на движке шаблонов проще, поэтому -в курсе мы будем использовать этот способ. - -### Слой контроллеров (Controller) -Следующий слой, который мы видим - это **Controller**. - -В многоуровневой архитектуре он соответствует слою, который -называется "**_Слой приложения_**" или "**_Application layer_**"). -В GRASP (General Responsibility Assignment Software Patterns) -он так и называется - Controller layer. - -Это слой приложения, который ответственен за обработку HTTP-запросов и проверку корректности входных данных. Если мы открываем главную страницу на сайте или отправляем заполненную на сайте форму, фронтенд-приложение отправляет HTTP-запрос серверу (в нашем случае контейнеру сервлетов), который принимает запрос и перенаправляет его в контроллер, соответствующий введенному в браузере URL или адресу, который вызывается при отправке формы через сайт. -Также контроллеры могут принимать запросы не только от фронтенда, но и от других приложений. - -Слой контроллеров не имеет доступа к базе данных. Контроллеры общаются только с сервисами. - -### Слой сервисов (Service layer) -Слой **Service** на схеме приложения соответствует слою -бизнес-логики (или **_Business layer_**) в многоуровневой архитектуре. -В слое Service инкапсулирована вся бизнес-логика нашего приложения. -Если коммуникация с фронтендом или другими приложениями - это ответственность контроллеров, -то обработка данных - это ответственность сервисов. - -### Слой доступа к данным (Data layer) -Слой сервисов общается со слоем, ответственным за работу с базами данных. -Этот слой называют **Data layer** (также можно встретить -названия **Persistence Layer** или **Data access layer**), и он -представлен в виде **_Data Access Object_** классов -(коротко - **_DAO-классы_**) или классов, реализующих паттерн "репозиторий". -С обоими видами классов вы попрактикуетесь в ходе курса. -[Репозиторий - это также один из архитектурных паттернов](https://martinfowler.com/eaaCatalog/repository.html) - -Подобное разделение приложения на слои дает гибкость -и существенно упрощает доработку и переиспользование приложения. -Например, создав по такому принципу приложение, -содержащее слои репозиториев, сервисов и контроллеров, -мы в дальнейшем можем легко использовать это приложение -с различными фронтенд-приложениями или мобильными приложениями. -Если мы решим перейти на другую базу данных, мы можем -переписать только слой репозиториев, и нам не требуется -вносить изменения в слои сервисов и контроллеров. - ->Для маленького приложение такой подход может показаться ->избыточно сложным, но по мере расширения это является спасением. - -Подавляющее большинство реальных приложений построено с использованием именно этой архитектуры. - -### Краткие итоги -В этом видео мы познакомились с концепцией многоуровневой архитектуры, -которую мы применим при создании приложения на курсе. -Многоуровневая архитектура предполагает разделение приложения на слои: -- Views - слой отображения является фронтендом; -- Controller - слой приложения ответственен за прием и валидацию входных данных; -- слой Service включает в себя весь код, отражающий бизнес логику; -- Repository или Data layer отвечает за взаимодействие с базой данных. - -Также мы обсудили какие преимущества дает такой подход. -Среди преимуществ в первую очередь возможность повторного -использования различных слоев и упрощение их доработки и изменения. - - - diff --git a/doc/video5-vcs-git.md b/doc/video5-vcs-git.md deleted file mode 100644 index 7a172e517bc8..000000000000 --- a/doc/video5-vcs-git.md +++ /dev/null @@ -1,180 +0,0 @@ -## Системы управления версиями, Git -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) [Видео](https://drive.google.com/file/d/1uFjIsxsaSAXxFSwSpjJIGK7Ug2VXf6yH) - -video5-vcs-git.md - -В этом уроке мы рассмотрим системы управления версиями -и самую популярную из них - Git. - ---- -* [StackOverflow 2021 survey](https://insights.stackoverflow.com/survey/2021#technology-most-popular-technologies) -* [Введение в Git и GitHub: установка и настройка](https://topjava.ru/blog/vvedeniye-v-git-github-ustanovka-i-nastroyka) -* [Введение в Git и GitHub: базовые команды](https://topjava.ru/blog/vvedeniye-v-git-github-bazovyye-komandy) -* [Введение в Git и GitHub: ошибки использования](https://topjava.ru/blog/vvedeniye-v-git-oshibki-ispolzovaniya-ch-9) -* [Бесплатная русскоязычная книга Pro Git](https://git-scm.com/book/ru/v2/) ---- - -Git является де-факто стандартом среди систем управления версиями. -Опрос, проведенный StackOverflow в 2021 году показывает, -что Git используют почти 95% опрошенных разработчиков. - -Когда-то популярная централизованная система контроля -версий SVN (Subversion) практически полностью заменена Git. -Но некоторые большие проекты все еще используют SVN. -Примером такого проекта является WordPress. -Я также все еще иногда встречаю SVN в вакансиях российских компаний. - -### Что такое Git и GitHub -В ходе курса мы будем использовать Git. -Git - это распределенная система управления версиями. -Это означает, что код, над которым работает команда, -и ранее сохраненные (закоммиченные) версии проекта -хранится на компьютерах каждого члена команды, -а также в удаленном репозитории, который можно -сравнить с облачным хранилищем. - -Некоторые путают Git и GitHub, но это не одно и то же. - -**Git** — это утилита, которую -программист устанавливает у себя на компьютере для -сохранения состояний проектов и контроля версий проекта. - -**GitHub** — это провайдер удаленных репозиториев, -сайт (хостинг) для хранения кода проекта и его изменений, -для обмена файлами с членами команды проекта. -Программисты могут создавать на GitHub публичные репозитории, -в которых код доступен всем. Компании могут приобрести платный -аккаунт на GitHub и вести свои проекты в закрытых репозиториях, -доступ к которым имеют только члены команды проекта. -Существуют и другие подобные сайты - провайдеры удаленных -репозиториев, например, BitBucket, SourceForge, GitLab и т. д. - - -### Как работают Git и GitHub - - -Говоря кратко, работа с Git и удаленным репозиторием -(в нашем случае это Github) выглядит следующим образом. - -Первый разработчик, который начинал работу над проектом, -использовал команду **_git init_** в корневой директории -проекта для того, чтобы инициализировать пустой Git-репозиторий. -В этот момент Git создает в директории с проектом скрытую -директорию, содержащую файлы, необходимые для его работы. -Теперь первый разработчик может с помощью команды **_git add_** -добавлять файлы проекта в индекс - то есть в зону, -отслеживаемую git, а также может фиксировать изменения, -создавая коммиты (**_git commit_**), и загружать их в -удаленный репозиторий (например, на GitHub) с помощью -команды **_git push_**. - -Спустя некоторое время к работе над проектом подключается второй разработчик. - -Поскольку первый разработчик пушил изменения файлов проекта -на удаленный репозиторий, второй разработчик может скачать -их себе на компьютер с помощью команды **_git pull_** -и продолжить работу с учетом этих изменений. - -Давайте повторим. -В описанной работе использовались команды: - -* **_git init_** - эта команда инициализирует работу Git -для конкретного проекта. В папке с проектом создается -скрытая папка, хранящая все файлы, необходимые Git -для работы. Папка с этими файлами впоследствии будет -загружена в удаленный репозиторий, и второй программист, -скачавший проект, увидит у себя на компьютере все ранее -созданные с помощью Git версии. -* **_git add_** - это команда, которая добавляет файлы -проекта в индекс (или отслеживаемую зону), чтобы их можно -было в какой-то момент закоммитить (то есть сохранить, зафиксировать). -* **_git commit_** - сохраняет все файлы проекта в -текущем состоянии так, что мы в любой момент сможем -вернуться к этому состоянию - посмотреть, как изменились -файлы, или даже полностью вернуть проект к состоянию -данного коммита. -* **_git push_** - загружает файлы в удаленный Git-репозиторий, -чтобы их могли скачать другие программисты. -* **_git pull_** используют для того, чтобы скачать -файлы из удаленного репозитория себе на компьютер. - -Также Git позволяет создавать ветки. Это означает, -что вы можете создать копию стабильного кода и -продолжить работу в этой копии, не подвергая риску -стабильную версию. Когда код с новой функциональностью -готов, отлажен и протестирован, он может быть -слит ("смержен", от слова "merge") в основную ветку со стабильным кодом. - -С помощью этих функций Git позволяет команде разработчиков -работать одновременно над разными задачами и поддерживать -код в стабильном состоянии. - -Также Git позволяет в любой момент понять, кто и когда внес -изменения в определенный код, что существенно упрощает взаимодействия -с коллегами. Например, в случае, если вы видите новые -строки кода, которые вызывают у вас вопросы, с помощью -Git вы можете разобраться, кто внес изменения именно -в эти строки, и связаться с этим человеком -для уточнения деталей. - -Если произошла ситуация, при которой два разработчика -изменили один и тот же файл, Git потребует от вас вручную -отрегулировать такой конфликт. - -Также благодаря Git у вас всегда под рукой детальная -история изменений. С помощью комментариев к коммитам -вы можете детально документировать ход работы над -проектом и в случае необходимости разобраться, в -какой момент что-то пошло не так, и откатиться -к той версии, где ошибка еще не была сделана. -Или просто выяснить, как выглядел код определенного -класса или метода в определенной версии и внести правки -в текущую версию, если это необходимо. - -### Дополнительные материалы - -Для изучения Git рекомендуем в первую очередь ознакомиться со статьями на сайте [topjava.ru](https://topjava.ru): - -* [Введение в Git и GitHub: установка и настройка](https://topjava.ru/blog/vvedeniye-v-git-github-ustanovka-i-nastroyka) -* [Введение в Git и GitHub: базовые команды](https://topjava.ru/blog/vvedeniye-v-git-github-bazovyye-komandy) -* [Введение в Git и GitHub: ошибки использования](https://topjava.ru/blog/vvedeniye-v-git-oshibki-ispolzovaniya-ch-9) - -В них дается детальная инструкция по первоначальной настройке -Git, GitHub, в том числе инструкция по настройке -Access token в GitHub, что вам обязательно нужно будет сделать. - -Также можно упомянуть официальную русскоязычную книгу Pro Git, -которая доступна [бесплатно в электронном виде](https://git-scm.com/book/ru/v2/). - -### Как мы будем использовать Git на курсе -В Git очень много команд, но в ходе курса мы будем использовать -только самые необходимые из них. -С git можно работать как через терминал с помощью команд, -так и с помощью различных программ с графическим интерфейсом. -Мы будем преимущественно использовать интеграцию -IntelliJ Idea с Git, поскольку она очень удобна -и существенно упрощает работу. -Однако знать, как выполнять аналогичные операции -в терминале, очень полезно. - -### Резюме -В этом уроке мы сделали краткий обзор систем управления -версиями и самой популярной из них - Git: - -* разобрались в различиях Git и GitHub. -Git - это система контроля версий, а -GitHub - провайдер удаленных репозиториев, один из многих, доступных на рынке. -* кратко познакомились с тем, как Git работает. -Узнали о командах git init, git add, git commit, git push и git pull. -* Также мы обсудили, что такое ветки и какие возможности они дают. - ---- - -Если вы не работали раньше с Git, не беспокойтесь. -В последующих уроках работа с Git будет изучаться -на практике в ходе работы над проектом. Но дополнительно -почитать о Git все же стоит. - - - -