diff --git a/2025-11/spring-17-ajax/ajax-demo.html b/2025-11/spring-17-ajax/ajax-demo.html
new file mode 100644
index 00000000..9e302da4
--- /dev/null
+++ b/2025-11/spring-17-ajax/ajax-demo.html
@@ -0,0 +1,91 @@
+
+
+
+ Технологии JS для отправки запросов
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/2025-11/spring-17-ajax/pom.xml b/2025-11/spring-17-ajax/pom.xml
new file mode 100644
index 00000000..0a47af1c
--- /dev/null
+++ b/2025-11/spring-17-ajax/pom.xml
@@ -0,0 +1,17 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-mvc-ajax
+ 1.0
+
+ pom
+
+
+ spring-ajax-demo
+ spring-boot-and-react-demo
+
+
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/pom.xml b/2025-11/spring-17-ajax/spring-ajax-demo/pom.xml
new file mode 100644
index 00000000..360ef7b5
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/pom.xml
@@ -0,0 +1,85 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-ajax-demo
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.5.6
+
+
+
+
+ 17
+ 17
+ 1.18.32
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+
+ org.webjars
+ jquery
+ 3.7.1
+
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+ provided
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/Main.java b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/Main.java
new file mode 100644
index 00000000..725af191
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/Main.java
@@ -0,0 +1,15 @@
+package ru.otus.spring;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Main {
+ // http://localhost:8080/
+ // http://localhost:8080/api/persons
+ // http://localhost:8080/server/system/info
+ public static void main(String[] args) {
+ SpringApplication.run(Main.class);
+ }
+
+}
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/config/WebConfig.java b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/config/WebConfig.java
new file mode 100644
index 00000000..e201fdcf
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/config/WebConfig.java
@@ -0,0 +1,21 @@
+package ru.otus.spring.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import ru.otus.spring.rest.resolvers.SystemInfoMethodArgumentResolver;
+
+import java.util.List;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+
+ @Autowired
+ private SystemInfoMethodArgumentResolver systemInfoMethodArgumentResolver;
+
+ @Override
+ public void addArgumentResolvers(List resolvers) {
+ resolvers.add(systemInfoMethodArgumentResolver);
+ }
+}
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/domain/Person.java b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/domain/Person.java
new file mode 100644
index 00000000..4c7af816
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/domain/Person.java
@@ -0,0 +1,26 @@
+package ru.otus.spring.domain;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "persons")
+public class Person {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private long id;
+
+ @Column(name = "name")
+ private String name;
+}
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/domain/SystemInfo.java b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/domain/SystemInfo.java
new file mode 100644
index 00000000..c16a8a78
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/domain/SystemInfo.java
@@ -0,0 +1,31 @@
+package ru.otus.spring.domain;
+
+public class SystemInfo {
+ private final String osName;
+ private final String timeZone;
+ private final String osArch;
+ private final int processorsCount;
+
+ public SystemInfo(String osName, String timeZone, String osArch, int processorsCount) {
+ this.osName = osName;
+ this.timeZone = timeZone;
+ this.osArch = osArch;
+ this.processorsCount = processorsCount;
+ }
+
+ public String getOsName() {
+ return osName;
+ }
+
+ public String getTimeZone() {
+ return timeZone;
+ }
+
+ public String getOsArch() {
+ return osArch;
+ }
+
+ public int getProcessorsCount() {
+ return processorsCount;
+ }
+}
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/page/PersonPagesController.java b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/page/PersonPagesController.java
new file mode 100644
index 00000000..5e59b329
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/page/PersonPagesController.java
@@ -0,0 +1,20 @@
+package ru.otus.spring.page;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+
+@Controller
+public class PersonPagesController {
+
+ @GetMapping("/")
+ public String listPersonsPage(Model model) {
+ model.addAttribute("keywords", "list users in Omsk, omsk, list users, list users free");
+ return "list";
+ }
+
+ @GetMapping("/add")
+ public String addPersonPage() {
+ return "add";
+ }
+}
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/repostory/PersonRepository.java b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/repostory/PersonRepository.java
new file mode 100644
index 00000000..5fbe33cc
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/repostory/PersonRepository.java
@@ -0,0 +1,7 @@
+package ru.otus.spring.repostory;
+
+import org.springframework.data.repository.ListCrudRepository;
+import ru.otus.spring.domain.Person;
+
+public interface PersonRepository extends ListCrudRepository {
+}
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/GlobalExceptionHandler.java b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/GlobalExceptionHandler.java
new file mode 100644
index 00000000..f622165a
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/GlobalExceptionHandler.java
@@ -0,0 +1,20 @@
+package ru.otus.spring.rest;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import ru.otus.spring.rest.exceptions.NotFoundException;
+
+@RequiredArgsConstructor
+@ControllerAdvice
+public class GlobalExceptionHandler {
+
+ public static final String ERROR_STRING = "Тут пёрсонов нет(";
+
+ @ExceptionHandler(NotFoundException.class)
+ public ResponseEntity handeNotFoundException(NotFoundException ex) {
+ return ResponseEntity.status(404).body(ERROR_STRING);
+ }
+
+}
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/PersonController.java b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/PersonController.java
new file mode 100644
index 00000000..d437f390
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/PersonController.java
@@ -0,0 +1,38 @@
+package ru.otus.spring.rest;
+
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+import ru.otus.spring.rest.dto.PersonDto;
+import ru.otus.spring.rest.exceptions.NotFoundException;
+import ru.otus.spring.service.PersonService;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RequiredArgsConstructor
+@RestController
+public class PersonController {
+
+ private final PersonService personService;
+
+ @GetMapping("/api/persons")
+ public List getAllPersons() {
+ List persons = personService.findAll().stream().map(PersonDto::toDto)
+ .collect(Collectors.toList());
+ if (persons.isEmpty()) {
+ throw new NotFoundException("Persons not found!");
+ }
+ return persons;
+ }
+
+ @PostMapping("/api/persons")
+ public ResponseEntity addPerson(@Valid @RequestBody PersonDto personDto) {
+ var savedPerson = personService.save(personDto.toDomainObject());
+ return ResponseEntity.ok(PersonDto.toDto(savedPerson));
+ }
+}
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/SystemInfoController.java b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/SystemInfoController.java
new file mode 100644
index 00000000..c255342f
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/SystemInfoController.java
@@ -0,0 +1,15 @@
+package ru.otus.spring.rest;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import ru.otus.spring.domain.SystemInfo;
+
+@RestController
+public class SystemInfoController {
+
+ @GetMapping("api/server/system/info")
+ public ResponseEntity getServerSystemInfo(SystemInfo systemInfo) {
+ return ResponseEntity.ok(systemInfo);
+ }
+}
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/dto/PersonDto.java b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/dto/PersonDto.java
new file mode 100644
index 00000000..d8f03542
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/dto/PersonDto.java
@@ -0,0 +1,23 @@
+package ru.otus.spring.rest.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.hibernate.validator.constraints.Length;
+import ru.otus.spring.domain.Person;
+
+@Data
+@AllArgsConstructor
+public class PersonDto {
+
+ private long id;
+ @Length(min = 3)
+ private String name;
+
+ public static PersonDto toDto(Person person) {
+ return new PersonDto(person.getId(), person.getName());
+ }
+
+ public Person toDomainObject() {
+ return new Person(id, name);
+ }
+}
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/exceptions/NotFoundException.java b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/exceptions/NotFoundException.java
new file mode 100644
index 00000000..36593145
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/exceptions/NotFoundException.java
@@ -0,0 +1,8 @@
+package ru.otus.spring.rest.exceptions;
+
+public class NotFoundException extends RuntimeException{
+
+ public NotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/resolvers/SystemInfoMethodArgumentResolver.java b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/resolvers/SystemInfoMethodArgumentResolver.java
new file mode 100644
index 00000000..51f517a7
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/rest/resolvers/SystemInfoMethodArgumentResolver.java
@@ -0,0 +1,33 @@
+package ru.otus.spring.rest.resolvers;
+
+import org.springframework.core.MethodParameter;
+import org.springframework.stereotype.Component;
+import org.springframework.web.bind.support.WebDataBinderFactory;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.method.support.ModelAndViewContainer;
+import ru.otus.spring.domain.SystemInfo;
+import ru.otus.spring.service.SystemInfoService;
+
+@Component
+public class SystemInfoMethodArgumentResolver implements HandlerMethodArgumentResolver {
+
+ private final SystemInfoService systemInfoService;
+
+ public SystemInfoMethodArgumentResolver(SystemInfoService systemInfoService) {
+ this.systemInfoService = systemInfoService;
+ }
+
+ @Override
+ public boolean supportsParameter(MethodParameter parameter) {
+ return parameter.getParameterType().equals(SystemInfo.class);
+ }
+
+ @Override
+ public Object resolveArgument(MethodParameter parameter,
+ ModelAndViewContainer mavContainer,
+ NativeWebRequest webRequest,
+ WebDataBinderFactory binderFactory) throws Exception {
+ return systemInfoService.getSystemInfo();
+ }
+}
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/service/PersonService.java b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/service/PersonService.java
new file mode 100644
index 00000000..3604f7df
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/service/PersonService.java
@@ -0,0 +1,11 @@
+package ru.otus.spring.service;
+
+import ru.otus.spring.domain.Person;
+
+import java.util.List;
+
+public interface PersonService {
+
+ List findAll();
+ Person save(Person person);
+}
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/service/PersonServiceImpl.java b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/service/PersonServiceImpl.java
new file mode 100644
index 00000000..22d99a0b
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/service/PersonServiceImpl.java
@@ -0,0 +1,27 @@
+package ru.otus.spring.service;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import ru.otus.spring.domain.Person;
+import ru.otus.spring.repostory.PersonRepository;
+
+import java.util.List;
+
+@RequiredArgsConstructor
+@Service
+public class PersonServiceImpl implements PersonService {
+
+ private final PersonRepository personRepository;
+
+ @Override
+ public List findAll() {
+ return personRepository.findAll();
+ }
+
+ @Transactional
+ @Override
+ public Person save(Person person) {
+ return personRepository.save(person);
+ }
+}
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/service/SystemInfoService.java b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/service/SystemInfoService.java
new file mode 100644
index 00000000..26fb34d2
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/java/ru/otus/spring/service/SystemInfoService.java
@@ -0,0 +1,17 @@
+package ru.otus.spring.service;
+
+import org.springframework.stereotype.Service;
+import ru.otus.spring.domain.SystemInfo;
+
+@Service
+public class SystemInfoService {
+
+ public SystemInfo getSystemInfo(){
+ String osName = System.getProperty("os.name");
+ String timeZone = System.getProperty("user.timezone");
+ String osArch = System.getProperty("os.arch");
+ int processorsCount = Runtime.getRuntime().availableProcessors();
+ return new SystemInfo(osName, timeZone, osArch, processorsCount);
+
+ }
+}
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/resources/application.yml b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/resources/application.yml
new file mode 100644
index 00000000..02e01231
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/resources/application.yml
@@ -0,0 +1,11 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb
+ initialization-mode: always
+
+ jpa:
+ generate-ddl: false
+ hibernate:
+ ddl-auto: none
+
+ show-sql: true
\ No newline at end of file
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/resources/data.sql b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/resources/data.sql
new file mode 100644
index 00000000..b840826c
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/resources/data.sql
@@ -0,0 +1,2 @@
+INSERT INTO persons (name) VALUES ('Pushkin'), ('Lermontov');
+select * from persons;
\ No newline at end of file
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/resources/schema.sql b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/resources/schema.sql
new file mode 100644
index 00000000..2190e858
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/resources/schema.sql
@@ -0,0 +1,8 @@
+DROP TABLE IF EXISTS persons;
+
+CREATE TABLE persons (
+ id BIGSERIAL,
+ name VARCHAR(250),
+
+ PRIMARY KEY (id)
+);
\ No newline at end of file
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/resources/static/listmark.png b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/resources/static/listmark.png
new file mode 100644
index 00000000..f8eb391b
Binary files /dev/null and b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/resources/static/listmark.png differ
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/resources/templates/add.html b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/resources/templates/add.html
new file mode 100644
index 00000000..11d14e0f
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/resources/templates/add.html
@@ -0,0 +1,75 @@
+
+
+
+
+ Edit person
+
+
+
+
+
+
+Form for new person creation:
+
+
+Saved person:
+
+
+
+
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/main/resources/templates/list.html b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/resources/templates/list.html
new file mode 100644
index 00000000..26a0c8a0
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/main/resources/templates/list.html
@@ -0,0 +1,88 @@
+
+
+
+
+
+ List of all persons
+
+
+
+
+
+
+System Info:
+
+
+Persons:
+
+Add new
+
+
+
+ | ID |
+ Name |
+
+
+
+
+
+
+
+
+
+
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/test/java/ru/otus/spring/rest/PersonControllerTest.java b/2025-11/spring-17-ajax/spring-ajax-demo/src/test/java/ru/otus/spring/rest/PersonControllerTest.java
new file mode 100644
index 00000000..5dc2e9d4
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/test/java/ru/otus/spring/rest/PersonControllerTest.java
@@ -0,0 +1,74 @@
+package ru.otus.spring.rest;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.web.servlet.MockMvc;
+import ru.otus.spring.domain.Person;
+import ru.otus.spring.rest.dto.PersonDto;
+import ru.otus.spring.service.PersonService;
+import ru.otus.spring.service.SystemInfoService;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.springframework.http.MediaType.APPLICATION_JSON;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static ru.otus.spring.rest.GlobalExceptionHandler.ERROR_STRING;
+
+@WebMvcTest(PersonController.class)
+class PersonControllerTest {
+
+ @Autowired
+ private MockMvc mvc;
+
+ @Autowired
+ private ObjectMapper mapper;
+
+ @MockitoBean
+ private PersonService personService;
+
+ @MockitoBean
+ private SystemInfoService systemInfoService;
+
+ @Test
+ void shouldReturnCorrectPersonsList() throws Exception {
+ List persons = List.of(new Person(1, "Person1"), new Person(2, "Person2"));
+ given(personService.findAll()).willReturn(persons);
+
+ List expectedResult = persons.stream()
+ .map(PersonDto::toDto).collect(Collectors.toList());
+
+ mvc.perform(get("/api/persons"))
+ .andExpect(status().isOk())
+ .andExpect(content().json(mapper.writeValueAsString(expectedResult)));
+ }
+
+ @Test
+ void shouldReturnExpectedErrorWhenPersonsNotFound() throws Exception {
+ given(personService.findAll()).willReturn(List.of());
+
+ mvc.perform(get("/api/persons"))
+ .andExpect(status().isNotFound())
+ .andExpect(content().string(ERROR_STRING));
+ }
+
+ @Test
+ void shouldCorrectSaveNewPerson() throws Exception {
+ Person person = new Person(1, "Person1");
+ given(personService.save(any())).willReturn(person);
+ String expectedResult = mapper.writeValueAsString(PersonDto.toDto(person));
+
+ mvc.perform(post("/api/persons").contentType(APPLICATION_JSON)
+ .content(expectedResult))
+ .andExpect(status().isOk())
+ .andExpect(content().json(expectedResult));
+ }
+}
\ No newline at end of file
diff --git a/2025-11/spring-17-ajax/spring-ajax-demo/src/test/java/ru/otus/spring/rest/SystemInfoControllerTest.java b/2025-11/spring-17-ajax/spring-ajax-demo/src/test/java/ru/otus/spring/rest/SystemInfoControllerTest.java
new file mode 100644
index 00000000..2911919a
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-ajax-demo/src/test/java/ru/otus/spring/rest/SystemInfoControllerTest.java
@@ -0,0 +1,39 @@
+package ru.otus.spring.rest;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.web.servlet.MockMvc;
+import ru.otus.spring.domain.SystemInfo;
+import ru.otus.spring.repostory.PersonRepository;
+import ru.otus.spring.service.SystemInfoService;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+
+@WebMvcTest(SystemInfoController.class)
+@Import(SystemInfoService.class)
+class SystemInfoControllerTest {
+
+ @Autowired
+ private MockMvc mvc;
+
+ @Autowired
+ private ObjectMapper mapper;
+
+ @MockitoBean
+ private PersonRepository repository;
+
+ @Autowired
+ private SystemInfoService systemInfoService;
+
+ @Test
+ void shouldReturnCorrectServerSystemInfo() throws Exception {
+ SystemInfo expectedSystemInfo = systemInfoService.getSystemInfo();
+ mvc.perform(get("/api/server/system/info"))
+ .andExpect(content().json(mapper.writeValueAsString(expectedSystemInfo)));
+ }
+}
\ No newline at end of file
diff --git a/2025-11/spring-17-ajax/spring-boot-and-react-demo/.gitignore b/2025-11/spring-17-ajax/spring-boot-and-react-demo/.gitignore
new file mode 100644
index 00000000..b11e399f
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-boot-and-react-demo/.gitignore
@@ -0,0 +1,30 @@
+target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/build/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+
+node/
+/node_modules
+/output
+package-lock.json
diff --git a/2025-11/spring-17-ajax/spring-boot-and-react-demo/package.json b/2025-11/spring-17-ajax/spring-boot-and-react-demo/package.json
new file mode 100644
index 00000000..22d1433b
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-boot-and-react-demo/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "client",
+ "version": "1.0.0",
+ "description": "Simple react demo",
+ "main": "index.js",
+ "scripts": {
+ "dev": "webpack-dev-server --config webpack.dev.config.js",
+ "build": "webpack"
+ },
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.19.1",
+ "babel-loader": "^8.2.5",
+ "@babel/preset-env": "^7.19.1",
+ "@babel/preset-react": "^7.18.6",
+ "react-css-modules": "^4.7.11",
+ "html-webpack-plugin": "^5.5.0",
+ "terser-webpack-plugin": "^5.3.6",
+ "webpack": "^5.74.0",
+ "webpack-cli": "^4.10.0",
+ "webpack-dev-server": "^4.11.1"
+ }
+}
diff --git a/2025-11/spring-17-ajax/spring-boot-and-react-demo/pom.xml b/2025-11/spring-17-ajax/spring-boot-and-react-demo/pom.xml
new file mode 100644
index 00000000..4eb4ea41
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-boot-and-react-demo/pom.xml
@@ -0,0 +1,88 @@
+
+
+ 4.0.0
+
+ ru.otus
+ spring-boot-and-react-demo
+ 1.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.5.6
+
+
+
+
+ 17
+ 17
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ com.github.eirslett
+ frontend-maven-plugin
+ 1.12.1
+
+
+ install node and npm
+
+ install-node-and-npm
+
+
+ v16.13.2
+ 8.3.2
+
+
+
+ npm install
+
+ npm
+
+ generate-resources
+
+ install
+
+
+
+ npm run build
+
+ npm
+
+ generate-resources
+
+ run build
+
+
+
+
+
+
+
diff --git a/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/Main.java b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/Main.java
new file mode 100644
index 00000000..9c34a6f3
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/Main.java
@@ -0,0 +1,15 @@
+package ru.otus.spring;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Main {
+
+ // http://localhost:8080
+ // http://localhost:8080/api/persons
+ public static void main(String[] args) {
+ SpringApplication.run(Main.class);
+ }
+
+}
diff --git a/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/domain/Person.java b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/domain/Person.java
new file mode 100644
index 00000000..81dae69e
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/domain/Person.java
@@ -0,0 +1,40 @@
+package ru.otus.spring.domain;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+@Entity
+@Table(name = "persons")
+public class Person {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private int id;
+ private String name;
+
+ public Person() {
+ }
+
+ public Person(String name) {
+ this.name = name;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/repostory/PersonRepository.java b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/repostory/PersonRepository.java
new file mode 100644
index 00000000..4b20e5b7
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/repostory/PersonRepository.java
@@ -0,0 +1,11 @@
+package ru.otus.spring.repostory;
+
+import org.springframework.data.repository.CrudRepository;
+import ru.otus.spring.domain.Person;
+
+import java.util.List;
+
+public interface PersonRepository extends CrudRepository {
+
+ List findAll();
+}
diff --git a/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/rest/PersonController.java b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/rest/PersonController.java
new file mode 100644
index 00000000..47c36b18
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/rest/PersonController.java
@@ -0,0 +1,25 @@
+package ru.otus.spring.rest;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import ru.otus.spring.repostory.PersonRepository;
+import ru.otus.spring.rest.dto.PersonDto;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RestController
+public class PersonController {
+
+ private final PersonRepository repository;
+
+ public PersonController(PersonRepository repository) {
+ this.repository = repository;
+ }
+
+ @GetMapping("/api/persons")
+ public List getAllPersons() {
+ return repository.findAll().stream().map(PersonDto::toDto)
+ .collect(Collectors.toList());
+ }
+}
diff --git a/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/rest/dto/PersonDto.java b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/rest/dto/PersonDto.java
new file mode 100644
index 00000000..03918447
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/java/ru/otus/spring/rest/dto/PersonDto.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016 Russian Post
+ *
+ * This source code is Russian Post Confidential Proprietary.
+ * This software is protected by copyright. All rights and titles are reserved.
+ * You shall not use, copy, distribute, modify, decompile, disassemble or reverse engineer the software.
+ * Otherwise this violation would be treated by law and would be subject to legal prosecution.
+ * Legal use of the software provides receipt of a license from the right name only.
+ */
+package ru.otus.spring.rest.dto;
+
+import ru.otus.spring.domain.Person;
+
+/**
+ * DTO that represents Account
+ */
+@SuppressWarnings("all")
+public class PersonDto {
+
+ private int id = -1;
+ private String name;
+
+ public PersonDto() {
+ }
+
+ public PersonDto(int id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public static PersonDto toDto(Person person) {
+ return new PersonDto(person.getId(), person.getName());
+ }
+}
diff --git a/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/resources/application.yml b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/resources/application.yml
new file mode 100644
index 00000000..02e01231
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/resources/application.yml
@@ -0,0 +1,11 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb
+ initialization-mode: always
+
+ jpa:
+ generate-ddl: false
+ hibernate:
+ ddl-auto: none
+
+ show-sql: true
\ No newline at end of file
diff --git a/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/resources/data.sql b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/resources/data.sql
new file mode 100644
index 00000000..e60c79ca
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/resources/data.sql
@@ -0,0 +1 @@
+INSERT INTO persons (name) VALUES ('Pushkin'), ('Lermontov')
\ No newline at end of file
diff --git a/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/resources/schema.sql b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/resources/schema.sql
new file mode 100644
index 00000000..2190e858
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/main/resources/schema.sql
@@ -0,0 +1,8 @@
+DROP TABLE IF EXISTS persons;
+
+CREATE TABLE persons (
+ id BIGSERIAL,
+ name VARCHAR(250),
+
+ PRIMARY KEY (id)
+);
\ No newline at end of file
diff --git a/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/ui/components/App.js b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/ui/components/App.js
new file mode 100644
index 00000000..4a638db3
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/ui/components/App.js
@@ -0,0 +1,58 @@
+import React from 'react'
+
+const styles = {
+ personsTable: {
+ border: "1px solid steelblue",
+ width: "300px",
+ borderCollapse: "collapse",
+ },
+
+ personsTableItem: {
+ padding: "5px",
+ border: "1px solid steelblue"
+ }
+}
+
+const Header = (props) => (
+ {props.title}
+);
+
+export default class App extends React.Component {
+
+ constructor() {
+ super();
+ this.state = {persons: []};
+ }
+
+ componentDidMount() {
+ fetch('/api/persons')
+ .then(response => response.json())
+ .then(persons => this.setState({persons}));
+ }
+
+ render() {
+ return (
+
+
+
+
+
+ | ID |
+ Name |
+
+
+
+ {
+ this.state.persons.map((person, i) => (
+
+ | {person.id} |
+ {person.name} |
+
+ ))
+ }
+
+
+
+ )
+ }
+};
diff --git a/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/ui/index.html b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/ui/index.html
new file mode 100644
index 00000000..86fc7182
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/ui/index.html
@@ -0,0 +1,15 @@
+
+
+
+ Minimal React Boilerplate
+
+
+
+
+
+
+
+
+
diff --git a/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/ui/index.js b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/ui/index.js
new file mode 100644
index 00000000..b054022e
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-boot-and-react-demo/src/ui/index.js
@@ -0,0 +1,8 @@
+import React from 'react'
+import ReactDOM from 'react-dom'
+import App from './components/App'
+
+ReactDOM.render(
+ ,
+ document.getElementById('root')
+)
\ No newline at end of file
diff --git a/2025-11/spring-17-ajax/spring-boot-and-react-demo/webpack.config.js b/2025-11/spring-17-ajax/spring-boot-and-react-demo/webpack.config.js
new file mode 100644
index 00000000..a1a380c5
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-boot-and-react-demo/webpack.config.js
@@ -0,0 +1,51 @@
+const TerserPlugin = require("terser-webpack-plugin");
+const HtmlWebpackPlugin = require('html-webpack-plugin')
+const path = require('path');
+const webpack = require('webpack');
+
+module.exports = {
+ entry: './src/ui/index.js',
+ mode: "production",
+ output: {
+ path: path.resolve(__dirname, 'target/classes/public/'),
+ filename: 'bundle.min.js',
+ libraryTarget: 'umd'
+ },
+
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ exclude: /(node_modules|bower_components|build)/,
+ use: {
+ loader: 'babel-loader',
+ options: {
+ presets: ["@babel/preset-env", '@babel/preset-react']
+ }
+ }
+ }
+ ]
+ },
+
+ optimization: {
+ minimize: true,
+ minimizer: [
+ new TerserPlugin({
+ extractComments: true,
+ }),
+ ],
+ },
+
+ plugins: [
+ new webpack.DefinePlugin({
+ "process.env": {
+ NODE_ENV: JSON.stringify("production")
+ }
+ }),
+
+ new HtmlWebpackPlugin({
+ filename: 'index.html',
+ template: 'src/ui/index.html'
+ })
+ ]
+}
diff --git a/2025-11/spring-17-ajax/spring-boot-and-react-demo/webpack.dev.config.js b/2025-11/spring-17-ajax/spring-boot-and-react-demo/webpack.dev.config.js
new file mode 100644
index 00000000..af675edd
--- /dev/null
+++ b/2025-11/spring-17-ajax/spring-boot-and-react-demo/webpack.dev.config.js
@@ -0,0 +1,63 @@
+const HtmlWebpackPlugin = require('html-webpack-plugin')
+const path = require('path');
+
+module.exports = {
+ entry: './src/ui/index.js',
+ devtool: 'inline-source-map',
+ mode: 'development',
+ output: {
+ path: path.resolve(__dirname),
+ filename: 'bundle.js',
+ libraryTarget: 'umd'
+ },
+
+ devServer: {
+ static: path.resolve(__dirname) + '/src/ui',
+ compress: true,
+ port: 9000,
+ host: 'localhost',
+ open: true,
+/*
+ setupMiddlewares: (middlewares, devServer) => {
+ middlewares.unshift({
+ name: 'inital-data-mw',
+ path: '/api/persons',
+ middleware: (req, res) => res.send([
+ {id: '1', name: 'Привяу'}
+ ])
+ });
+ return middlewares;
+ },
+*/
+ proxy: {
+ '*': {
+ target: 'http://localhost:8080',
+ secure: false,
+ changeOrigin: true
+ }
+ }
+
+
+ },
+
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ exclude: /(node_modules|bower_components|build)/,
+ use: {
+ loader: 'babel-loader',
+ options: {
+ presets: ["@babel/preset-env", '@babel/preset-react']
+ }
+ }
+ }
+ ]
+ },
+ plugins: [
+ new HtmlWebpackPlugin({
+ filename: 'index.html',
+ template: 'src/ui/index.html'
+ })
+ ]
+}