Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cba4c19f9f | ||
|
|
ba9e0a908b | ||
|
|
91de0ee0c0 | ||
|
|
3960c0d557 | ||
|
|
ea8b6591c2 | ||
|
|
14ae167d61 | ||
|
|
9dddb11baf | ||
|
|
2c210e6657 | ||
|
|
c7dc979193 | ||
|
|
17014bcb67 | ||
|
|
4a409b63d7 | ||
|
|
12007bd3ea | ||
|
|
743b7c1c6a | ||
|
|
499b51d2e1 | ||
|
|
51875bcc5e | ||
|
|
51545137be | ||
|
|
0dd5c03d7d |
14
.gitignore
vendored
@@ -1,14 +0,0 @@
|
||||
# ---> VisualStudioCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
3
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
9
.idea/internetDev.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/internetDev.iml" filepath="$PROJECT_DIR$/.idea/internetDev.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
BIN
LabWork4Report.docx
Normal file
38
catalog.html
@@ -1,38 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<head>
|
||||
<title>Каталог</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h3>Тут разбита музыка на жанры</h3>
|
||||
<p>Инфа будет +- такая</p>
|
||||
<div class="list">
|
||||
<ul class="punk-list">
|
||||
<li>
|
||||
<div class="item">
|
||||
<img class="catalog" src="pankrock.jpg" alt="Панк-Рок" width=200>
|
||||
<a href="punkrock.html">Панк-Рок</a>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="item">
|
||||
<img class="catalog" src="psy.png" alt="Психоделический рок" width=200>
|
||||
<a href="">Психоделика</a>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="item">
|
||||
<img class="catalog" src="garajnipunk.jpg" alt="Гаражный-панк" width=200>
|
||||
<a href="">Гражный-панк</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<a href="index.html"> Вернуться назад</a>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
3
demo/.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/gradlew text eol=lf
|
||||
*.bat text eol=crlf
|
||||
*.jar binary
|
||||
37
demo/.gitignore
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
HELP.md
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
0
demo/Configure
Normal file
0
demo/Frontend
Normal file
1
demo/Get
Normal file
@@ -0,0 +1 @@
|
||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>⨬<EFBFBD><EFBFBD> <20><>ࠬ<EFBFBD><E0A0AC><EFBFBD>: //help.gradle.org.
|
||||
273
demo/README.md
Normal file
@@ -0,0 +1,273 @@
|
||||
# Инструкция по переносу проекта на новый компьютер
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
### Что работает сразу (без настройки)
|
||||
|
||||
1. **Dev профиль** (H2 база данных) — работает сразу без установки PostgreSQL
|
||||
- Данные хранятся в файле `data.mv.db` (уже есть в проекте)
|
||||
- Не требует установки отдельного сервера БД
|
||||
|
||||
2. **Java 21** — должна быть установлена
|
||||
|
||||
### Что нужно настроить (опционально)
|
||||
|
||||
- **PostgreSQL** — только если нужно запускать с профилем `prod`
|
||||
- **Node.js** — только если нужно собирать фронтенд
|
||||
|
||||
---
|
||||
|
||||
## Пошаговая инструкция
|
||||
|
||||
### Шаг 1: Проверка Java
|
||||
|
||||
Убедитесь, что установлена Java 21:
|
||||
|
||||
```bash
|
||||
java -version
|
||||
```
|
||||
|
||||
Должно быть что-то вроде:
|
||||
```
|
||||
openjdk version "21.x.x"
|
||||
```
|
||||
|
||||
**Если Java нет:**
|
||||
- Скачайте с [Oracle JDK](https://www.oracle.com/java/technologies/downloads/#java21) или [OpenJDK](https://adoptium.net/)
|
||||
- Установите и добавьте в PATH
|
||||
|
||||
---
|
||||
|
||||
### Шаг 2: Запуск проекта (Dev профиль - H2)
|
||||
|
||||
Проект использует Gradle Wrapper, поэтому Gradle не нужно устанавливать отдельно.
|
||||
|
||||
**Windows:**
|
||||
```bash
|
||||
cd demo
|
||||
.\gradlew.bat bootRun
|
||||
```
|
||||
|
||||
**Linux/Mac:**
|
||||
```bash
|
||||
cd demo
|
||||
./gradlew bootRun
|
||||
```
|
||||
|
||||
Профиль `dev` используется по умолчанию. Приложение запустится на порту 8080.
|
||||
|
||||
**Проверка:**
|
||||
- Откройте: http://localhost:8080/swagger-ui.html
|
||||
- Или: http://localhost:8080/api-docs
|
||||
|
||||
---
|
||||
|
||||
### Шаг 3: Настройка PostgreSQL (только для prod профиля)
|
||||
|
||||
Если нужно запускать с профилем `prod`, установите PostgreSQL:
|
||||
|
||||
#### Установка PostgreSQL (Windows)
|
||||
|
||||
1. Скачайте PostgreSQL: https://www.postgresql.org/download/windows/
|
||||
2. Установите PostgreSQL (запомните пароль для пользователя `postgres`)
|
||||
3. Запустите pgAdmin или используйте командную строку
|
||||
|
||||
#### Создание базы данных
|
||||
|
||||
Откройте psql или pgAdmin и выполните:
|
||||
|
||||
```sql
|
||||
CREATE DATABASE demo;
|
||||
```
|
||||
|
||||
Или через командную строку:
|
||||
```bash
|
||||
psql -U postgres
|
||||
CREATE DATABASE demo;
|
||||
\q
|
||||
```
|
||||
|
||||
#### Проверка настроек
|
||||
|
||||
В файле `src/main/resources/application-prod.yml` должны быть настройки:
|
||||
```yaml
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:postgresql://127.0.0.1/demo
|
||||
username: postgres
|
||||
password: postgres # ← Укажите ваш пароль PostgreSQL
|
||||
```
|
||||
|
||||
**⚠️ Важно:** Если пароль PostgreSQL отличается от `postgres`, измените его в файле!
|
||||
|
||||
#### Запуск с prod профилем
|
||||
|
||||
**Windows:**
|
||||
```bash
|
||||
.\gradlew.bat bootRun -Pprod
|
||||
```
|
||||
|
||||
**Linux/Mac:**
|
||||
```bash
|
||||
./gradlew bootRun -Pprod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Шаг 4: Миграции базы данных
|
||||
|
||||
Проект использует Liquibase для миграций. Миграции запускаются автоматически при старте приложения.
|
||||
|
||||
**Файлы миграций находятся в:**
|
||||
- `src/main/resources/db/changes/`
|
||||
|
||||
При первом запуске Liquibase:
|
||||
- Создаст все таблицы
|
||||
- Заполнит начальные данные (эпохи, страны)
|
||||
|
||||
**Если нужно сбросить БД и начать заново:**
|
||||
- Для H2: удалите файл `data.mv.db` и перезапустите приложение
|
||||
- Для PostgreSQL: удалите и создайте базу заново
|
||||
|
||||
---
|
||||
|
||||
## Полезные команды
|
||||
|
||||
### Запуск приложения
|
||||
|
||||
```bash
|
||||
# Dev профиль (H2) - по умолчанию
|
||||
.\gradlew.bat bootRun
|
||||
|
||||
# Prod профиль (PostgreSQL)
|
||||
.\gradlew.bat bootRun -Pprod
|
||||
|
||||
# С фронтендом
|
||||
.\gradlew.bat bootRun -Pfront
|
||||
```
|
||||
|
||||
### Сборка проекта
|
||||
|
||||
```bash
|
||||
# Собрать JAR файл
|
||||
.\gradlew.bat bootJar
|
||||
|
||||
# Очистить и собрать
|
||||
.\gradlew.bat clean build
|
||||
```
|
||||
|
||||
### Запуск тестов
|
||||
|
||||
```bash
|
||||
# Все тесты (dev профиль)
|
||||
.\gradlew.bat test
|
||||
|
||||
# Тесты с prod профилем
|
||||
.\gradlew.bat test -Pprod
|
||||
```
|
||||
|
||||
### Просмотр H2 базы данных (dev профиль)
|
||||
|
||||
После запуска приложения с dev профилем:
|
||||
1. Откройте: http://localhost:8080/h2-console
|
||||
2. JDBC URL: `jdbc:h2:file:./data`
|
||||
3. Username: `sa`
|
||||
4. Password: `sa`
|
||||
5. Нажмите "Connect"
|
||||
|
||||
---
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
demo/
|
||||
├── src/
|
||||
│ ├── main/
|
||||
│ │ ├── java/ # Java код
|
||||
│ │ └── resources/
|
||||
│ │ ├── application.yml # Основная конфигурация
|
||||
│ │ ├── application-dev.yml # Dev профиль (H2)
|
||||
│ │ ├── application-prod.yml # Prod профиль (PostgreSQL)
|
||||
│ │ └── db/
|
||||
│ │ ├── master.yml # Liquibase master файл
|
||||
│ │ └── changes/ # Файлы миграций
|
||||
│ └── test/ # Тесты
|
||||
├── data.mv.db # H2 база данных (dev)
|
||||
├── build.gradle # Конфигурация Gradle
|
||||
├── gradlew # Gradle Wrapper (Linux/Mac)
|
||||
└── gradlew.bat # Gradle Wrapper (Windows)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Частые проблемы
|
||||
|
||||
### Ошибка: "Could not find or load main class"
|
||||
|
||||
**Решение:** Выполните:
|
||||
```bash
|
||||
.\gradlew.bat clean build
|
||||
```
|
||||
|
||||
### Ошибка подключения к PostgreSQL
|
||||
|
||||
**Проверьте:**
|
||||
1. PostgreSQL запущен
|
||||
2. База данных `demo` создана
|
||||
3. Пароль в `application-prod.yml` правильный
|
||||
4. Порт 5432 не заблокирован
|
||||
|
||||
**Проверка подключения:**
|
||||
```bash
|
||||
psql -U postgres -d demo
|
||||
```
|
||||
|
||||
### Порт 8080 уже занят
|
||||
|
||||
Измените порт в `src/main/resources/application.yml`:
|
||||
```yaml
|
||||
server:
|
||||
port: 8081 # или другой свободный порт
|
||||
```
|
||||
|
||||
### Ошибка миграций Liquibase
|
||||
|
||||
**Для dev профиля (H2):**
|
||||
- Удалите файл `data.mv.db` и перезапустите
|
||||
|
||||
**Для prod профиля (PostgreSQL):**
|
||||
- Удалите базу данных и создайте заново
|
||||
- Или проверьте логи для конкретной ошибки
|
||||
|
||||
---
|
||||
|
||||
## Что перенеслось автоматически
|
||||
|
||||
✅ Код приложения
|
||||
✅ Конфигурация Gradle
|
||||
✅ Миграции Liquibase
|
||||
✅ Данные H2 (файл `data.mv.db`)
|
||||
✅ Статические файлы фронтенда
|
||||
|
||||
❌ PostgreSQL база данных (нужно установить и настроить отдельно)
|
||||
|
||||
---
|
||||
|
||||
## Контакты и документация
|
||||
|
||||
- Swagger UI: http://localhost:8080/swagger-ui.html
|
||||
- API Docs: http://localhost:8080/api-docs
|
||||
- H2 Console (dev): http://localhost:8080/h2-console
|
||||
|
||||
---
|
||||
|
||||
## Следующие шаги
|
||||
|
||||
1. ✅ Убедитесь, что Java 21 установлена
|
||||
2. ✅ Запустите проект с dev профилем
|
||||
3. ✅ Проверьте Swagger UI
|
||||
4. ⏭️ (Опционально) Установите PostgreSQL для prod профиля
|
||||
5. ⏭️ (Опционально) Настройте фронтенд, если нужно
|
||||
|
||||
Если что-то не работает, проверьте логи в консоли или файле логов.
|
||||
|
||||
50
demo/build.front.gradle
Normal file
@@ -0,0 +1,50 @@
|
||||
apply plugin: "com.github.node-gradle.node"
|
||||
|
||||
logger.quiet("Configure front builder")
|
||||
|
||||
ext {
|
||||
frontDir = file("${project.projectDir}/../punkrock-react")
|
||||
staticDir = file("${project.projectDir}/src/main/resources/static")
|
||||
if (!frontDir.exists()) {
|
||||
throw new GradleException("Frontend app directory is not exists")
|
||||
}
|
||||
logger.quiet("Webapp dir is {}", frontDir.toString())
|
||||
}
|
||||
|
||||
node {
|
||||
version = "22.17.1"
|
||||
npmVersion = "10.9.2"
|
||||
download = true
|
||||
}
|
||||
|
||||
tasks.register("frontDepsInstall", NpmTask) {
|
||||
group = "front"
|
||||
description = "Installs dependencies from package.json"
|
||||
logger.quiet(description)
|
||||
workingDir = frontDir
|
||||
args = ["install"]
|
||||
}
|
||||
|
||||
tasks.register("frontBuild", NpmTask) {
|
||||
group = "front"
|
||||
description = "Build frontend webapp"
|
||||
logger.quiet(description)
|
||||
workingDir = frontDir
|
||||
dependsOn frontDepsInstall
|
||||
args = ["run", "build"]
|
||||
}
|
||||
|
||||
tasks.register("copyFrontend", org.gradle.api.tasks.Copy) {
|
||||
group = "front"
|
||||
description = "Copy built frontend to static resources"
|
||||
dependsOn frontBuild
|
||||
from("${frontDir}/build")
|
||||
into(staticDir)
|
||||
includeEmptyDirs = false
|
||||
}
|
||||
|
||||
if (frontDir.exists()) {
|
||||
processResources.dependsOn copyFrontend
|
||||
bootJar.dependsOn copyFrontend
|
||||
}
|
||||
|
||||
91
demo/build.gradle
Normal file
@@ -0,0 +1,91 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '3.2.5'
|
||||
id 'io.spring.dependency-management' version '1.1.5'
|
||||
id 'com.github.node-gradle.node' version '7.1.0' apply false
|
||||
id 'org.liquibase.gradle' version '2.2.2' apply false
|
||||
}
|
||||
|
||||
group = 'com.example'
|
||||
version = '0.0.1-SNAPSHOT'
|
||||
description = 'Demo project for Spring Boot'
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
ext {
|
||||
springdocVersion = "2.2.0"
|
||||
h2Version = "2.4.240"
|
||||
postgresVersion = "42.7.8"
|
||||
liquibaseVersion = "4.33.0"
|
||||
|
||||
springProfiles = []
|
||||
if (project.hasProperty("front")) {
|
||||
springProfiles.add("front")
|
||||
}
|
||||
if (project.hasProperty("prod")) {
|
||||
springProfiles.add("prod")
|
||||
} else {
|
||||
springProfiles.add("dev")
|
||||
}
|
||||
currentProfiles = springProfiles.join(",")
|
||||
logger.quiet("Current profiles are: " + currentProfiles)
|
||||
}
|
||||
|
||||
if (springProfiles.contains("front")) {
|
||||
apply from: "build.front.gradle"
|
||||
}
|
||||
if (springProfiles.contains("dev")) {
|
||||
apply from: "build.migrations.gradle"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springdocVersion}"
|
||||
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
runtimeOnly "org.liquibase:liquibase-core:${liquibaseVersion}"
|
||||
|
||||
if (springProfiles.contains("prod")) {
|
||||
runtimeOnly "org.postgresql:postgresql:${postgresVersion}"
|
||||
} else {
|
||||
runtimeOnly "org.postgresql:postgresql:${postgresVersion}"
|
||||
runtimeOnly "com.h2database:h2:${h2Version}"
|
||||
}
|
||||
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
bootRun {
|
||||
def currentArgs = ["--spring.profiles.active=" + currentProfiles]
|
||||
if (project.hasProperty("args")) {
|
||||
currentArgs.addAll(project.args.split(","))
|
||||
}
|
||||
args currentArgs
|
||||
}
|
||||
|
||||
bootJar {
|
||||
archiveFileName = String.format("%s-%s.jar", rootProject.name, version)
|
||||
}
|
||||
|
||||
test {
|
||||
systemProperty "spring.profiles.active", currentProfiles
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
processResources {
|
||||
filesMatching("**/application.yml") {
|
||||
filter { line ->
|
||||
line.replace("active: dev", "active: ${currentProfiles}")
|
||||
}
|
||||
}
|
||||
}
|
||||
67
demo/build.migrations.gradle
Normal file
@@ -0,0 +1,67 @@
|
||||
apply plugin: "org.liquibase.gradle"
|
||||
|
||||
logger.quiet("Configure migrations generator")
|
||||
|
||||
ext {
|
||||
picocliVersion = "4.7.7"
|
||||
timestamp = new Date().format("yyyy-MM-dd-HHmmss")
|
||||
}
|
||||
|
||||
liquibase {
|
||||
activities {
|
||||
main {
|
||||
changelogFile "db/master.yml"
|
||||
url "jdbc:h2:file:./data"
|
||||
username "sa"
|
||||
password "sa"
|
||||
referenceUrl "hibernate:spring:com.example.demo.entity?dialect=org.hibernate.dialect.H2Dialect"
|
||||
logLevel "warn"
|
||||
}
|
||||
}
|
||||
}
|
||||
update.dependsOn processResources
|
||||
|
||||
dependencies {
|
||||
liquibaseRuntime "org.liquibase.ext:liquibase-hibernate6:${liquibaseVersion}"
|
||||
liquibaseRuntime "info.picocli:picocli:${picocliVersion}"
|
||||
liquibaseRuntime sourceSets.main.runtimeClasspath
|
||||
liquibaseRuntime sourceSets.main.output
|
||||
}
|
||||
|
||||
tasks.register("generateFull") {
|
||||
group = "migrations"
|
||||
description = "Generate changelog from existing database"
|
||||
doFirst(){
|
||||
liquibase {
|
||||
activities {
|
||||
main {
|
||||
changeLogFile "src/main/resources/db/generated-full-${timestamp}.yml"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finalizedBy generateChangelog
|
||||
}
|
||||
|
||||
tasks.register("generateDiff") {
|
||||
group = "liquibase"
|
||||
description = "Generate diff between DB and JPA entities"
|
||||
doFirst(){
|
||||
liquibase {
|
||||
activities {
|
||||
main {
|
||||
changeLogFile "src/main/resources/db/generated-diff-${timestamp}.yml"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finalizedBy diffChangelog
|
||||
}
|
||||
diffChangelog.dependsOn compileJava
|
||||
|
||||
tasks.register("clearCheckSums") {
|
||||
group = "migrations"
|
||||
description = "Clear checksums in database (use when changeset was modified after being applied)"
|
||||
finalizedBy clearChecksums
|
||||
}
|
||||
|
||||
BIN
demo/data.mv.db
Normal file
8
demo/data.trace.db
Normal file
@@ -0,0 +1,8 @@
|
||||
2025-11-29 03:33:06.457278+04:00 jdbc[3]: exception
|
||||
org.h2.jdbc.JdbcSQLSyntaxErrorException: Таблица "DATABASECHANGELOGLOCK" не найдена
|
||||
Table "DATABASECHANGELOGLOCK" not found; SQL statement:
|
||||
SELECT COUNT(*) FROM PUBLIC.DATABASECHANGELOGLOCK [42102-240]
|
||||
2025-11-29 09:45:14.491494+04:00 jdbc[13]: exception
|
||||
org.h2.jdbc.JdbcSQLSyntaxErrorException: Синтаксическая ошибка в выражении SQL "SELECT * FROM DATABASECHANGELOG ARTISTS [*]DATABASECHANGELOG"
|
||||
Syntax error in SQL statement "SELECT * FROM DATABASECHANGELOG ARTISTS [*]DATABASECHANGELOG"; SQL statement:
|
||||
SELECT * FROM DATABASECHANGELOG ARTISTS DATABASECHANGELOG [42000-240]
|
||||
BIN
demo/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
7
demo/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
0
demo/gradlew
vendored
Normal file
94
demo/gradlew.bat
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
6
demo/package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "demo",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
1
demo/settings.gradle
Normal file
@@ -0,0 +1 @@
|
||||
rootProject.name = 'demo'
|
||||
13
demo/src/main/java/com/example/demo/DemoApplication.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.example.demo;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class DemoApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(DemoApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
15
demo/src/main/java/com/example/demo/api/PageHelper.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.example.demo.api;
|
||||
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
public class PageHelper {
|
||||
private PageHelper() {
|
||||
}
|
||||
|
||||
public static Pageable toPageable(int page, int size) {
|
||||
return PageRequest.of(page - 1, size, Sort.by("id"));
|
||||
}
|
||||
}
|
||||
|
||||
40
demo/src/main/java/com/example/demo/api/PageRs.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package com.example.demo.api;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
|
||||
public record PageRs<D>(
|
||||
List<D> items,
|
||||
int itemsCount,
|
||||
int currentPage,
|
||||
int currentSize,
|
||||
int totalPages,
|
||||
long totalItems,
|
||||
boolean isFirst,
|
||||
boolean isLast,
|
||||
boolean hasNext,
|
||||
boolean hasPrevious) {
|
||||
|
||||
public List<D> items() {
|
||||
return Optional.ofNullable(items).orElse(Collections.emptyList());
|
||||
}
|
||||
|
||||
public static <D, E> PageRs<D> from(Page<E> page, Function<E, D> mapper) {
|
||||
return new PageRs<>(
|
||||
page.getContent().stream().map(mapper::apply).toList(),
|
||||
page.getNumberOfElements(),
|
||||
page.getNumber() + 1,
|
||||
page.getSize(),
|
||||
page.getTotalPages(),
|
||||
page.getTotalElements(),
|
||||
page.isFirst(),
|
||||
page.isLast(),
|
||||
page.hasNext(),
|
||||
page.hasPrevious());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.example.demo.config;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class OpenApiConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("Punk Rock API")
|
||||
.version("1.0.0")
|
||||
.description("REST API для управления данными о панк-рок исполнителях")
|
||||
.contact(new Contact()
|
||||
.name("Developer")
|
||||
.email("developer@example.com")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.example.demo.configuration;
|
||||
|
||||
public class Constants {
|
||||
public static final String DEV_ORIGIN = "http://localhost:5173";
|
||||
public static final String API_URL = "/api";
|
||||
|
||||
private Constants() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.demo.configuration;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfiguration implements WebMvcConfigurer {
|
||||
@Override
|
||||
public void addCorsMappings(@NonNull CorsRegistry registry) {
|
||||
registry
|
||||
.addMapping(Constants.API_URL + "/**")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedOrigins("http://localhost:3000", "http://localhost:5173", Constants.DEV_ORIGIN)
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import com.example.demo.api.PageHelper;
|
||||
import com.example.demo.api.PageRs;
|
||||
import com.example.demo.configuration.Constants;
|
||||
import com.example.demo.dto.ArtistRq;
|
||||
import com.example.demo.dto.ArtistRs;
|
||||
import com.example.demo.service.ArtistService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(Constants.API_URL + ArtistController.URL)
|
||||
public class ArtistController {
|
||||
public static final String URL = "/artists";
|
||||
private final ArtistService artistService;
|
||||
|
||||
public ArtistController(ArtistService artistService) {
|
||||
this.artistService = artistService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public PageRs<ArtistRs> getAll(
|
||||
@RequestParam(defaultValue = "1") @Min(1) int page,
|
||||
@RequestParam(defaultValue = "3") @Min(1) int size) {
|
||||
return artistService.getAll(PageHelper.toPageable(page, size));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ArtistRs get(@PathVariable Long id) {
|
||||
return artistService.get(id);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ArtistRs create(@RequestBody @Valid ArtistRq dto) {
|
||||
return artistService.create(dto);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ArtistRs update(@PathVariable Long id, @RequestBody @Valid ArtistRq dto) {
|
||||
return artistService.update(id, dto);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ArtistRs delete(@PathVariable Long id) {
|
||||
return artistService.delete(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import com.example.demo.configuration.Constants;
|
||||
import com.example.demo.dto.CountryRq;
|
||||
import com.example.demo.dto.CountryRs;
|
||||
import com.example.demo.service.CountryService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(Constants.API_URL + CountryController.URL)
|
||||
public class CountryController {
|
||||
public static final String URL = "/countries";
|
||||
private final CountryService countryService;
|
||||
|
||||
public CountryController(CountryService countryService) {
|
||||
this.countryService = countryService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<CountryRs> getAll() {
|
||||
return countryService.getAll();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public CountryRs get(@PathVariable Long id) {
|
||||
return countryService.get(id);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public CountryRs create(@RequestBody @Valid CountryRq dto) {
|
||||
return countryService.create(dto);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public CountryRs update(@PathVariable Long id, @RequestBody @Valid CountryRq dto) {
|
||||
return countryService.update(id, dto);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public CountryRs delete(@PathVariable Long id) {
|
||||
return countryService.delete(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import com.example.demo.configuration.Constants;
|
||||
import com.example.demo.dto.EpochRq;
|
||||
import com.example.demo.dto.EpochRs;
|
||||
import com.example.demo.service.EpochService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(Constants.API_URL + EpochController.URL)
|
||||
public class EpochController {
|
||||
public static final String URL = "/epochs";
|
||||
private final EpochService epochService;
|
||||
|
||||
public EpochController(EpochService epochService) {
|
||||
this.epochService = epochService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<EpochRs> getAll() {
|
||||
return epochService.getAll();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public EpochRs get(@PathVariable Long id) {
|
||||
return epochService.get(id);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public EpochRs create(@RequestBody @Valid EpochRq dto) {
|
||||
return epochService.create(dto);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public EpochRs update(@PathVariable Long id, @RequestBody @Valid EpochRq dto) {
|
||||
return epochService.update(id, dto);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public EpochRs delete(@PathVariable Long id) {
|
||||
return epochService.delete(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
|
||||
@Controller
|
||||
public class SpaController {
|
||||
@GetMapping(value = "/{path:^(?!api|assets|images|swagger-ui|.*\\.[a-zA-Z0-9]{2,10}).*}/**")
|
||||
public String forwardToIndex(@PathVariable(required = false) String path) {
|
||||
return "forward:/index.html";
|
||||
}
|
||||
}
|
||||
|
||||
59
demo/src/main/java/com/example/demo/dto/ArtistDto.java
Normal file
@@ -0,0 +1,59 @@
|
||||
package com.example.demo.dto;
|
||||
|
||||
public class ArtistDto {
|
||||
private Integer id;
|
||||
private String name;
|
||||
private String description;
|
||||
private Integer epochId;
|
||||
private Integer countryId;
|
||||
|
||||
public ArtistDto() {}
|
||||
|
||||
public ArtistDto(Integer id, String name, String description, Integer epochId, Integer countryId) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.epochId = epochId;
|
||||
this.countryId = countryId;
|
||||
}
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public Integer getEpochId() {
|
||||
return epochId;
|
||||
}
|
||||
|
||||
public void setEpochId(Integer epochId) {
|
||||
this.epochId = epochId;
|
||||
}
|
||||
|
||||
public Integer getCountryId() {
|
||||
return countryId;
|
||||
}
|
||||
|
||||
public void setCountryId(Integer countryId) {
|
||||
this.countryId = countryId;
|
||||
}
|
||||
}
|
||||
47
demo/src/main/java/com/example/demo/dto/ArtistRq.java
Normal file
@@ -0,0 +1,47 @@
|
||||
package com.example.demo.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public class ArtistRq {
|
||||
@NotBlank
|
||||
private String name;
|
||||
private String description;
|
||||
@NotNull
|
||||
private Long epochId;
|
||||
@NotNull
|
||||
private Long countryId;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public Long getEpochId() {
|
||||
return epochId;
|
||||
}
|
||||
|
||||
public void setEpochId(Long epochId) {
|
||||
this.epochId = epochId;
|
||||
}
|
||||
|
||||
public Long getCountryId() {
|
||||
return countryId;
|
||||
}
|
||||
|
||||
public void setCountryId(Long countryId) {
|
||||
this.countryId = countryId;
|
||||
}
|
||||
}
|
||||
|
||||
50
demo/src/main/java/com/example/demo/dto/ArtistRs.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package com.example.demo.dto;
|
||||
|
||||
public class ArtistRs {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String description;
|
||||
private EpochRs epoch;
|
||||
private CountryRs country;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public EpochRs getEpoch() {
|
||||
return epoch;
|
||||
}
|
||||
|
||||
public void setEpoch(EpochRs epoch) {
|
||||
this.epoch = epoch;
|
||||
}
|
||||
|
||||
public CountryRs getCountry() {
|
||||
return country;
|
||||
}
|
||||
|
||||
public void setCountry(CountryRs country) {
|
||||
this.country = country;
|
||||
}
|
||||
}
|
||||
|
||||
29
demo/src/main/java/com/example/demo/dto/CountryDto.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.example.demo.dto;
|
||||
|
||||
public class CountryDto {
|
||||
private Integer id;
|
||||
private String name;
|
||||
|
||||
public CountryDto() {}
|
||||
|
||||
public CountryDto(Integer id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
17
demo/src/main/java/com/example/demo/dto/CountryRq.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.example.demo.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public class CountryRq {
|
||||
@NotBlank
|
||||
private String name;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
23
demo/src/main/java/com/example/demo/dto/CountryRs.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.example.demo.dto;
|
||||
|
||||
public class CountryRs {
|
||||
private Long id;
|
||||
private String name;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
29
demo/src/main/java/com/example/demo/dto/EpochDto.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.example.demo.dto;
|
||||
|
||||
public class EpochDto {
|
||||
private Integer id;
|
||||
private String name;
|
||||
|
||||
public EpochDto() {}
|
||||
|
||||
public EpochDto(Integer id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
17
demo/src/main/java/com/example/demo/dto/EpochRq.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.example.demo.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public class EpochRq {
|
||||
@NotBlank
|
||||
private String name;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
23
demo/src/main/java/com/example/demo/dto/EpochRs.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.example.demo.dto;
|
||||
|
||||
public class EpochRs {
|
||||
private Long id;
|
||||
private String name;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
92
demo/src/main/java/com/example/demo/entity/ArtistEntity.java
Normal file
@@ -0,0 +1,92 @@
|
||||
package com.example.demo.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "artists")
|
||||
public class ArtistEntity extends BaseEntity {
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(columnDefinition = "text")
|
||||
private String description;
|
||||
|
||||
@JoinColumn(name = "epoch_id", nullable = false)
|
||||
@ManyToOne
|
||||
private EpochEntity epoch;
|
||||
|
||||
@JoinColumn(name = "country_id", nullable = false)
|
||||
@ManyToOne
|
||||
private CountryEntity country;
|
||||
|
||||
public ArtistEntity() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ArtistEntity(String name, String description, EpochEntity epoch, CountryEntity country) {
|
||||
this();
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
setEpoch(epoch);
|
||||
setCountry(country);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public EpochEntity getEpoch() {
|
||||
return epoch;
|
||||
}
|
||||
|
||||
public void setEpoch(EpochEntity epoch) {
|
||||
if (this.epoch == epoch) {
|
||||
return;
|
||||
}
|
||||
if (this.epoch != null) {
|
||||
EpochEntity oldEpoch = this.epoch;
|
||||
this.epoch = null;
|
||||
oldEpoch.removeArtist(this);
|
||||
}
|
||||
this.epoch = epoch;
|
||||
if (epoch != null) {
|
||||
epoch.addArtist(this);
|
||||
}
|
||||
}
|
||||
|
||||
public CountryEntity getCountry() {
|
||||
return country;
|
||||
}
|
||||
|
||||
public void setCountry(CountryEntity country) {
|
||||
if (this.country == country) {
|
||||
return;
|
||||
}
|
||||
if (this.country != null) {
|
||||
CountryEntity oldCountry = this.country;
|
||||
this.country = null;
|
||||
oldCountry.removeArtist(this);
|
||||
}
|
||||
this.country = country;
|
||||
if (country != null) {
|
||||
country.addArtist(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
27
demo/src/main/java/com/example/demo/entity/BaseEntity.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.example.demo.entity;
|
||||
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
import jakarta.persistence.SequenceGenerator;
|
||||
|
||||
@MappedSuperclass
|
||||
public abstract class BaseEntity {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "hibernate_sequence")
|
||||
@SequenceGenerator(name = "hibernate_sequence", sequenceName = "hibernate_sequence", allocationSize = 50)
|
||||
protected Long id;
|
||||
|
||||
protected BaseEntity() {
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.example.demo.entity;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.OrderBy;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "countries")
|
||||
public class CountryEntity extends BaseEntity {
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@OneToMany(mappedBy = "country")
|
||||
@OrderBy("id ASC")
|
||||
private Set<ArtistEntity> artists = new HashSet<>();
|
||||
|
||||
public CountryEntity() {
|
||||
super();
|
||||
}
|
||||
|
||||
public CountryEntity(String name) {
|
||||
this();
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Set<ArtistEntity> getArtists() {
|
||||
return artists;
|
||||
}
|
||||
|
||||
public void addArtist(ArtistEntity artist) {
|
||||
if (!artists.contains(artist)) {
|
||||
artists.add(artist);
|
||||
if (artist.getCountry() != this) {
|
||||
artist.setCountry(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeArtist(ArtistEntity artist) {
|
||||
if (artists.contains(artist)) {
|
||||
artists.remove(artist);
|
||||
if (artist.getCountry() == this) {
|
||||
artist.setCountry(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
61
demo/src/main/java/com/example/demo/entity/EpochEntity.java
Normal file
@@ -0,0 +1,61 @@
|
||||
package com.example.demo.entity;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.OrderBy;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "epochs")
|
||||
public class EpochEntity extends BaseEntity {
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@OneToMany(mappedBy = "epoch")
|
||||
@OrderBy("id ASC")
|
||||
private Set<ArtistEntity> artists = new HashSet<>();
|
||||
|
||||
public EpochEntity() {
|
||||
super();
|
||||
}
|
||||
|
||||
public EpochEntity(String name) {
|
||||
this();
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Set<ArtistEntity> getArtists() {
|
||||
return artists;
|
||||
}
|
||||
|
||||
public void addArtist(ArtistEntity artist) {
|
||||
if (!artists.contains(artist)) {
|
||||
artists.add(artist);
|
||||
if (artist.getEpoch() != this) {
|
||||
artist.setEpoch(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeArtist(ArtistEntity artist) {
|
||||
if (artists.contains(artist)) {
|
||||
artists.remove(artist);
|
||||
if (artist.getEpoch() == this) {
|
||||
artist.setEpoch(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.example.demo.entity.projection;
|
||||
|
||||
public interface ArtistStatsProjection {
|
||||
Long getTotalArtists();
|
||||
Long getArtistsWithDescription();
|
||||
Long getArtistsWithoutDescription();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.demo.entity.projection;
|
||||
|
||||
import com.example.demo.entity.CountryEntity;
|
||||
|
||||
public interface CountryStatsProjection {
|
||||
CountryEntity getCountry();
|
||||
Long getArtistsCount();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.demo.entity.projection;
|
||||
|
||||
import com.example.demo.entity.EpochEntity;
|
||||
|
||||
public interface EpochStatsProjection {
|
||||
EpochEntity getEpoch();
|
||||
Long getArtistsCount();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.example.demo.error;
|
||||
|
||||
public class NotFoundException extends RuntimeException {
|
||||
public <T> NotFoundException(Class<T> entClass, Long id) {
|
||||
super(String.format("%s with id %s is not found", entClass.getSimpleName(), id));
|
||||
}
|
||||
}
|
||||
|
||||
51
demo/src/main/java/com/example/demo/mapper/ArtistMapper.java
Normal file
@@ -0,0 +1,51 @@
|
||||
package com.example.demo.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.example.demo.dto.ArtistRq;
|
||||
import com.example.demo.dto.ArtistRs;
|
||||
import com.example.demo.entity.ArtistEntity;
|
||||
|
||||
@Component
|
||||
public class ArtistMapper {
|
||||
private final EpochMapper epochMapper;
|
||||
private final CountryMapper countryMapper;
|
||||
|
||||
public ArtistMapper(EpochMapper epochMapper, CountryMapper countryMapper) {
|
||||
this.epochMapper = epochMapper;
|
||||
this.countryMapper = countryMapper;
|
||||
}
|
||||
|
||||
public ArtistRq toRqDto(String name, String description, Long epochId, Long countryId) {
|
||||
final ArtistRq dto = new ArtistRq();
|
||||
dto.setName(name);
|
||||
dto.setDescription(description);
|
||||
dto.setEpochId(epochId);
|
||||
dto.setCountryId(countryId);
|
||||
return dto;
|
||||
}
|
||||
|
||||
public ArtistRs toRsDto(ArtistEntity entity) {
|
||||
final ArtistRs dto = new ArtistRs();
|
||||
dto.setId(entity.getId());
|
||||
dto.setName(entity.getName());
|
||||
dto.setDescription(entity.getDescription());
|
||||
if (entity.getEpoch() != null) {
|
||||
dto.setEpoch(epochMapper.toRsDto(entity.getEpoch()));
|
||||
}
|
||||
if (entity.getCountry() != null) {
|
||||
dto.setCountry(countryMapper.toRsDto(entity.getCountry()));
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
|
||||
public List<ArtistRs> toRsDtoList(Iterable<ArtistEntity> entities) {
|
||||
return StreamSupport.stream(entities.spliterator(), false)
|
||||
.map(this::toRsDto)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.example.demo.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.example.demo.dto.CountryRq;
|
||||
import com.example.demo.dto.CountryRs;
|
||||
import com.example.demo.entity.CountryEntity;
|
||||
|
||||
@Component
|
||||
public class CountryMapper {
|
||||
public CountryRq toRqDto(String name) {
|
||||
final CountryRq dto = new CountryRq();
|
||||
dto.setName(name);
|
||||
return dto;
|
||||
}
|
||||
|
||||
public CountryRs toRsDto(CountryEntity entity) {
|
||||
final CountryRs dto = new CountryRs();
|
||||
dto.setId(entity.getId());
|
||||
dto.setName(entity.getName());
|
||||
return dto;
|
||||
}
|
||||
|
||||
public List<CountryRs> toRsDtoList(Iterable<CountryEntity> entities) {
|
||||
return StreamSupport.stream(entities.spliterator(), false)
|
||||
.map(this::toRsDto)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
33
demo/src/main/java/com/example/demo/mapper/EpochMapper.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.example.demo.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.example.demo.dto.EpochRq;
|
||||
import com.example.demo.dto.EpochRs;
|
||||
import com.example.demo.entity.EpochEntity;
|
||||
|
||||
@Component
|
||||
public class EpochMapper {
|
||||
public EpochRq toRqDto(String name) {
|
||||
final EpochRq dto = new EpochRq();
|
||||
dto.setName(name);
|
||||
return dto;
|
||||
}
|
||||
|
||||
public EpochRs toRsDto(EpochEntity entity) {
|
||||
final EpochRs dto = new EpochRs();
|
||||
dto.setId(entity.getId());
|
||||
dto.setName(entity.getName());
|
||||
return dto;
|
||||
}
|
||||
|
||||
public List<EpochRs> toRsDtoList(Iterable<EpochEntity> entities) {
|
||||
return StreamSupport.stream(entities.spliterator(), false)
|
||||
.map(this::toRsDto)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.example.demo.repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import com.example.demo.entity.ArtistEntity;
|
||||
import com.example.demo.entity.projection.ArtistStatsProjection;
|
||||
|
||||
public interface ArtistRepository extends JpaRepository<ArtistEntity, Long> {
|
||||
List<ArtistEntity> findByEpochId(Long epochId);
|
||||
|
||||
List<ArtistEntity> findByCountryId(Long countryId);
|
||||
|
||||
List<ArtistEntity> findByEpochIdAndCountryId(Long epochId, Long countryId);
|
||||
|
||||
@Query("select count(a) as totalArtists, " +
|
||||
"sum(case when a.description is not null and length(a.description) > 0 then 1 else 0 end) as artistsWithDescription, " +
|
||||
"sum(case when a.description is null or length(a.description) = 0 then 1 else 0 end) as artistsWithoutDescription " +
|
||||
"from ArtistEntity a")
|
||||
ArtistStatsProjection getArtistsStatistics();
|
||||
|
||||
@Query("select count(a) as totalArtists, " +
|
||||
"sum(case when a.description is not null and length(a.description) > 0 then 1 else 0 end) as artistsWithDescription, " +
|
||||
"sum(case when a.description is null or length(a.description) = 0 then 1 else 0 end) as artistsWithoutDescription " +
|
||||
"from ArtistEntity a " +
|
||||
"where a.epoch.id = :epochId")
|
||||
ArtistStatsProjection getArtistsStatisticsByEpoch(@Param("epochId") Long epochId);
|
||||
|
||||
@Query("select count(a) as totalArtists, " +
|
||||
"sum(case when a.description is not null and length(a.description) > 0 then 1 else 0 end) as artistsWithDescription, " +
|
||||
"sum(case when a.description is null or length(a.description) = 0 then 1 else 0 end) as artistsWithoutDescription " +
|
||||
"from ArtistEntity a " +
|
||||
"where a.country.id = :countryId")
|
||||
ArtistStatsProjection getArtistsStatisticsByCountry(@Param("countryId") Long countryId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.example.demo.repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import com.example.demo.entity.CountryEntity;
|
||||
import com.example.demo.entity.projection.CountryStatsProjection;
|
||||
|
||||
public interface CountryRepository extends JpaRepository<CountryEntity, Long> {
|
||||
Optional<CountryEntity> findOneByNameIgnoreCase(String name);
|
||||
|
||||
@Query("select c as country, count(a) as artistsCount " +
|
||||
"from CountryEntity c left join c.artists a " +
|
||||
"group by c " +
|
||||
"order by c.id")
|
||||
List<CountryStatsProjection> getAllCountriesStatistics();
|
||||
|
||||
@Query("select c as country, count(a) as artistsCount " +
|
||||
"from CountryEntity c left join c.artists a " +
|
||||
"where c.id = :countryId " +
|
||||
"group by c")
|
||||
CountryStatsProjection getCountryStatistics(@Param("countryId") Long countryId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.example.demo.repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import com.example.demo.entity.EpochEntity;
|
||||
import com.example.demo.entity.projection.EpochStatsProjection;
|
||||
|
||||
public interface EpochRepository extends JpaRepository<EpochEntity, Long> {
|
||||
Optional<EpochEntity> findOneByNameIgnoreCase(String name);
|
||||
|
||||
@Query("select e as epoch, count(a) as artistsCount " +
|
||||
"from EpochEntity e left join e.artists a " +
|
||||
"group by e " +
|
||||
"order by e.id")
|
||||
List<EpochStatsProjection> getAllEpochsStatistics();
|
||||
|
||||
@Query("select e as epoch, count(a) as artistsCount " +
|
||||
"from EpochEntity e left join e.artists a " +
|
||||
"where e.id = :epochId " +
|
||||
"group by e")
|
||||
EpochStatsProjection getEpochStatistics(@Param("epochId") Long epochId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.example.demo.api.PageRs;
|
||||
import com.example.demo.dto.ArtistRq;
|
||||
import com.example.demo.dto.ArtistRs;
|
||||
import com.example.demo.entity.ArtistEntity;
|
||||
import com.example.demo.entity.CountryEntity;
|
||||
import com.example.demo.entity.EpochEntity;
|
||||
import com.example.demo.error.NotFoundException;
|
||||
import com.example.demo.mapper.ArtistMapper;
|
||||
import com.example.demo.repository.ArtistRepository;
|
||||
|
||||
@Service
|
||||
public class ArtistService {
|
||||
private final ArtistRepository repository;
|
||||
private final EpochService epochService;
|
||||
private final CountryService countryService;
|
||||
private final ArtistMapper mapper;
|
||||
|
||||
public ArtistService(ArtistRepository repository, EpochService epochService,
|
||||
CountryService countryService, ArtistMapper mapper) {
|
||||
this.repository = repository;
|
||||
this.epochService = epochService;
|
||||
this.countryService = countryService;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.MANDATORY)
|
||||
public ArtistEntity getEntity(Long id) {
|
||||
return repository.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException(ArtistEntity.class, id));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageRs<ArtistRs> getAll(Pageable pageable) {
|
||||
return PageRs.from(repository.findAll(pageable), mapper::toRsDto);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ArtistRs get(Long id) {
|
||||
final ArtistEntity entity = getEntity(id);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ArtistRs create(ArtistRq dto) {
|
||||
final EpochEntity epoch = epochService.getEntity(dto.getEpochId());
|
||||
final CountryEntity country = countryService.getEntity(dto.getCountryId());
|
||||
ArtistEntity entity = new ArtistEntity(
|
||||
dto.getName(),
|
||||
dto.getDescription(),
|
||||
epoch,
|
||||
country);
|
||||
entity = repository.save(entity);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ArtistRs update(Long id, ArtistRq dto) {
|
||||
ArtistEntity entity = getEntity(id);
|
||||
entity.setName(dto.getName());
|
||||
entity.setDescription(dto.getDescription());
|
||||
entity.setEpoch(epochService.getEntity(dto.getEpochId()));
|
||||
entity.setCountry(countryService.getEntity(dto.getCountryId()));
|
||||
entity = repository.save(entity);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ArtistRs delete(Long id) {
|
||||
final ArtistEntity entity = getEntity(id);
|
||||
repository.delete(entity);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.example.demo.dto.CountryRq;
|
||||
import com.example.demo.dto.CountryRs;
|
||||
import com.example.demo.entity.CountryEntity;
|
||||
import com.example.demo.error.NotFoundException;
|
||||
import com.example.demo.mapper.CountryMapper;
|
||||
import com.example.demo.repository.CountryRepository;
|
||||
|
||||
@Service
|
||||
public class CountryService {
|
||||
private final CountryRepository repository;
|
||||
private final CountryMapper mapper;
|
||||
|
||||
public CountryService(CountryRepository repository, CountryMapper mapper) {
|
||||
this.repository = repository;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.MANDATORY)
|
||||
public CountryEntity getEntity(Long id) {
|
||||
return repository.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException(CountryEntity.class, id));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<CountryRs> getAll() {
|
||||
return mapper.toRsDtoList(repository.findAll());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public CountryRs get(Long id) {
|
||||
final CountryEntity entity = getEntity(id);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CountryRs create(CountryRq dto) {
|
||||
CountryEntity entity = new CountryEntity(dto.getName());
|
||||
entity = repository.save(entity);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CountryRs update(Long id, CountryRq dto) {
|
||||
CountryEntity entity = getEntity(id);
|
||||
entity.setName(dto.getName());
|
||||
entity = repository.save(entity);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CountryRs delete(Long id) {
|
||||
final CountryEntity entity = getEntity(id);
|
||||
repository.delete(entity);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.example.demo.dto.EpochRq;
|
||||
import com.example.demo.dto.EpochRs;
|
||||
import com.example.demo.entity.EpochEntity;
|
||||
import com.example.demo.error.NotFoundException;
|
||||
import com.example.demo.mapper.EpochMapper;
|
||||
import com.example.demo.repository.EpochRepository;
|
||||
|
||||
@Service
|
||||
public class EpochService {
|
||||
private final EpochRepository repository;
|
||||
private final EpochMapper mapper;
|
||||
|
||||
public EpochService(EpochRepository repository, EpochMapper mapper) {
|
||||
this.repository = repository;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.MANDATORY)
|
||||
public EpochEntity getEntity(Long id) {
|
||||
return repository.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException(EpochEntity.class, id));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<EpochRs> getAll() {
|
||||
return mapper.toRsDtoList(repository.findAll());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public EpochRs get(Long id) {
|
||||
final EpochEntity entity = getEntity(id);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public EpochRs create(EpochRq dto) {
|
||||
EpochEntity entity = new EpochEntity(dto.getName());
|
||||
entity = repository.save(entity);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public EpochRs update(Long id, EpochRq dto) {
|
||||
EpochEntity entity = getEntity(id);
|
||||
entity.setName(dto.getName());
|
||||
entity = repository.save(entity);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public EpochRs delete(Long id) {
|
||||
final EpochEntity entity = getEntity(id);
|
||||
repository.delete(entity);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
}
|
||||
|
||||
20
demo/src/main/resources/application-dev.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
logging:
|
||||
level:
|
||||
com:
|
||||
example:
|
||||
demo: DEBUG
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:h2:file:./data
|
||||
username: sa
|
||||
password: sa
|
||||
driver-class-name: org.h2.Driver
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
jpa:
|
||||
show-sql: false
|
||||
properties:
|
||||
hibernate:
|
||||
format-sql: false
|
||||
|
||||
23
demo/src/main/resources/application-prod.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
# Available levels: TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF
|
||||
logging:
|
||||
level:
|
||||
com:
|
||||
example:
|
||||
demo: INFO
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:postgresql://127.0.0.1/demo
|
||||
username: postgres
|
||||
password: postgres
|
||||
driver-class-name: org.postgresql.Driver
|
||||
jpa:
|
||||
show-sql: true
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
format-sql: true
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
use_jdbc_metadata_defaults: false
|
||||
|
||||
12
demo/src/main/resources/application.properties
Normal file
@@ -0,0 +1,12 @@
|
||||
spring.application.name=demo
|
||||
server.port=8080
|
||||
springdoc.api-docs.path=/api-docs
|
||||
springdoc.swagger-ui.path=/swagger-ui.html
|
||||
|
||||
spring.datasource.url=jdbc:h2:file:./data
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=sa
|
||||
spring.datasource.driver-class-name=org.h2.Driver
|
||||
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
spring.h2.console.enabled=true
|
||||
29
demo/src/main/resources/application.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
spring:
|
||||
main:
|
||||
banner-mode: off
|
||||
application:
|
||||
name: demo
|
||||
profiles:
|
||||
active: dev
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
open-in-view: false
|
||||
properties:
|
||||
hibernate:
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
liquibase:
|
||||
enabled: true
|
||||
drop-first: false
|
||||
change-log: classpath:db/master.yml
|
||||
server:
|
||||
port: 8080
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /api-docs
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
|
||||
|
||||
20
demo/src/main/resources/db/changes/0-sequences.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: create-sequences
|
||||
author: user
|
||||
preConditions:
|
||||
- onFail: MARK_RAN
|
||||
- not:
|
||||
- sequenceExists:
|
||||
sequenceName: hibernate_sequence
|
||||
schemaName: public
|
||||
changes:
|
||||
- createSequence:
|
||||
sequenceName: hibernate_sequence
|
||||
schemaName: public
|
||||
startValue: 1
|
||||
incrementBy: 50
|
||||
minValue: 1
|
||||
cycle: false
|
||||
dataType: bigint
|
||||
|
||||
73
demo/src/main/resources/db/changes/1-tables.yml
Normal file
@@ -0,0 +1,73 @@
|
||||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: create-tables
|
||||
author: user
|
||||
preConditions:
|
||||
- onFail: MARK_RAN
|
||||
- not:
|
||||
- tableExists:
|
||||
tableName: epochs
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: epochs
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: BIGINT
|
||||
constraints:
|
||||
primaryKey: true
|
||||
nullable: false
|
||||
primaryKeyName: pk_epochs
|
||||
- column:
|
||||
name: name
|
||||
type: VARCHAR(255)
|
||||
constraints:
|
||||
nullable: false
|
||||
- createTable:
|
||||
tableName: countries
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: BIGINT
|
||||
constraints:
|
||||
primaryKey: true
|
||||
nullable: false
|
||||
primaryKeyName: pk_countries
|
||||
- column:
|
||||
name: name
|
||||
type: VARCHAR(255)
|
||||
constraints:
|
||||
nullable: false
|
||||
- createTable:
|
||||
tableName: artists
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: BIGINT
|
||||
constraints:
|
||||
primaryKey: true
|
||||
nullable: false
|
||||
primaryKeyName: pk_artists
|
||||
- column:
|
||||
name: name
|
||||
type: VARCHAR(255)
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: description
|
||||
type: TEXT
|
||||
- column:
|
||||
name: epoch_id
|
||||
type: BIGINT
|
||||
constraints:
|
||||
nullable: false
|
||||
foreignKeyName: fk_artists_epoch
|
||||
references: epochs(id)
|
||||
- column:
|
||||
name: country_id
|
||||
type: BIGINT
|
||||
constraints:
|
||||
nullable: false
|
||||
foreignKeyName: fk_artists_country
|
||||
references: countries(id)
|
||||
|
||||
53
demo/src/main/resources/db/changes/2-populate-epochs.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: populate-epochs
|
||||
author: user
|
||||
preConditions:
|
||||
- onFail: MARK_RAN
|
||||
- sqlCheck:
|
||||
expectedResult: 0
|
||||
sql: "SELECT COUNT(*) FROM epochs"
|
||||
changes:
|
||||
- insert:
|
||||
tableName: epochs
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
value: 1
|
||||
- column:
|
||||
name: name
|
||||
value: "1980-е"
|
||||
- insert:
|
||||
tableName: epochs
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
value: 2
|
||||
- column:
|
||||
name: name
|
||||
value: "1990-е"
|
||||
- insert:
|
||||
tableName: epochs
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
value: 3
|
||||
- column:
|
||||
name: name
|
||||
value: "2000-е"
|
||||
- sql:
|
||||
sql: "SELECT setval('hibernate_sequence', COALESCE((SELECT MAX(id) FROM epochs), 1) + 50)"
|
||||
dbms: postgresql
|
||||
- sql:
|
||||
sql: "ALTER SEQUENCE hibernate_sequence RESTART WITH (SELECT COALESCE(MAX(id), 1) + 50 FROM epochs)"
|
||||
dbms: h2
|
||||
rollback:
|
||||
- delete:
|
||||
tableName: epochs
|
||||
- sql:
|
||||
sql: "SELECT setval('hibernate_sequence', 1)"
|
||||
dbms: postgresql
|
||||
- sql:
|
||||
sql: "ALTER SEQUENCE hibernate_sequence RESTART WITH 1"
|
||||
dbms: h2
|
||||
|
||||
53
demo/src/main/resources/db/changes/3-populate-countries.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: populate-countries
|
||||
author: user
|
||||
preConditions:
|
||||
- onFail: MARK_RAN
|
||||
- sqlCheck:
|
||||
expectedResult: 0
|
||||
sql: "SELECT COUNT(*) FROM countries"
|
||||
changes:
|
||||
- insert:
|
||||
tableName: countries
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
value: 1
|
||||
- column:
|
||||
name: name
|
||||
value: "Россия"
|
||||
- insert:
|
||||
tableName: countries
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
value: 2
|
||||
- column:
|
||||
name: name
|
||||
value: "Омск"
|
||||
- insert:
|
||||
tableName: countries
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
value: 3
|
||||
- column:
|
||||
name: name
|
||||
value: "Тайга"
|
||||
- sql:
|
||||
sql: "SELECT setval('hibernate_sequence', GREATEST(COALESCE((SELECT MAX(id) FROM countries), 1), COALESCE((SELECT MAX(id) FROM epochs), 1)) + 50)"
|
||||
dbms: postgresql
|
||||
- sql:
|
||||
sql: "ALTER SEQUENCE hibernate_sequence RESTART WITH (SELECT GREATEST(COALESCE(MAX(id), 1), (SELECT COALESCE(MAX(id), 1) FROM epochs)) + 50 FROM countries)"
|
||||
dbms: h2
|
||||
rollback:
|
||||
- sql:
|
||||
sql: "DELETE FROM countries WHERE id IN (1, 2, 3)"
|
||||
- sql:
|
||||
sql: "SELECT setval('hibernate_sequence', COALESCE((SELECT MAX(id) FROM epochs), 1))"
|
||||
dbms: postgresql
|
||||
- sql:
|
||||
sql: "ALTER SEQUENCE hibernate_sequence RESTART WITH (SELECT COALESCE(MAX(id), 1) FROM epochs)"
|
||||
dbms: h2
|
||||
|
||||
80
demo/src/main/resources/db/changes/4-populate-artists.yml
Normal file
@@ -0,0 +1,80 @@
|
||||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: populate-artists
|
||||
author: user
|
||||
preConditions:
|
||||
- onFail: MARK_RAN
|
||||
- sqlCheck:
|
||||
expectedResult: 0
|
||||
sql: "SELECT COUNT(*) FROM artists"
|
||||
changes:
|
||||
- insert:
|
||||
tableName: artists
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
value: 1
|
||||
- column:
|
||||
name: name
|
||||
value: "Кино"
|
||||
- column:
|
||||
name: description
|
||||
value: "Советская рок-группа"
|
||||
- column:
|
||||
name: epoch_id
|
||||
value: 1
|
||||
- column:
|
||||
name: country_id
|
||||
value: 1
|
||||
- insert:
|
||||
tableName: artists
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
value: 2
|
||||
- column:
|
||||
name: name
|
||||
value: "Nirvana"
|
||||
- column:
|
||||
name: description
|
||||
value: "Американская рок-группа"
|
||||
- column:
|
||||
name: epoch_id
|
||||
value: 2
|
||||
- column:
|
||||
name: country_id
|
||||
value: 2
|
||||
- insert:
|
||||
tableName: artists
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
value: 3
|
||||
- column:
|
||||
name: name
|
||||
value: "The Beatles"
|
||||
- column:
|
||||
name: description
|
||||
value: "Британская рок-группа"
|
||||
- column:
|
||||
name: epoch_id
|
||||
value: 1
|
||||
- column:
|
||||
name: country_id
|
||||
value: 3
|
||||
- sql:
|
||||
sql: "SELECT setval('hibernate_sequence', GREATEST(COALESCE((SELECT MAX(id) FROM artists), 1), COALESCE((SELECT MAX(id) FROM countries), 1), COALESCE((SELECT MAX(id) FROM epochs), 1)) + 50)"
|
||||
dbms: postgresql
|
||||
- sql:
|
||||
sql: "ALTER SEQUENCE hibernate_sequence RESTART WITH (SELECT GREATEST(COALESCE(MAX(id), 1), (SELECT COALESCE(MAX(id), 1) FROM countries), (SELECT COALESCE(MAX(id), 1) FROM epochs)) + 50 FROM artists)"
|
||||
dbms: h2
|
||||
rollback:
|
||||
- sql:
|
||||
sql: "DELETE FROM artists WHERE id IN (1, 2, 3)"
|
||||
- sql:
|
||||
sql: "SELECT setval('hibernate_sequence', GREATEST(COALESCE((SELECT MAX(id) FROM countries), 1), COALESCE((SELECT MAX(id) FROM epochs), 1)))"
|
||||
dbms: postgresql
|
||||
- sql:
|
||||
sql: "ALTER SEQUENCE hibernate_sequence RESTART WITH (SELECT GREATEST(COALESCE(MAX(id), 1), (SELECT COALESCE(MAX(id), 1) FROM epochs)) + 50 FROM countries)"
|
||||
dbms: h2
|
||||
|
||||
6
demo/src/main/resources/db/master.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
databaseChangeLog:
|
||||
- includeAll:
|
||||
path: changes/
|
||||
relativeToChangelogFile: true
|
||||
errorIfMissingOrEmpty: false
|
||||
|
||||
15
demo/src/main/resources/static/asset-manifest.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/punkrock/static/css/main.87b4f72f.css",
|
||||
"main.js": "/punkrock/static/js/main.e5d90428.js",
|
||||
"static/media/bootstrap-icons.woff?": "/punkrock/static/media/bootstrap-icons.1295669cd4e305c97f2c.woff",
|
||||
"static/media/bootstrap-icons.woff2?": "/punkrock/static/media/bootstrap-icons.92ea18a81d737146ff04.woff2",
|
||||
"index.html": "/punkrock/index.html",
|
||||
"main.87b4f72f.css.map": "/punkrock/static/css/main.87b4f72f.css.map",
|
||||
"main.e5d90428.js.map": "/punkrock/static/js/main.e5d90428.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.87b4f72f.css",
|
||||
"static/js/main.e5d90428.js"
|
||||
]
|
||||
}
|
||||
78
demo/src/main/resources/static/catalog.html
Normal file
@@ -0,0 +1,78 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="src/styles.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||
<title>Панкуха</title>
|
||||
</head>
|
||||
<body class="bg-dark text-light">
|
||||
<header class="sticky-top navbar navbar-expand-lg navbar-dark bg-black border-bottom border-punk px-0">
|
||||
<div class="container-fluid">
|
||||
<a href="/" class="navbar-brand d-flex align-items-center ms-3">
|
||||
<img src="/res/logo.png" alt="Панкуха" height="60" class="me-2">
|
||||
<span class="text-punk fs-4 fw-bold">Панкуха</span>
|
||||
</a>
|
||||
<button class="navbar-toggler me-3" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse bg-black" id="navbarContent">
|
||||
<ul class="navbar-nav w-100 justify-content-end pe-4">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link text-punk fw-bold dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-list me-1"></i>Страницы
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end bg-dark">
|
||||
<li><a class="dropdown-item text-punk" href="/">Главная</a></li>
|
||||
<li><a class="dropdown-item text-punk" href="/grob">Исполнитель</a></li>
|
||||
<li><a class="dropdown-item text-punk" href="/grobKaifIliBolshe">Песня</a></li>
|
||||
<li><a class="dropdown-item text-punk" href="/catalog">Каталог</a></li>
|
||||
<li><a class="dropdown-item text-punk" href="/punkrock">Список исполнителей</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-punk fw-bold" href="https://vk.com/kadyshevever">
|
||||
<i class="bi bi-people-fill me-1"></i>Контакты
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container-fluid my-5 flex-grow-1">
|
||||
<div class="container">
|
||||
<div class="card bg-dark border-punk mb-4">
|
||||
<div class="card-body">
|
||||
<h3 class="text-punk mb-0"><a class="text-punk" href="/punkrock">Панк-Рок</a></h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-dark border-punk mb-4">
|
||||
<div class="card-body">
|
||||
<h3 class="text-punk mb-0">Психоделика</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-dark border-punk">
|
||||
<div class="card-body">
|
||||
<h3 class="text-punk mb-0">Гаражный панк</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="bg-black py-3 border-top border-punk mt-auto">
|
||||
<div class="container">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center">
|
||||
<p class="mb-0 text-punk">© 2025. Все права защищены.</p>
|
||||
<nav class="d-flex align-items-center">
|
||||
<a href="#" class="text-punk me-3">Политика конфиденциальности</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
demo/src/main/resources/static/favicon.ico
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
110
demo/src/main/resources/static/grob.html
Normal file
@@ -0,0 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/styles.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||
<title>Панкуха</title>
|
||||
</head>
|
||||
<body class="bg-dark text-light">
|
||||
<header class="sticky-top navbar navbar-expand-lg navbar-dark bg-black border-bottom border-punk px-0">
|
||||
<div class="container-fluid">
|
||||
<a href="/" class="navbar-brand d-flex align-items-center ms-3">
|
||||
<img src="/res/logo.png" alt="Панкуха" height="60" class="me-2">
|
||||
<span class="text-punk fs-4 fw-bold">Панкуха</span>
|
||||
</a>
|
||||
<button class="navbar-toggler me-3" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse bg-black" id="navbarContent">
|
||||
<ul class="navbar-nav w-100 justify-content-end pe-4">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link text-punk fw-bold dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-list me-1"></i>Страницы
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end bg-dark">
|
||||
<li><a class="dropdown-item text-punk" href="/">Главная</a></li>
|
||||
<li><a class="dropdown-item text-punk" href="/grob">Исполнитель</a></li>
|
||||
<li><a class="dropdown-item text-punk" href="/grobKaifIliBolshe">Песня</a></li>
|
||||
<li><a class="dropdown-item text-punk" href="/catalog">Каталог</a></li>
|
||||
<li><a class="dropdown-item text-punk" href="/punkrock">Список исполнителей</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-punk fw-bold" href="https://vk.com/kadyshevever">
|
||||
<i class="bi bi-people-fill me-1"></i>Контакты
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container-fluid my-5 flex-grow-1">
|
||||
<div class="container">
|
||||
<div class="card bg-dark border-punk">
|
||||
<div class="card-body">
|
||||
<h1 class="text-punk mb-4">Гражданская Оборона</h1>
|
||||
<div style="text-align: center;">
|
||||
<img src="/res/grob.jpg" alt="uGrob" class="me-2">
|
||||
</div>
|
||||
<p class="lead text-light">
|
||||
Здесь можно почитать инфу про исполнителя и перейти на песню
|
||||
</p>
|
||||
<div class="card bg-dark border-punk mb-4">
|
||||
<div class="card-body">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item bg-dark text-light border-punk">
|
||||
«Гражданская Оборона» — культовая советская и российская рок-группа, основанная в 1984 году в Омске Егором Летовым.
|
||||
Коллектив стал одним из самых влиятельных в андеграундной среде.
|
||||
</li>
|
||||
<li class="list-group-item bg-dark text-light border-punk">
|
||||
Музыка «Гражданской Обороны» сочетает в себе элементы панк-рока, гаражного рока и лоу-фая.
|
||||
Несмотря на минималистичный подход к звучанию, группа смогла создать уникальный стиль.
|
||||
</li>
|
||||
<li class="list-group-item bg-dark text-light border-punk">
|
||||
Среди самых известных альбомов группы — «Тоталитаризм», «Мышеловка», «Здорово и вечно»,
|
||||
«Русское поле экспериментов» и «Инструкция по выживанию». Творчество «Гражданской Обороны»
|
||||
остается актуальным и по сей день, а Егор Летов считается одной из ключевых фигур в истории
|
||||
русской рок-музыки.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-dark border-punk">
|
||||
<div class="card-body">
|
||||
<h3 class="text-punk mb-3">Популярные песни:</h3>
|
||||
<div class="list-group">
|
||||
<a class="list-group-item list-group-item-action bg-dark text-punk border-punk" href="/grobKaifIliBolshe">
|
||||
Кайф или больше
|
||||
</a>
|
||||
<a class="list-group-item list-group-item-action bg-dark text-punk border-punk" href="#">
|
||||
Зоопарк
|
||||
</a>
|
||||
<a class="list-group-item list-group-item-action bg-dark text-punk border-punk" href="#">
|
||||
Новая патриотическая
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="bg-black py-3 border-top border-punk mt-auto">
|
||||
<div class="container">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center">
|
||||
<p class="mb-0 text-punk">© 2025. Все права защищены.</p>
|
||||
<nav class="d-flex align-items-center">
|
||||
<a href="#" class="text-punk me-3">Политика конфиденциальности</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
123
demo/src/main/resources/static/grobKaifIliBolshe.html
Normal file
@@ -0,0 +1,123 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/styles.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||
<title>Панкуха</title>
|
||||
</head>
|
||||
<body class="bg-dark text-light">
|
||||
<header class="sticky-top navbar navbar-expand-lg navbar-dark bg-black border-bottom border-punk px-0">
|
||||
<div class="container-fluid">
|
||||
<a href="/" class="navbar-brand d-flex align-items-center ms-3">
|
||||
<img src="/res/logo.png" alt="Панкуха" height="60" class="me-2">
|
||||
<span class="text-punk fs-4 fw-bold">Панкуха</span>
|
||||
</a>
|
||||
<button class="navbar-toggler me-3" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse bg-black" id="navbarContent">
|
||||
<ul class="navbar-nav w-100 justify-content-end pe-4">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link text-punk fw-bold dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-list me-1"></i>Страницы
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end bg-dark">
|
||||
<li><a class="dropdown-item text-punk" href="/">Главная</a></li>
|
||||
<li><a class="dropdown-item text-punk" href="/grob">Исполнитель</a></li>
|
||||
<li><a class="dropdown-item text-punk" href="/grobKaifIliBolshe">Песня</a></li>
|
||||
<li><a class="dropdown-item text-punk" href="/catalog">Каталог</a></li>
|
||||
<li><a class="dropdown-item text-punk" href="/punkrock">Список исполнителей</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-punk fw-bold" href="https://vk.com/kadyshevever">
|
||||
<i class="bi bi-people-fill me-1"></i>Контакты
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container-fluid my-5 flex-grow-1">
|
||||
<div class="container">
|
||||
<div class="card bg-dark border-punk mb-4">
|
||||
<div class="card-body">
|
||||
<h1 class="text-punk mb-4">Кайф или больше</h1>
|
||||
<h3 class="text-light mb-3">
|
||||
<div style="text-align: center;">
|
||||
|
||||
<img src="/res/nekrofilia.jpg" alt="uGrob" class="me-2">
|
||||
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<a class="text-punk" href="/grob">Гражданская оборона</a>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark border-punk mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="text-punk mb-3">Описание</h5>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item bg-dark text-light border-punk">
|
||||
Была выпущена в 1987 году
|
||||
</li>
|
||||
<li class="list-group-item bg-dark text-light border-punk">
|
||||
Входит в альбом "Некрофилия"
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark border-punk">
|
||||
<div class="card-body">
|
||||
<h5 class="text-punk mb-3">Текст песни:</h5>
|
||||
<pre class="text-light" style="white-space: pre-wrap;">
|
||||
[Куплет 1]
|
||||
Рука повисла в небе, полном до краёв
|
||||
Мои ошибки устилают мой позор
|
||||
Я сочно благодарен, словно кошкин блёв
|
||||
И смачно богомолен, словно приговор
|
||||
|
||||
[Припев]
|
||||
Но мне придётся выбирать
|
||||
Кайф или больше
|
||||
Рай или больше
|
||||
Свет или больше... Хей-йо
|
||||
|
||||
[Куплет 2]
|
||||
Я буду ласковым, как тёплый банный лист
|
||||
Я буду вежливым, как битое окно
|
||||
Я буду благотворен, словно онанист
|
||||
Я буду зазеркален, словно всё равно
|
||||
|
||||
[Припев]
|
||||
Но мне придётся выбирать
|
||||
Кайф или больше
|
||||
Рай или больше
|
||||
Свет или больше...хей-йо
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="bg-black py-3 border-top border-punk mt-auto">
|
||||
<div class="container">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center">
|
||||
<p class="mb-0 text-punk">© 2025. Все права защищены.</p>
|
||||
<nav class="d-flex align-items-center">
|
||||
<a href="#" class="text-punk me-3">Политика конфиденциальности</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1
demo/src/main/resources/static/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link href="/styles.css" rel="stylesheet"><link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"><link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet"><title>Панкуха</title><script defer="defer" src="/punkrock/static/js/main.e5d90428.js"></script><link href="/punkrock/static/css/main.87b4f72f.css" rel="stylesheet"></head><body class="bg-dark text-light"><div id="root"></div><script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script></body></html>
|
||||
BIN
demo/src/main/resources/static/logo192.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
demo/src/main/resources/static/logo512.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
15
demo/src/main/resources/static/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "PunkRock",
|
||||
"name": "PunkRock SPA",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 969 KiB After Width: | Height: | Size: 969 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
3
demo/src/main/resources/static/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
12
demo/src/main/resources/static/static/css/main.87b4f72f.css
Normal file
@@ -0,0 +1,56 @@
|
||||
/*!
|
||||
Copyright (c) 2018 Jed Watson.
|
||||
Licensed under the MIT License (MIT), see
|
||||
http://jedwatson.github.io/classnames
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react-dom.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react-jsx-runtime.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* scheduler.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* react-router v7.6.0
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
189
demo/src/main/resources/static/styles.css
Normal file
@@ -0,0 +1,189 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
--punk-primary: blueviolet;
|
||||
--punk-dark: #121212;
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: blueviolet;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
|
||||
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
background-color: var(--punk-dark);
|
||||
color: white;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
text-align: center;
|
||||
}
|
||||
.lyrics {
|
||||
text-align: center;
|
||||
line-height: 1.8;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.chorus {
|
||||
font-weight: bold;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.vanilla:hover {
|
||||
filter: drop-shadow(0 0 2em #f7df1eaa);
|
||||
}
|
||||
/* Анимация карточек */
|
||||
.catalog-item {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.catalog-item:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(138, 43, 226, 0.3);
|
||||
}
|
||||
.btn-punk {
|
||||
background-color: blueviolet;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-punk:hover {
|
||||
background-color: #9d4edd;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.artist-card {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.artist-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(138, 43, 226, 0.4);
|
||||
}
|
||||
|
||||
.bg-punk {
|
||||
background-color: blueviolet !important;
|
||||
}
|
||||
/* Стиль кнопок */
|
||||
.btn-outline-punk {
|
||||
color: blueviolet;
|
||||
border-color: blueviolet;
|
||||
}
|
||||
.btn-outline-punk:hover {
|
||||
background-color: blueviolet;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
color: blueviolet;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
.bg-punk {
|
||||
background-color: var(--punk-primary) !important;
|
||||
}
|
||||
|
||||
.text-punk {
|
||||
color: var(--punk-primary) !important;
|
||||
}
|
||||
.border-punk {
|
||||
border-color: var(--punk-primary) !important;
|
||||
}
|
||||
.nav-link:hover, .dropdown-item:hover {
|
||||
color: white !important;
|
||||
background-color: var(--punk-primary) !important;
|
||||
}
|
||||
.list-group-item-action:hover {
|
||||
transform: translateX(5px);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.lead{
|
||||
color: blueviolet;
|
||||
text-align: center;
|
||||
font-size: 20pt;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
box-shadow: 0 0 15px rgba(138, 43, 226, 0.4);
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
background-color: #000 !important;
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link:focus {
|
||||
text-shadow: 0 0 8px blueviolet;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.demo;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class DemoApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||