+ * Handling Hibernate lazy-loading
+ *
+ * @link https://github.com/FasterXML/jackson
+ * @link https://github.com/FasterXML/jackson-datatype-hibernate
+ * @link https://github.com/FasterXML/jackson-docs/wiki/JacksonHowToCustomSerializers
+ */
+public class JacksonObjectMapper extends ObjectMapper {
+
+ private static final ObjectMapper MAPPER = new JacksonObjectMapper();
+
+ private JacksonObjectMapper() {
+ registerModule(new Hibernate5Module());
+
+ registerModule(new JavaTimeModule());
+ configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
+
+ setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
+ setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
+ setSerializationInclusion(JsonInclude.Include.NON_NULL);
+ }
+
+ public static ObjectMapper getMapper() {
+ return MAPPER;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java
new file mode 100644
index 000000000..fda04590d
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java
@@ -0,0 +1,37 @@
+package ru.javawebinar.topjava.web.json;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectReader;
+
+import java.io.IOException;
+import java.util.List;
+
+import static ru.javawebinar.topjava.web.json.JacksonObjectMapper.getMapper;
+
+public class JsonUtil {
+
+ public static
+
+
+
+ * Comparing actual and expected objects via AssertJ
+ * Support converting json MvcResult to objects for comparation.
+ */
+public class MatcherFactory {
+
+ public static Filter separately
+ *
+ */
+ public List
"));
+ return ResponseEntity.unprocessableEntity().body(errorFieldsMsg);
+ }
+ if (userTo.isNew()) {
+ super.create(userTo);
+ } else {
+ super.update(userTo, userTo.id());
+ }
+ return ResponseEntity.ok().build();
+ }
+
+ @Override
+ @PostMapping("/{id}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void enable(@PathVariable int id, @RequestParam boolean enabled) {
+ super.enable(id, enabled);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java
new file mode 100644
index 000000000..ccc43013a
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java
@@ -0,0 +1,42 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.to.UserTo;
+
+import static ru.javawebinar.topjava.web.SecurityUtil.authUserId;
+
+@RestController
+@RequestMapping(value = ProfileRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+public class ProfileRestController extends AbstractUserController {
+ static final String REST_URL = "/rest/profile";
+
+ @GetMapping
+ public User get() {
+ return super.get(authUserId());
+ }
+
+ @DeleteMapping
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void delete() {
+ super.delete(authUserId());
+ }
+
+ @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void update(@RequestBody UserTo userTo) {
+ super.update(userTo, authUserId());
+ }
+
+ @GetMapping("/text")
+ public String testUTF() {
+ return "Русский текст";
+ }
+
+ @GetMapping("/with-meals")
+ public User getWithMeals() {
+ return super.getWithMeals(authUserId());
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/cache/ehcache.xml b/src/main/resources/cache/ehcache.xml
new file mode 100644
index 000000000..05589f71f
--- /dev/null
+++ b/src/main/resources/cache/ehcache.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+ Spring Security,
+ Spring MVC,
+ Spring Data JPA,
+ Spring Security
+ Test,
+ Hibernate ORM,
+ Hibernate Validator,
+ SLF4J,
+ Json Jackson,
+ JSP,
+ JSTL,
+ Apache Tomcat,
+ WebJars,
+ DataTables,
+ EHCACHE,
+ PostgreSQL,
+ HSQLDB,
+ JUnit 5,
+ Hamcrest,
+ AssertJ,
+ jQuery,
+ jQuery plugins,
+ Bootstrap.
+
+ <%--https://getbootstrap.com/docs/4.0/components/card/--%>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
" + jqXHR.responseJSON : ""),
+ type: "error",
+ layout: "bottomRight"
+ });
+ failedNote.show()
+}
\ No newline at end of file
diff --git a/src/main/webapp/resources/js/topjava.meals.js b/src/main/webapp/resources/js/topjava.meals.js
new file mode 100644
index 000000000..dacdad3d4
--- /dev/null
+++ b/src/main/webapp/resources/js/topjava.meals.js
@@ -0,0 +1,52 @@
+const mealAjaxUrl = "profile/meals/";
+
+// https://stackoverflow.com/a/5064235/548473
+const ctx = {
+ ajaxUrl: mealAjaxUrl,
+ updateTable: function () {
+ $.ajax({
+ type: "GET",
+ url: mealAjaxUrl + "filter",
+ data: $("#filter").serialize()
+ }).done(updateTableByData);
+ }
+}
+
+function clearFilter() {
+ $("#filter")[0].reset();
+ $.get(mealAjaxUrl, updateTableByData);
+}
+
+$(function () {
+ makeEditable(
+ $("#datatable").DataTable({
+ "paging": false,
+ "info": true,
+ "columns": [
+ {
+ "data": "dateTime"
+ },
+ {
+ "data": "description"
+ },
+ {
+ "data": "calories"
+ },
+ {
+ "defaultContent": "Edit",
+ "orderable": false
+ },
+ {
+ "defaultContent": "Delete",
+ "orderable": false
+ }
+ ],
+ "order": [
+ [
+ 0,
+ "desc"
+ ]
+ ]
+ })
+ );
+});
\ No newline at end of file
diff --git a/src/main/webapp/resources/js/topjava.users.js b/src/main/webapp/resources/js/topjava.users.js
new file mode 100644
index 000000000..384f07bc3
--- /dev/null
+++ b/src/main/webapp/resources/js/topjava.users.js
@@ -0,0 +1,94 @@
+const userAjaxUrl = "admin/users/";
+
+// https://stackoverflow.com/a/5064235/548473
+const ctx = {
+ ajaxUrl: userAjaxUrl,
+ updateTable: function () {
+ $.get(userAjaxUrl, updateTableByData);
+ }
+}
+
+function enable(chkbox, id) {
+ var enabled = chkbox.is(":checked");
+// https://stackoverflow.com/a/22213543/548473
+ $.ajax({
+ url: userAjaxUrl + id,
+ type: "POST",
+ data: "enabled=" + enabled
+ }).done(function () {
+ chkbox.closest("tr").attr("data-user-enabled", enabled);
+ successNoty(enabled ? "common.enabled" : "common.disabled");
+ }).fail(function () {
+ $(chkbox).prop("checked", !enabled);
+ });
+}
+
+// $(document).ready(function () {
+$(function () {
+ makeEditable(
+ $("#datatable").DataTable({
+ "ajax": {
+ "url": userAjaxUrl,
+ "dataSrc": ""
+ },
+ "paging": false,
+ "info": true,
+ "columns": [
+ {
+ "data": "name"
+ },
+ {
+ "data": "email",
+ "render": function (data, type, row) {
+ if (type === "display") {
+ return "" + data + "";
+ }
+ return data;
+ }
+ },
+ {
+ "data": "roles"
+ },
+ {
+ "data": "enabled",
+ "render": function (data, type, row) {
+ if (type === "display") {
+ return "";
+ }
+ return data;
+ }
+ },
+ {
+ "data": "registered",
+ "render": function (date, type, row) {
+ if (type === "display") {
+ return date.substring(0, 10);
+ }
+ return date;
+ }
+ },
+ {
+ "orderable": false,
+ "defaultContent": "",
+ "render": renderEditBtn
+ },
+ {
+ "orderable": false,
+ "defaultContent": "",
+ "render": renderDeleteBtn
+ }
+ ],
+ "order": [
+ [
+ 0,
+ "asc"
+ ]
+ ],
+ "createdRow": function (row, data, dataIndex) {
+ if (!data.enabled) {
+ $(row).attr("data-user-enabled", false);
+ }
+ }
+ })
+ );
+});
\ No newline at end of file
diff --git a/src/main/webapp/test.html b/src/main/webapp/test.html
new file mode 100644
index 000000000..e50b33277
--- /dev/null
+++ b/src/main/webapp/test.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java b/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java
new file mode 100644
index 000000000..43f143cc7
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java
@@ -0,0 +1,19 @@
+package ru.javawebinar.topjava;
+
+import org.springframework.lang.NonNull;
+import org.springframework.test.context.support.DefaultActiveProfilesResolver;
+
+import java.util.Arrays;
+
+//http://stackoverflow.com/questions/23871255/spring-profiles-simple-example-of-activeprofilesresolver
+public class ActiveDbProfileResolver extends DefaultActiveProfilesResolver {
+ @Override
+ public @NonNull
+ String[] resolve(@NonNull Class> aClass) {
+ // https://stackoverflow.com/a/52438829/548473
+ String[] activeProfiles = super.resolve(aClass);
+ String[] activeProfilesWithDb = Arrays.copyOf(activeProfiles, activeProfiles.length + 1);
+ activeProfilesWithDb[activeProfiles.length] = Profiles.getActiveDbProfile();
+ return activeProfilesWithDb;
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/MatcherFactory.java b/src/test/java/ru/javawebinar/topjava/MatcherFactory.java
new file mode 100644
index 000000000..15e01e155
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/MatcherFactory.java
@@ -0,0 +1,83 @@
+package ru.javawebinar.topjava;
+
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.ResultMatcher;
+import ru.javawebinar.topjava.web.json.JsonUtil;
+
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+import java.util.function.BiConsumer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Factory for creating test matchers.
+ *