From ab78a6fe8c93ed1d027dc498d25be961b1a6f4f7 Mon Sep 17 00:00:00 2001 From: JavaOPs Date: Thu, 11 Dec 2025 00:06:39 +0300 Subject: [PATCH 1/2] 10_04_opt_json_view --- .../java/ru/javawebinar/topjava/View.java | 6 ++++++ .../ru/javawebinar/topjava/model/Meal.java | 19 ++++++++++++++++++- .../ru/javawebinar/topjava/to/MealTo.java | 14 ++++++++++++++ .../topjava/web/json/JacksonObjectMapper.java | 4 ++++ .../topjava/web/json/JsonUtil.java | 9 +++++++++ .../topjava/web/meal/MealUIController.java | 5 +++++ src/main/webapp/WEB-INF/jsp/meals.jsp | 2 +- .../webapp/resources/js/topjava.common.js | 8 +------- src/main/webapp/resources/js/topjava.meals.js | 8 +------- .../topjava/web/json/JsonUtilTest.java | 12 ++++++++++++ 10 files changed, 71 insertions(+), 16 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/View.java diff --git a/src/main/java/ru/javawebinar/topjava/View.java b/src/main/java/ru/javawebinar/topjava/View.java new file mode 100644 index 000000000..1300d177a --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/View.java @@ -0,0 +1,6 @@ +package ru.javawebinar.topjava; + +public class View { + public interface JsonREST {} + public interface JsonUI {} +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index 0a833ecea..73e243cb1 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -1,10 +1,14 @@ package ru.javawebinar.topjava.model; import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonView; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; import org.hibernate.validator.constraints.Range; import org.springframework.format.annotation.DateTimeFormat; +import ru.javawebinar.topjava.View; import ru.javawebinar.topjava.util.DateTimeUtil; import javax.persistence.*; @@ -34,7 +38,7 @@ public class Meal extends AbstractBaseEntity { @Column(name = "date_time", nullable = false) @NotNull - @DateTimeFormat(pattern = DateTimeUtil.DATE_TIME_PATTERN) + @JsonView(View.JsonREST.class) private LocalDateTime dateTime; @Column(name = "description", nullable = false) @@ -104,6 +108,19 @@ public void setUser(User user) { this.user = user; } + + @JsonGetter + @JsonView(View.JsonUI.class) + @JsonFormat(pattern = DateTimeUtil.DATE_TIME_PATTERN) + public LocalDateTime getDateTimeUI() { + return dateTime; + } + + @DateTimeFormat(pattern = DateTimeUtil.DATE_TIME_PATTERN) + public void setDateTimeUI(LocalDateTime dateTime) { + this.dateTime = dateTime; + } + @Override public String toString() { return "Meal{" + diff --git a/src/main/java/ru/javawebinar/topjava/to/MealTo.java b/src/main/java/ru/javawebinar/topjava/to/MealTo.java index 059f14a44..526f26e40 100644 --- a/src/main/java/ru/javawebinar/topjava/to/MealTo.java +++ b/src/main/java/ru/javawebinar/topjava/to/MealTo.java @@ -1,11 +1,18 @@ package ru.javawebinar.topjava.to; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonView; +import ru.javawebinar.topjava.View; +import ru.javawebinar.topjava.util.DateTimeUtil; + import java.beans.ConstructorProperties; import java.time.LocalDateTime; import java.util.Objects; public class MealTo extends BaseTo { + @JsonView(View.JsonREST.class) private final LocalDateTime dateTime; private final String description; @@ -56,6 +63,13 @@ public int hashCode() { return Objects.hash(id, dateTime, description, calories, excess); } + @JsonGetter + @JsonView(View.JsonUI.class) + @JsonFormat(pattern = DateTimeUtil.DATE_TIME_PATTERN) + public LocalDateTime getDateTimeUI() { + return dateTime; + } + @Override public String toString() { return "MealTo{" + diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java b/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java index 8237df93b..58704d86d 100644 --- a/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java +++ b/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import ru.javawebinar.topjava.View; /** *

@@ -29,6 +30,9 @@ private JacksonObjectMapper() { setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); setSerializationInclusion(JsonInclude.Include.NON_NULL); + +// https://stackoverflow.com/questions/22875642/jackson-set-default-view + setConfig(getSerializationConfig().withView(View.JsonREST.class)); } public static ObjectMapper getMapper() { diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java index fda04590d..99618d333 100644 --- a/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java +++ b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; import java.io.IOException; import java.util.List; @@ -34,4 +35,12 @@ public static String writeValue(T obj) { throw new IllegalStateException("Invalid write to JSON:\n'" + obj + "'", e); } } + + public static String writeValue(T obj, ObjectWriter ow) { + try { + return ow.writeValueAsString(obj); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Invalid write to JSON:\n'" + obj + "'", e); + } + } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java index 2b4f2bdcd..900db5e84 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java @@ -1,11 +1,13 @@ package ru.javawebinar.topjava.web.meal; +import com.fasterxml.jackson.annotation.JsonView; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.lang.Nullable; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; +import ru.javawebinar.topjava.View; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.to.MealTo; import ru.javawebinar.topjava.util.ValidationUtil; @@ -21,12 +23,14 @@ public class MealUIController extends AbstractMealController { @Override @GetMapping + @JsonView(View.JsonUI.class) public List getAll() { return super.getAll(); } @Override @GetMapping( "/{id}") + @JsonView(View.JsonUI.class) public Meal get(@PathVariable int id) { return super.get(id); } @@ -54,6 +58,7 @@ public ResponseEntity createOrUpdate(@Valid Meal meal, BindingResult res @Override @GetMapping("/filter") + @JsonView(View.JsonUI.class) public List getBetween( @RequestParam @Nullable LocalDate startDate, @RequestParam @Nullable LocalTime startTime, diff --git a/src/main/webapp/WEB-INF/jsp/meals.jsp b/src/main/webapp/WEB-INF/jsp/meals.jsp index f66313bb3..783b3b2cc 100644 --- a/src/main/webapp/WEB-INF/jsp/meals.jsp +++ b/src/main/webapp/WEB-INF/jsp/meals.jsp @@ -79,7 +79,7 @@

- ">
diff --git a/src/main/webapp/resources/js/topjava.common.js b/src/main/webapp/resources/js/topjava.common.js index 3df7f5c58..972154a44 100644 --- a/src/main/webapp/resources/js/topjava.common.js +++ b/src/main/webapp/resources/js/topjava.common.js @@ -33,18 +33,12 @@ function updateRow(id) { $("#modalTitle").html(i18n["editTitle"]); $.get(ctx.ajaxUrl + id, function (data) { $.each(data, function (key, value) { - form.find("input[name='" + key + "']").val( - key === "dateTime" ? formatDate(value) : value - ); + form.find("input[name='" + key + "']").val(value); }); $('#editRow').modal(); }); } -function formatDate(date) { - return date.replace('T', ' ').substr(0, 16); -} - function deleteRow(id) { if (confirm(i18n['common.confirm'])) { $.ajax({ diff --git a/src/main/webapp/resources/js/topjava.meals.js b/src/main/webapp/resources/js/topjava.meals.js index 5036a03c9..bd2d70f1d 100644 --- a/src/main/webapp/resources/js/topjava.meals.js +++ b/src/main/webapp/resources/js/topjava.meals.js @@ -21,13 +21,7 @@ $(function () { makeEditable({ "columns": [ { - "data": "dateTime", - "render": function (date, type, row) { - if (type === 'display') { - return formatDate(date); - } - return date; - } + "data": "dateTimeUI" }, { "data": "description" diff --git a/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java b/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java index 540586d11..67d68472e 100644 --- a/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java @@ -1,12 +1,16 @@ package ru.javawebinar.topjava.web.json; +import com.fasterxml.jackson.databind.ObjectWriter; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ru.javawebinar.topjava.View; import ru.javawebinar.topjava.model.Meal; import java.util.List; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; import static ru.javawebinar.topjava.MealTestData.*; class JsonUtilTest { @@ -27,4 +31,12 @@ void readWriteValues() { List actual = JsonUtil.readValues(json, Meal.class); MEAL_MATCHER.assertMatch(actual, meals); } + + @Test + public void writeWithView() { + ObjectWriter uiWriter = JacksonObjectMapper.getMapper().writerWithView(View.JsonUI.class); + String json = JsonUtil.writeValue(adminMeal1, uiWriter); + System.out.println(json); + assertThat(json, containsString("dateTimeUI")); + } } \ No newline at end of file From 6b7c7427a2cb37d936ce71c80260f6d104880a22 Mon Sep 17 00:00:00 2001 From: JavaOPs Date: Thu, 11 Dec 2025 00:06:55 +0300 Subject: [PATCH 2/2] 10_05_opt_validated_groups --- src/main/java/ru/javawebinar/topjava/View.java | 4 ++++ .../java/ru/javawebinar/topjava/model/Meal.java | 14 ++++++++------ .../repository/jdbc/JdbcMealRepository.java | 3 ++- .../javawebinar/topjava/util/ValidationUtil.java | 4 ++-- .../topjava/web/meal/MealUIController.java | 5 +++-- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/View.java b/src/main/java/ru/javawebinar/topjava/View.java index 1300d177a..81d178333 100644 --- a/src/main/java/ru/javawebinar/topjava/View.java +++ b/src/main/java/ru/javawebinar/topjava/View.java @@ -3,4 +3,8 @@ public class View { public interface JsonREST {} public interface JsonUI {} + + // https://narmo7.wordpress.com/2014/04/26/how-to-set-up-validation-group-in-springmvc/ + // http://forum.spring.io/forum/spring-projects/web/117289-validated-s-given-groups-should-consider-default-group-or-not + public interface ValidatedUI {} } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index 73e243cb1..e6f78ddaa 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -9,12 +9,14 @@ import org.hibernate.validator.constraints.Range; import org.springframework.format.annotation.DateTimeFormat; import ru.javawebinar.topjava.View; +import ru.javawebinar.topjava.View.ValidatedUI; import ru.javawebinar.topjava.util.DateTimeUtil; import javax.persistence.*; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; +import javax.validation.groups.Default; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; @@ -37,25 +39,25 @@ public class Meal extends AbstractBaseEntity { public static final String GET_BETWEEN = "Meal.getBetween"; @Column(name = "date_time", nullable = false) - @NotNull + @NotNull(groups = {ValidatedUI.class, Default.class}) @JsonView(View.JsonREST.class) private LocalDateTime dateTime; @Column(name = "description", nullable = false) - @NotBlank - @Size(min = 2, max = 120) + @NotBlank(groups = {ValidatedUI.class, Default.class}) + @Size(min = 2, max = 120, groups = {ValidatedUI.class, Default.class}) private String description; @Column(name = "calories", nullable = false) - @NotNull - @Range(min = 10, max = 5000) + @NotNull(groups = {ValidatedUI.class, Default.class}) + @Range(min = 10, max = 5000, groups = {ValidatedUI.class, Default.class}) private Integer calories; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) @OnDelete(action = OnDeleteAction.CASCADE) @JsonBackReference -// @NotNull + @NotNull private User user; public Meal() { diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java index 775d314ed..9ff92e631 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java @@ -9,6 +9,7 @@ import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.View; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; import ru.javawebinar.topjava.util.ValidationUtil; @@ -40,7 +41,7 @@ public JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate @Override @Transactional public Meal save(Meal meal, int userId) { - ValidationUtil.validate(meal); + ValidationUtil.validate(meal, View.ValidatedUI.class); MapSqlParameterSource map = new MapSqlParameterSource() .addValue("id", meal.getId()) diff --git a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java index 93e5a4b93..e3ddddcfe 100644 --- a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java @@ -23,9 +23,9 @@ public class ValidationUtil { private ValidationUtil() { } - public static void validate(T bean) { + public static void validate(T bean, Class... groups) { // https://alexkosarev.name/2018/07/30/bean-validation-api/ - Set> violations = validator.validate(bean); + Set> violations = validator.validate(bean, groups); if (!violations.isEmpty()) { throw new ConstraintViolationException(violations); } diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java index 900db5e84..19363b882 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java @@ -6,13 +6,14 @@ import org.springframework.http.ResponseEntity; import org.springframework.lang.Nullable; import org.springframework.validation.BindingResult; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.javawebinar.topjava.View; +import ru.javawebinar.topjava.View.ValidatedUI; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.to.MealTo; import ru.javawebinar.topjava.util.ValidationUtil; -import javax.validation.Valid; import java.time.LocalDate; import java.time.LocalTime; import java.util.List; @@ -44,7 +45,7 @@ public void delete(@PathVariable int id) { @PostMapping @ResponseStatus(HttpStatus.NO_CONTENT) - public ResponseEntity createOrUpdate(@Valid Meal meal, BindingResult result) { + public ResponseEntity createOrUpdate(@Validated(ValidatedUI.class) Meal meal, BindingResult result) { if (result.hasErrors()) { return ValidationUtil.getErrorResponse(result); }