2 Commits

Author SHA1 Message Date
maxim
ab929504e1 сдал 2025-11-15 09:29:01 +04:00
nezui1
8ccd325ba9 с отчетами 2025-11-15 00:53:15 +04:00
304 changed files with 1482 additions and 14800 deletions

BIN
6lab.docx

Binary file not shown.

BIN
LabWork1Report.docx Normal file

Binary file not shown.

BIN
LabWork2Report.docx Normal file

Binary file not shown.

View File

View File

View File

@@ -1,50 +0,0 @@
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
}

View File

@@ -2,8 +2,6 @@ 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'
@@ -20,88 +18,14 @@ repositories {
mavenCentral()
}
ext {
springdocVersion = "2.2.0"
h2Version = "2.4.240"
postgresVersion = "42.7.8"
liquibaseVersion = "4.33.0"
thymeleafLayoutVersion = "3.4.0"
bootstrapVersion = "5.3.3"
bootstrapIconsVersion = "1.11.3"
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.springframework.boot:spring-boot-starter-security'
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}"
}
if (!springProfiles.contains("front")) {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation "nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:${thymeleafLayoutVersion}"
implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity6"
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly "org.webjars.npm:bootstrap:${bootstrapVersion}"
runtimeOnly "org.webjars.npm:bootstrap-icons:${bootstrapIconsVersion}"
// Добавляем implementation для OpenAPI, чтобы Swagger UI был доступен в runtime
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springdocVersion}"
} else {
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springdocVersion}"
}
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
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
tasks.named('test') {
useJUnitPlatform()
}
processResources {
filesMatching("**/application.yml") {
filter { line ->
line.replace("active: dev", "active: ${currentProfiles}")
}
}
}

View File

@@ -1,61 +0,0 @@
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

Binary file not shown.

View File

@@ -1,4 +0,0 @@
2025-12-12 17:34:35.260933+04:00 jdbc[3]: exception
org.h2.jdbc.JdbcSQLSyntaxErrorException: Таблица "DATABASECHANGELOGLOCK" не найдена
Table "DATABASECHANGELOGLOCK" not found; SQL statement:
SELECT COUNT(*) FROM PUBLIC.DATABASECHANGELOGLOCK [42102-240]

251
demo/gradlew vendored
View File

@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View File

@@ -1,6 +0,0 @@
{
"name": "demo",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@@ -1,15 +0,0 @@
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"));
}
}

View File

@@ -1,40 +0,0 @@
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());
}
}

View File

@@ -1,10 +1,8 @@
package com.example.demo.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.info.Contact;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -20,12 +18,6 @@ public class OpenApiConfig {
.description("REST API для управления данными о панк-рок исполнителях")
.contact(new Contact()
.name("Developer")
.email("developer@example.com")))
.addSecurityItem(new SecurityRequirement().addList("basicAuth"))
.components(new io.swagger.v3.oas.models.Components()
.addSecuritySchemes("basicAuth", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("basic")
.description("HTTP Basic Authentication")));
.email("developer@example.com")));
}
}

View File

@@ -3,8 +3,6 @@ package com.example.demo.configuration;
public class Constants {
public static final String DEV_ORIGIN = "http://localhost:5173";
public static final String API_URL = "/api";
public static final String MVC_REDIRECT = "redirect:";
public static final String LOGIN_URL = "/login";
private Constants() {
}

View File

@@ -1,17 +0,0 @@
package com.example.demo.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Profile("!front")
@Configuration
public class MvcWebConfiguration implements WebMvcConfigurer {
@Override
public void addViewControllers(@NonNull ViewControllerRegistry registry) {
registry.addRedirectViewController("/", "/page-artists");
}
}

View File

@@ -11,8 +11,8 @@ public class WebConfiguration implements WebMvcConfigurer {
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)
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
.allowedOrigins("http://localhost:3000", "http://localhost:5173", "http://localhost:5174", Constants.DEV_ORIGIN)
.allowedHeaders("*")
.allowCredentials(true);
}

View File

@@ -1,5 +1,7 @@
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;
@@ -7,13 +9,9 @@ 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;
@@ -30,10 +28,8 @@ public class ArtistController {
}
@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));
public List<ArtistRs> getAll() {
return artistService.getAll();
}
@GetMapping("/{id}")

View File

@@ -1,16 +0,0 @@
package com.example.demo.controller;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Profile("front")
@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";
}
}

View File

@@ -1,39 +0,0 @@
package com.example.demo.controller.mvc;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ArtistFilterSession {
private Long countryId;
private Long epochId;
public Long getCountryId() {
return countryId;
}
public void setCountryId(Long countryId) {
this.countryId = countryId;
}
public Long getEpochId() {
return epochId;
}
public void setEpochId(Long epochId) {
this.epochId = epochId;
}
public void clearFilters() {
this.countryId = null;
this.epochId = null;
}
public boolean hasFilters() {
return countryId != null || epochId != null;
}
}

View File

@@ -1,179 +0,0 @@
package com.example.demo.controller.mvc;
import static com.example.demo.configuration.Constants.MVC_REDIRECT;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import jakarta.validation.Valid;
import com.example.demo.api.PageHelper;
import com.example.demo.api.PageRs;
import com.example.demo.dto.ArtistRs;
import com.example.demo.dto.mvc.ArtistFormDto;
import com.example.demo.service.ArtistService;
import com.example.demo.service.CountryService;
import com.example.demo.service.EpochService;
@Profile("!front")
@SessionAttributes(ArtistMvcController.ATR_COUNTRIES)
@Controller
@RequestMapping("/" + ArtistMvcController.VIEW)
public class ArtistMvcController {
public static final String VIEW = "page-artists";
private static final String VIEW_EDIT = "page-artists-edit";
private static final String ATR_ARTIST = "artist";
public static final String ATR_COUNTRIES = "countries";
public static final String ATR_EPOCHS = "epochs";
private static final String ATR_PAGE = "page";
private static final String ATR_ARTISTS = "artists";
private final ArtistService artistService;
private final CountryService countryService;
private final EpochService epochService;
private final ArtistFilterSession filterSession;
public ArtistMvcController(
ArtistService artistService,
CountryService countryService,
EpochService epochService,
ArtistFilterSession filterSession) {
this.artistService = artistService;
this.countryService = countryService;
this.epochService = epochService;
this.filterSession = filterSession;
}
@GetMapping
public String getAll(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "3") int size,
@RequestParam(required = false) Long countryId,
@RequestParam(required = false) Long epochId,
SessionStatus sessionStatus,
Model model) {
if (countryId != null) {
filterSession.setCountryId(countryId);
}
if (epochId != null) {
filterSession.setEpochId(epochId);
}
final PageRs<ArtistRs> artists = artistService.getAllFiltered(
filterSession.getCountryId(),
filterSession.getEpochId(),
PageHelper.toPageable(page, size));
if (artists.itemsCount() == 0 && page > 1) {
return MVC_REDIRECT + "/" + VIEW;
}
model.addAttribute(ATR_ARTISTS, artists);
model.addAttribute(ATR_PAGE, page);
model.addAttribute(ATR_COUNTRIES, countryService.getAll());
model.addAttribute(ATR_EPOCHS, epochService.getAll());
model.addAttribute("selectedCountryId", filterSession.getCountryId());
model.addAttribute("selectedEpochId", filterSession.getEpochId());
sessionStatus.setComplete();
return VIEW;
}
@PostMapping("/filter")
public String applyFilter(
@RequestParam(required = false) Long countryId,
@RequestParam(required = false) Long epochId,
RedirectAttributes redirectAttributes) {
filterSession.setCountryId(countryId);
filterSession.setEpochId(epochId);
redirectAttributes.addAttribute(ATR_PAGE, 1);
return MVC_REDIRECT + "/" + VIEW;
}
@PostMapping("/filter/clear")
public String clearFilter(RedirectAttributes redirectAttributes) {
filterSession.clearFilters();
redirectAttributes.addAttribute(ATR_PAGE, 1);
return MVC_REDIRECT + "/" + VIEW;
}
@GetMapping(value = { "/edit", "/edit/" })
public String create(@RequestParam(defaultValue = "1") int page, Model model) {
final ArtistFormDto artist = ArtistFormDto.empty();
model.addAttribute(ATR_ARTIST, artist);
model.addAttribute(ATR_COUNTRIES, countryService.getAll());
model.addAttribute(ATR_EPOCHS, epochService.getAll());
model.addAttribute(ATR_PAGE, page);
return VIEW_EDIT;
}
@PostMapping(value = { "/edit", "/edit/" })
public String create(
@RequestParam(defaultValue = "1") int page,
@ModelAttribute(name = ATR_ARTIST) @Valid ArtistFormDto dto,
BindingResult bindingResult,
Model model,
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
model.addAttribute(ATR_COUNTRIES, countryService.getAll());
model.addAttribute(ATR_EPOCHS, epochService.getAll());
model.addAttribute(ATR_PAGE, page);
return VIEW_EDIT;
}
artistService.create(dto.toRq());
redirectAttributes.addAttribute(ATR_PAGE, page);
redirectAttributes.addFlashAttribute("successSave", true);
return MVC_REDIRECT + "/" + VIEW;
}
@GetMapping("/edit/{id:\\d+}")
public String update(@PathVariable Long id, @RequestParam(defaultValue = "1") int page, Model model) {
final ArtistFormDto artist = ArtistFormDto.fromRs(artistService.get(id));
model.addAttribute(ATR_ARTIST, artist);
model.addAttribute(ATR_COUNTRIES, countryService.getAll());
model.addAttribute(ATR_EPOCHS, epochService.getAll());
model.addAttribute(ATR_PAGE, page);
return VIEW_EDIT;
}
@PostMapping("/edit/{id:\\d+}")
public String update(
@PathVariable Long id,
@RequestParam(defaultValue = "1") int page,
@ModelAttribute(name = ATR_ARTIST) @Valid ArtistFormDto dto,
BindingResult bindingResult,
Model model,
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
model.addAttribute(ATR_COUNTRIES, countryService.getAll());
model.addAttribute(ATR_EPOCHS, epochService.getAll());
model.addAttribute(ATR_PAGE, page);
return VIEW_EDIT;
}
artistService.update(id, dto.toRq());
redirectAttributes.addAttribute(ATR_PAGE, page);
redirectAttributes.addFlashAttribute("successSave", true);
return MVC_REDIRECT + "/" + VIEW;
}
@PostMapping("/delete/{id:\\d+}")
public String delete(
@PathVariable Long id,
@RequestParam(defaultValue = "1") int page,
RedirectAttributes redirectAttributes) {
artistService.delete(id);
redirectAttributes.addAttribute(ATR_PAGE, page);
return MVC_REDIRECT + "/" + VIEW;
}
}

View File

@@ -1,121 +0,0 @@
package com.example.demo.controller.mvc;
import static com.example.demo.configuration.Constants.MVC_REDIRECT;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import jakarta.validation.Valid;
import com.example.demo.api.PageHelper;
import com.example.demo.api.PageRs;
import com.example.demo.dto.CountryRs;
import com.example.demo.dto.mvc.CountryFormDto;
import com.example.demo.service.CountryService;
@Profile("!front")
@SessionAttributes(CountryMvcController.ATR_COUNTRIES)
@Controller
@RequestMapping("/" + CountryMvcController.VIEW)
public class CountryMvcController {
public static final String VIEW = "page-countries";
private static final String VIEW_EDIT = "page-countries-edit";
private static final String ATR_COUNTRY = "country";
public static final String ATR_COUNTRIES = "countries";
private static final String ATR_PAGE = "page";
private final CountryService countryService;
public CountryMvcController(CountryService countryService) {
this.countryService = countryService;
}
@GetMapping
public String getAll(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
SessionStatus sessionStatus,
Model model) {
final PageRs<CountryRs> countries = countryService.getAll(PageHelper.toPageable(page, size));
if (countries.itemsCount() == 0 && page > 1) {
return MVC_REDIRECT + "/" + VIEW;
}
model.addAttribute(ATR_COUNTRIES, countries);
model.addAttribute(ATR_PAGE, page);
sessionStatus.setComplete();
return VIEW;
}
@GetMapping(value = { "/edit", "/edit/" })
public String create(@RequestParam(defaultValue = "1") int page, Model model) {
final CountryFormDto country = CountryFormDto.empty();
model.addAttribute(ATR_COUNTRY, country);
model.addAttribute(ATR_PAGE, page);
return VIEW_EDIT;
}
@PostMapping(value = { "/edit", "/edit/" })
public String create(
@RequestParam(defaultValue = "1") int page,
@ModelAttribute(name = ATR_COUNTRY) @Valid CountryFormDto dto,
BindingResult bindingResult,
Model model,
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
model.addAttribute(ATR_PAGE, page);
return VIEW_EDIT;
}
countryService.create(dto.toRq());
redirectAttributes.addAttribute(ATR_PAGE, page);
redirectAttributes.addFlashAttribute("successSave", true);
return MVC_REDIRECT + "/" + VIEW;
}
@GetMapping("/edit/{id:\\d+}")
public String update(@PathVariable Long id, @RequestParam(defaultValue = "1") int page, Model model) {
final CountryFormDto country = CountryFormDto.fromRs(countryService.get(id));
model.addAttribute(ATR_COUNTRY, country);
model.addAttribute(ATR_PAGE, page);
return VIEW_EDIT;
}
@PostMapping("/edit/{id:\\d+}")
public String update(
@PathVariable Long id,
@RequestParam(defaultValue = "1") int page,
@ModelAttribute(name = ATR_COUNTRY) @Valid CountryFormDto dto,
BindingResult bindingResult,
Model model,
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
model.addAttribute(ATR_PAGE, page);
return VIEW_EDIT;
}
countryService.update(id, dto.toRq());
redirectAttributes.addAttribute(ATR_PAGE, page);
redirectAttributes.addFlashAttribute("successSave", true);
return MVC_REDIRECT + "/" + VIEW;
}
@PostMapping("/delete/{id:\\d+}")
public String delete(
@PathVariable Long id,
@RequestParam(defaultValue = "1") int page,
RedirectAttributes redirectAttributes) {
countryService.delete(id);
redirectAttributes.addAttribute(ATR_PAGE, page);
return MVC_REDIRECT + "/" + VIEW;
}
}

View File

@@ -1,21 +0,0 @@
package com.example.demo.controller.mvc;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.example.demo.configuration.Constants;
@Profile("!front")
@Controller
@RequestMapping(Constants.LOGIN_URL)
public class LoginMvcController {
public static final String VIEW = "login";
@GetMapping
public String getLogin() {
return VIEW;
}
}

View File

@@ -1,41 +0,0 @@
package com.example.demo.controller.mvc;
import org.springframework.context.annotation.Profile;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.example.demo.dto.UserRs;
import com.example.demo.security.UserPrincipal;
import com.example.demo.service.UserService;
@Profile("!front")
@Controller
@RequestMapping("/page-user")
public class UserMvcController {
public static final String VIEW = "page-user";
private final UserService userService;
public UserMvcController(UserService userService) {
this.userService = userService;
}
@GetMapping
public String getUserProfile(Authentication authentication, Model model) {
if (authentication != null && authentication.getPrincipal() instanceof UserPrincipal) {
UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
String username = principal.getUsername();
UserRs user = userService.get(username);
model.addAttribute("user", user);
model.addAttribute("username", user.login());
model.addAttribute("role", user.role());
}
return VIEW;
}
}

View File

@@ -1,62 +0,0 @@
package com.example.demo.controller.mvc;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import jakarta.validation.Valid;
import com.example.demo.configuration.Constants;
import com.example.demo.dto.mvc.UserSignupFormDto;
import com.example.demo.error.AlreadyExistsException;
import com.example.demo.error.PasswordConfirmationException;
import com.example.demo.service.UserService;
@Profile("!front")
@Controller
@RequestMapping("/" + UserSignupMvcController.VIEW)
public class UserSignupMvcController {
public static final String VIEW = "signup";
private static final String ATR_USER = "user";
private UserService userService;
public UserSignupMvcController(UserService userService) {
this.userService = userService;
}
@GetMapping
public String getSignup(Model model) {
model.addAttribute(ATR_USER, UserSignupFormDto.empty());
return VIEW;
}
@PostMapping
public String signup(
@ModelAttribute(name = ATR_USER) @Valid UserSignupFormDto dto,
BindingResult bindingResult,
Model model) {
if (bindingResult.hasErrors()) {
return VIEW;
}
try {
userService.create(dto.toRq());
} catch (AlreadyExistsException ae) {
bindingResult.rejectValue("login", "signup:login", ae.getMessage());
model.addAttribute(ATR_USER, dto);
return VIEW;
} catch (PasswordConfirmationException pce) {
bindingResult.rejectValue("password", "signup:password", pce.getMessage());
bindingResult.rejectValue("passwordConfirm", "signup:passwordConfirm", pce.getMessage());
model.addAttribute(ATR_USER, dto);
return VIEW;
}
return "redirect:" + Constants.LOGIN_URL + "?signup";
}
}

View File

@@ -2,9 +2,10 @@ package com.example.demo.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
public class ArtistRq {
@NotBlank
@Size(min = 0, max = 500)
private String name;
private String description;
@NotNull

View File

@@ -1,9 +1,11 @@
package com.example.demo.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class CountryRq {
@NotBlank
@Size(min = 0, max = 50)
private String name;
public String getName() {

View File

@@ -1,9 +1,11 @@
package com.example.demo.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class EpochRq {
@NotBlank
@Size(min = 0, max = 7)
private String name;
public String getName() {

View File

@@ -1,11 +0,0 @@
package com.example.demo.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public record UserRq(
@NotBlank @Size(min = 3, max = 20) String login,
@NotBlank @Size(min = 3, max = 60) String password,
@NotBlank @Size(min = 3, max = 60) String passwordConfirm) {
}

View File

@@ -1,11 +0,0 @@
package com.example.demo.dto;
import com.example.demo.entity.UserEntity;
import com.example.demo.entity.UserRole;
public record UserRs(Long id, String login, UserRole role) {
public static UserRs from(UserEntity entity) {
return new UserRs(entity.getId(), entity.getLogin(), entity.getRole());
}
}

View File

@@ -1,42 +0,0 @@
package com.example.demo.dto.mvc;
import java.util.Optional;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Min;
import com.example.demo.dto.ArtistRq;
import com.example.demo.dto.ArtistRs;
import com.example.demo.dto.CountryRs;
import com.example.demo.dto.EpochRs;
public record ArtistFormDto(
Long id,
@NotBlank String name,
String description,
@NotNull @Min(1) Long epochId,
@NotNull @Min(1) Long countryId) {
public static ArtistFormDto empty() {
return new ArtistFormDto(null, null, null, -1L, -1L);
}
public static ArtistFormDto fromRs(ArtistRs item) {
return new ArtistFormDto(
item.getId(),
item.getName(),
item.getDescription(),
Optional.ofNullable(item.getEpoch()).map(EpochRs::getId).orElse(-1L),
Optional.ofNullable(item.getCountry()).map(CountryRs::getId).orElse(-1L));
}
public ArtistRq toRq() {
ArtistRq rq = new ArtistRq();
rq.setName(name);
rq.setDescription(description);
rq.setEpochId(epochId);
rq.setCountryId(countryId);
return rq;
}
}

View File

@@ -1,25 +0,0 @@
package com.example.demo.dto.mvc;
import jakarta.validation.constraints.NotBlank;
import com.example.demo.dto.CountryRq;
import com.example.demo.dto.CountryRs;
public record CountryFormDto(
Long id,
@NotBlank String name) {
public static CountryFormDto empty() {
return new CountryFormDto(null, null);
}
public static CountryFormDto fromRs(CountryRs item) {
return new CountryFormDto(item.getId(), item.getName());
}
public CountryRq toRq() {
CountryRq rq = new CountryRq();
rq.setName(name);
return rq;
}
}

View File

@@ -1,20 +0,0 @@
package com.example.demo.dto.mvc;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import com.example.demo.dto.UserRq;
public record UserSignupFormDto(
@NotBlank @Size(min = 3, max = 20) String login,
@NotBlank @Size(min = 3, max = 60) String password,
@NotBlank @Size(min = 3, max = 60) String passwordConfirm) {
public static UserSignupFormDto empty() {
return new UserSignupFormDto(null, null, null);
}
public UserRq toRq() {
return new UserRq(login, password, passwordConfirm);
}
}

View File

@@ -1,26 +1,9 @@
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() {
@@ -31,8 +14,8 @@ public class ArtistEntity extends BaseEntity {
this();
this.name = name;
this.description = description;
setEpoch(epoch);
setCountry(country);
this.epoch = epoch;
this.country = country;
}
public String getName() {
@@ -56,18 +39,7 @@ public class ArtistEntity extends BaseEntity {
}
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() {
@@ -75,18 +47,7 @@ public class ArtistEntity extends BaseEntity {
}
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);
}
}
}

View File

@@ -1,16 +1,6 @@
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)
@SequenceGenerator(name = "hibernate_sequence")
protected Long id;
protected BaseEntity() {

View File

@@ -1,23 +1,7 @@
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();
@@ -35,27 +19,5 @@ public class CountryEntity extends BaseEntity {
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);
}
}
}
}

View File

@@ -1,23 +1,7 @@
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();
@@ -35,27 +19,5 @@ public class EpochEntity extends BaseEntity {
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);
}
}
}
}

View File

@@ -1,52 +0,0 @@
package com.example.demo.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Table;
@Entity
@Table(name = "sec_users")
public class UserEntity extends BaseEntity {
@Column(nullable = false, unique = true, length = 20)
private String login;
@Column(nullable = false, length = 255)
private String password;
@Enumerated(EnumType.STRING)
private UserRole role;
public UserEntity() {
}
public UserEntity(String login, String password) {
this.login = login;
this.password = password;
this.role = UserRole.USER;
}
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public UserRole getRole() {
return role;
}
public void setRole(UserRole role) {
this.role = role;
}
}

View File

@@ -1,14 +0,0 @@
package com.example.demo.entity;
import org.springframework.security.core.GrantedAuthority;
public enum UserRole implements GrantedAuthority {
ADMIN,
USER;
@Override
public String getAuthority() {
return "ROLE_" + this.name();
}
}

View File

@@ -1,8 +0,0 @@
package com.example.demo.entity.projection;
public interface ArtistStatsProjection {
Long getTotalArtists();
Long getArtistsWithDescription();
Long getArtistsWithoutDescription();
}

View File

@@ -1,9 +0,0 @@
package com.example.demo.entity.projection;
import com.example.demo.entity.CountryEntity;
public interface CountryStatsProjection {
CountryEntity getCountry();
Long getArtistsCount();
}

View File

@@ -1,9 +0,0 @@
package com.example.demo.entity.projection;
import com.example.demo.entity.EpochEntity;
public interface EpochStatsProjection {
EpochEntity getEpoch();
Long getArtistsCount();
}

View File

@@ -1,8 +0,0 @@
package com.example.demo.error;
public class AlreadyExistsException extends RuntimeException {
public AlreadyExistsException(Class<?> entityClass, String identifier) {
super(String.format("%s with identifier '%s' already exists", entityClass.getSimpleName(), identifier));
}
}

View File

@@ -4,9 +4,5 @@ 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));
}
public <T> NotFoundException(Class<T> entClass, String field, String value) {
super(String.format("%s with %s '%s' is not found", entClass.getSimpleName(), field, value));
}
}

View File

@@ -1,8 +0,0 @@
package com.example.demo.error;
public class PasswordConfirmationException extends RuntimeException {
public PasswordConfirmationException() {
super("Passwords do not match");
}
}

View File

@@ -1,47 +1,10 @@
package com.example.demo.repository;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
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);
Page<ArtistEntity> findByEpochId(Long epochId, Pageable pageable);
Page<ArtistEntity> findByCountryId(Long countryId, Pageable pageable);
Page<ArtistEntity> findByEpochIdAndCountryId(Long epochId, Long countryId, Pageable pageable);
@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);
@Repository
public class ArtistRepository extends MapRepository<ArtistEntity> {
}

View File

@@ -0,0 +1,16 @@
package com.example.demo.repository;
import java.util.Optional;
public interface CommonRepository<E, T> {
Iterable<E> findAll();
Optional<E> findById(T id);
E save(E entity);
void delete(E entity);
void deleteAll();
}

View File

@@ -1,28 +1,10 @@
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 org.springframework.stereotype.Repository;
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);
@Repository
public class CountryRepository extends MapRepository<CountryEntity> {
}

View File

@@ -1,28 +1,10 @@
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 org.springframework.stereotype.Repository;
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);
@Repository
public class EpochRepository extends MapRepository<EpochEntity> {
}

View File

@@ -0,0 +1,69 @@
package com.example.demo.repository;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.atomic.AtomicLong;
import com.example.demo.entity.BaseEntity;
public abstract class MapRepository<E extends BaseEntity> implements CommonRepository<E, Long> {
private final ConcurrentNavigableMap<Long, E> entities = new ConcurrentSkipListMap<>();
private final AtomicLong idGenerator = new AtomicLong(0L);
protected MapRepository() {
}
private boolean isNew(E entity) {
return Objects.isNull(entity.getId());
}
private E create(E entity) {
final Long lastId = idGenerator.incrementAndGet();
entity.setId(lastId);
entities.put(lastId, entity);
return entity;
}
private E update(E entity) {
if (findById(entity.getId()).isEmpty()) {
return null;
}
entities.put(entity.getId(), entity);
return entity;
}
@Override
public Iterable<E> findAll() {
return entities.values();
}
@Override
public Optional<E> findById(Long id) {
return Optional.ofNullable(entities.get(id));
}
@Override
public E save(E entity) {
if (isNew(entity)) {
return create(entity);
}
return update(entity);
}
@Override
public void delete(E entity) {
if (findById(entity.getId()).isEmpty()) {
return;
}
entities.remove(entity.getId());
}
@Override
public void deleteAll() {
entities.clear();
idGenerator.set(0L);
}
}

View File

@@ -1,12 +0,0 @@
package com.example.demo.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.demo.entity.UserEntity;
public interface UserRepository extends JpaRepository<UserEntity, Long> {
Optional<UserEntity> findByLoginIgnoreCase(String login);
}

View File

@@ -1,15 +0,0 @@
package com.example.demo.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class PasswordEncoderConfiguration {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@@ -1,84 +0,0 @@
package com.example.demo.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import com.example.demo.configuration.Constants;
import com.example.demo.controller.mvc.UserSignupMvcController;
import com.example.demo.entity.UserRole;
import com.example.demo.service.UserService;
@Profile("!front")
@Configuration
@EnableMethodSecurity
public class SecurityMvcConfiguration {
private final UserService userService;
public SecurityMvcConfiguration(UserService userService) {
this.userService = userService;
}
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.headers(headers -> headers.frameOptions(FrameOptionsConfig::sameOrigin));
httpSecurity.csrf(AbstractHttpConfigurer::disable);
httpSecurity.cors(Customizer.withDefaults());
// Настраиваем обработку исключений для API - возвращаем 401 вместо редиректа на логин
RequestMatcher apiMatcher = new AntPathRequestMatcher(Constants.API_URL + "/**");
httpSecurity.exceptionHandling(exceptions -> exceptions
.defaultAuthenticationEntryPointFor(
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
apiMatcher)
.defaultAccessDeniedHandlerFor(
(request, response, accessDeniedException) -> {
response.setStatus(HttpStatus.FORBIDDEN.value());
},
apiMatcher));
// Настраиваем HTTP Basic аутентификацию для API endpoints
httpSecurity.httpBasic(basic -> basic.authenticationEntryPoint(
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)));
httpSecurity.authorizeHttpRequests(requests -> requests
.requestMatchers("/css/**", "/js/**", "/webjars/**", "/images/**", "/icon.svg", "/favicon.ico")
.permitAll()
.requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/api-docs/**", "/api/swagger-ui.html")
.hasAuthority(UserRole.ADMIN.getAuthority())
.requestMatchers(Constants.LOGIN_URL, "/login/**").permitAll()
.requestMatchers("/" + UserSignupMvcController.VIEW).anonymous()
.requestMatchers("/error").permitAll()
.requestMatchers("/api/**").hasAuthority(UserRole.ADMIN.getAuthority())
.requestMatchers("/h2-console/**").hasAuthority(UserRole.ADMIN.getAuthority())
.anyRequest().authenticated());
httpSecurity.userDetailsService(userService);
httpSecurity.formLogin(formLogin -> formLogin
.loginPage(Constants.LOGIN_URL)
.defaultSuccessUrl("/", true)
.permitAll());
httpSecurity.rememberMe(rememberMe -> rememberMe.key("some-key"));
httpSecurity.logout(logout -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout", "POST"))
.deleteCookies("JSESSIONID")
.permitAll());
return httpSecurity.build();
}
}

View File

@@ -1,75 +0,0 @@
package com.example.demo.security;
import java.util.Collection;
import java.util.Set;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.example.demo.entity.UserEntity;
import com.example.demo.entity.UserRole;
public class UserPrincipal implements UserDetails {
private final Long id;
private final String username;
private final String password;
private final Set<? extends GrantedAuthority> roles;
private final boolean active;
private UserPrincipal(
Long id, String username, String password, Set<? extends GrantedAuthority> roles, boolean active) {
this.id = id;
this.username = username;
this.password = password;
this.roles = roles;
this.active = active;
}
public UserPrincipal(UserEntity user) {
this.id = user.getId();
this.username = user.getLogin();
this.password = user.getPassword();
this.roles = Set.of(user.getRole());
this.active = true;
}
public Long getId() {
return id;
}
@Override
public String getUsername() {
return username;
}
@Override
public String getPassword() {
return password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles;
}
@Override
public boolean isEnabled() {
return active;
}
@Override
public boolean isAccountNonExpired() {
return isEnabled();
}
@Override
public boolean isAccountNonLocked() {
return isEnabled();
}
@Override
public boolean isCredentialsNonExpired() {
return isEnabled();
}
}

View File

@@ -2,13 +2,8 @@ package com.example.demo.service;
import java.util.List;
import org.springframework.data.domain.Page;
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;
@@ -33,39 +28,20 @@ public class ArtistService {
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);
public List<ArtistRs> getAll() {
return mapper.toRsDtoList(repository.findAll());
}
@Transactional(readOnly = true)
public PageRs<ArtistRs> getAllFiltered(Long countryId, Long epochId, Pageable pageable) {
Page<ArtistEntity> page;
if (countryId != null && epochId != null) {
page = repository.findByEpochIdAndCountryId(epochId, countryId, pageable);
} else if (countryId != null) {
page = repository.findByCountryId(countryId, pageable);
} else if (epochId != null) {
page = repository.findByEpochId(epochId, pageable);
} else {
page = repository.findAll(pageable);
}
return PageRs.from(page, 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());
@@ -78,7 +54,6 @@ public class ArtistService {
return mapper.toRsDto(entity);
}
@Transactional
public ArtistRs update(Long id, ArtistRq dto) {
ArtistEntity entity = getEntity(id);
entity.setName(dto.getName());
@@ -89,7 +64,6 @@ public class ArtistService {
return mapper.toRsDto(entity);
}
@Transactional
public ArtistRs delete(Long id) {
final ArtistEntity entity = getEntity(id);
repository.delete(entity);

View File

@@ -2,12 +2,8 @@ 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.CountryRq;
import com.example.demo.dto.CountryRs;
import com.example.demo.entity.CountryEntity;
@@ -25,36 +21,26 @@ public class CountryService {
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 PageRs<CountryRs> getAll(Pageable pageable) {
return PageRs.from(repository.findAll(pageable), mapper::toRsDto);
}
@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());
@@ -62,7 +48,6 @@ public class CountryService {
return mapper.toRsDto(entity);
}
@Transactional
public CountryRs delete(Long id) {
final CountryEntity entity = getEntity(id);
repository.delete(entity);

View File

@@ -3,8 +3,6 @@ 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;
@@ -23,31 +21,26 @@ public class EpochService {
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());
@@ -55,7 +48,6 @@ public class EpochService {
return mapper.toRsDto(entity);
}
@Transactional
public EpochRs delete(Long id) {
final EpochEntity entity = getEntity(id);
repository.delete(entity);

View File

@@ -1,74 +0,0 @@
package com.example.demo.service;
import java.util.Objects;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.dto.UserRq;
import com.example.demo.dto.UserRs;
import com.example.demo.entity.UserEntity;
import com.example.demo.error.AlreadyExistsException;
import com.example.demo.error.NotFoundException;
import com.example.demo.error.PasswordConfirmationException;
import com.example.demo.repository.UserRepository;
import com.example.demo.security.UserPrincipal;
@Service
public class UserService implements UserDetailsService {
private final UserRepository repository;
private final PasswordEncoder passwordEncoder;
public UserService(UserRepository repository, PasswordEncoder passwordEncoder) {
this.repository = repository;
this.passwordEncoder = passwordEncoder;
}
@Transactional(propagation = Propagation.MANDATORY)
public UserEntity getEntity(Long id) {
return repository.findById(id)
.orElseThrow(() -> new NotFoundException(UserEntity.class, id));
}
@Transactional(propagation = Propagation.MANDATORY)
public UserEntity getEntityByLogin(String login) {
return repository.findByLoginIgnoreCase(login)
.orElseThrow(() -> new NotFoundException(UserEntity.class, "login", login));
}
@Transactional(readOnly = true)
public UserRs get(String login) {
final UserEntity entity = getEntityByLogin(login);
return UserRs.from(entity);
}
@Transactional
public UserRs create(UserRq dto) {
if (repository.findByLoginIgnoreCase(dto.login()).isPresent()) {
throw new AlreadyExistsException(UserEntity.class, dto.login());
}
if (!Objects.equals(dto.password(), dto.passwordConfirm())) {
throw new PasswordConfirmationException();
}
UserEntity entity = new UserEntity(dto.login(), passwordEncoder.encode(dto.password()));
entity = repository.save(entity);
return UserRs.from(entity);
}
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try {
final UserEntity existsUser = getEntityByLogin(username);
return new UserPrincipal(existsUser);
} catch (NotFoundException e) {
throw new UsernameNotFoundException(e.getMessage());
}
}
}

View File

@@ -1,21 +0,0 @@
# Available levels: TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF
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

View File

@@ -1,20 +0,0 @@
# 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: false
properties:
hibernate:
jdbc:
lob:
non_contextual_creation: true

View File

@@ -2,11 +2,3 @@ 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

View File

@@ -1,39 +0,0 @@
spring:
main:
banner-mode: off
application:
name: demo
profiles:
active: dev
datasource:
url: jdbc:h2:file:./data
username: sa
password: sa
driver-class-name: org.h2.Driver
h2:
console:
enabled: true
jpa:
hibernate:
ddl-auto: validate
open-in-view: false
properties:
hibernate:
dialect: org.hibernate.dialect.H2Dialect
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
persist-authorization: true

View File

@@ -1,20 +0,0 @@
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

View File

@@ -1,73 +0,0 @@
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)

View File

@@ -1,37 +0,0 @@
databaseChangeLog:
- changeSet:
id: create-users-table
author: user
preConditions:
- onFail: MARK_RAN
- not:
- tableExists:
tableName: sec_users
changes:
- createTable:
tableName: sec_users
columns:
- column:
name: id
type: BIGINT
constraints:
primaryKey: true
nullable: false
primaryKeyName: pk_sec_users
- column:
name: login
type: VARCHAR(20)
constraints:
nullable: false
unique: true
- column:
name: password
type: VARCHAR(255)
constraints:
nullable: false
- column:
name: role
type: VARCHAR(20)
constraints:
nullable: false

View File

@@ -1,6 +0,0 @@
databaseChangeLog:
- includeAll:
path: changes/
relativeToChangelogFile: true
errorIfMissingOrEmpty: false

View File

@@ -1,39 +0,0 @@
:root {
--punk-primary: blueviolet;
--punk-dark: #121212;
}
.text-punk {
color: var(--punk-primary) !important;
}
.border-punk {
border-color: var(--punk-primary) !important;
}
.bg-punk {
background-color: var(--punk-primary) !important;
}
.page-link {
background-color: transparent;
border-color: var(--punk-primary);
color: var(--punk-primary);
}
.page-link:hover {
background-color: var(--punk-primary);
color: white;
border-color: var(--punk-primary);
}
.page-item.active .page-link {
background-color: var(--punk-primary);
border-color: var(--punk-primary);
color: white;
}
.nav-link.active {
color: var(--punk-primary) !important;
}

View File

@@ -1,16 +0,0 @@
<!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">
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -1,67 +0,0 @@
<!DOCTYPE html>
<html lang="ru" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title layout:title-pattern="$LAYOUT_TITLE - $CONTENT_TITLE">Панк-рок исполнители</title>
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<script type="text/javascript" src="/webjars/bootstrap/5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<link rel="stylesheet" href="/webjars/bootstrap/5.3.3/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="/webjars/bootstrap-icons/1.11.3/font/bootstrap-icons.min.css" />
<link rel="stylesheet" href="/css/style.css" />
<th:block layout:fragment="css"></th:block>
</head>
<body class="bg-dark text-light">
<div class="d-flex flex-column min-vh-100">
<header>
<nav class="navbar navbar-expand-md navbar-dark bg-dark border-bottom border-punk">
<div class="container-fluid">
<a class="navbar-brand text-punk" th:href="@{/}">
<i class="bi bi-music-note-beamed"></i>
Панк-рок
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse justify-content-end" id="navbarNav">
<div class="navbar-nav">
<a class="nav-link" th:href="@{/page-artists}">
Исполнители
</a>
<a class="nav-link" th:href="@{/page-countries}">
Страны
</a>
<a class="nav-link" th:href="@{/swagger-ui.html}" target="_blank" sec:authorize="hasRole('ADMIN')">Swagger</a>
<th:block sec:authorize="isAuthenticated()">
<div class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle"></i>
<span sec:authentication="name"></span>
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<li>
<a class="dropdown-item" th:href="@{/page-user}">Профиль</a>
</li>
<li>
<form th:action="@{/logout}" method="post" class="d-inline">
<button class="dropdown-item" type="submit">Выйти</button>
</form>
</li>
</ul>
</div>
</th:block>
<a class="nav-link" th:href="@{/login}" sec:authorize="!isAuthenticated()">Войти</a>
</div>
</div>
</div>
</nav>
</header>
<main class="flex-grow-1 container-fluid p-4" layout:fragment="content"></main>
<footer class="d-flex flex-shrink-0 align-items-center justify-content-center bg-dark border-top border-punk py-3">
<span class="text-muted">Панк-рок исполнители, [[${#dates.year(#dates.createNow())}]]</span>
</footer>
</div>
<th:block layout:fragment="js"></th:block>
</body>
</html>

View File

@@ -1,76 +0,0 @@
<!DOCTYPE html>
<html lang="ru" xmlns:th="http://www.thymeleaf.org">
<head>
<title></title>
</head>
<body>
<th:block th:fragment="draw (items, totalPages, currentPage)">
<th:block th:with="isEmpty=${items.size() == 0}">
<div class="table-responsive">
<table class="table table-dark table-hover table-sm">
<thead>
<tr>
<th></th>
<th>Название</th>
<th>Описание</th>
<th>Эпоха</th>
<th>Страна</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<th:block th:if="${isEmpty}">
<tr>
<td class="text-center" colspan="100">
<h5 class="text-center text-muted">Данные отсутствуют</h5>
</td>
</tr>
</th:block>
<th:block th:unless="${isEmpty}">
<tr th:each="artist : ${items}">
<td th:text="${artist.id}"></td>
<td th:text="${artist.name}"></td>
<td>
<span th:if="${artist.description != null and artist.description.length() > 0}" th:text="${#strings.abbreviate(artist.description, 50)}"></span>
<span th:unless="${artist.description != null and artist.description.length() > 0}" class="text-muted">-</span>
</td>
<td th:text="${artist.epoch?.name}"></td>
<td th:text="${artist.country?.name}"></td>
<td class="p-1">
<a
class="btn btn-warning btn-sm"
th:href="@{/page-artists/edit/{id}(id=${artist.id},page=${currentPage})}"
>
<i class="bi bi-pencil-fill"></i>
</a>
</td>
<td class="p-1">
<form
th:action="@{/page-artists/delete/{id}(id=${artist.id},page=${currentPage})}"
method="post"
>
<button
class="btn btn-danger btn-sm"
onclick="return confirm('Вы уверены?')"
>
<i class="bi bi-trash-fill"></i>
</button>
</form>
</td>
</tr>
</th:block>
</tbody>
</table>
</div>
<th:block
th:replace="~{ f-pagination :: draw (
path='page-artists',
totalPages=${totalPages},
currentPage=${currentPage}) }"
/>
</th:block>
</th:block>
</body>
</html>

View File

@@ -1,67 +0,0 @@
<!DOCTYPE html>
<html lang="ru" xmlns:th="http://www.thymeleaf.org">
<head>
<title></title>
</head>
<body>
<th:block th:fragment="draw (items, totalPages, currentPage)">
<th:block th:with="isEmpty=${items.size() == 0}">
<div class="table-responsive">
<table class="table table-dark table-hover table-sm">
<thead>
<tr>
<th></th>
<th>Название</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<th:block th:if="${isEmpty}">
<tr>
<td class="text-center" colspan="100">
<h5 class="text-center text-muted">Данные отсутствуют</h5>
</td>
</tr>
</th:block>
<th:block th:unless="${isEmpty}">
<tr th:each="country : ${items}">
<td th:text="${country.id}"></td>
<td th:text="${country.name}"></td>
<td class="p-1">
<a
class="btn btn-warning btn-sm"
th:href="@{/page-countries/edit/{id}(id=${country.id},page=${currentPage})}"
>
<i class="bi bi-pencil-fill"></i>
</a>
</td>
<td class="p-1">
<form
th:action="@{/page-countries/delete/{id}(id=${country.id},page=${currentPage})}"
method="post"
>
<button
class="btn btn-danger btn-sm"
onclick="return confirm('Вы уверены?')"
>
<i class="bi bi-trash-fill"></i>
</button>
</form>
</td>
</tr>
</th:block>
</tbody>
</table>
</div>
<th:block
th:replace="~{ f-pagination :: draw (
path='page-countries',
totalPages=${totalPages},
currentPage=${currentPage}) }"
/>
</th:block>
</th:block>
</body>
</html>

View File

@@ -1,56 +0,0 @@
<!DOCTYPE html>
<html lang="ru" xmlns:th="http://www.thymeleaf.org">
<head>
<title></title>
</head>
<body>
<th:block th:fragment="draw (path, totalPages, currentPage)">
<nav th:if="${totalPages > 1}" th:with="maxPage=2">
<ul
class="pagination justify-content-center"
th:with="seqFrom=${currentPage - maxPage < 1 ? 1 : currentPage - maxPage},
seqTo=${currentPage + maxPage > totalPages ? totalPages : currentPage + maxPage}"
>
<th:block th:if="${currentPage > maxPage + 1}">
<li class="page-item">
<a class="page-link" aria-label="Previous" th:href="@{/{url}(url=${path})}">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
<li class="page-item disabled">
<span class="page-link" aria-label="Previous">
<span aria-hidden="true">&hellip;</span>
</span>
</li>
</th:block>
<li
class="page-item"
th:each="page : ${#numbers.sequence(seqFrom, seqTo)}"
th:classappend="${page == currentPage} ? 'active' : ''"
>
<a class="page-link" th:href="@{/{url}?page={page}(url=${path},page=${page})}">
<span th:text="${page}" />
</a>
</li>
<th:block th:if="${currentPage < totalPages - maxPage}">
<li class="page-item disabled">
<span class="page-link" aria-label="Previous">
<span aria-hidden="true">&hellip;</span>
</span>
</li>
<li class="page-item">
<a
class="page-link"
aria-label="Next"
th:href="@{/{url}?page={page}(url=${path},page=${totalPages})}"
>
<span aria-hidden="true">&raquo;</span>
</a>
</li>
</th:block>
</ul>
</nav>
</th:block>
</body>
</html>

View File

@@ -1,51 +0,0 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<title>Вход</title>
</head>
<body>
<main layout:fragment="content">
<div class="row justify-content-center">
<form action="#" th:action="@{/login}" method="post" class="col col-md-6">
<div th:if="${param.error}" class="alert alert-danger">Неверный логин или пароль</div>
<div th:if="${param.logout}" class="alert alert-success">Выход успешно произведен</div>
<div th:if="${param.signup}" class="alert alert-success">Пользователь успешно создан</div>
<div class="mb-3">
<label for="username" class="form-label">Имя пользователя</label>
<input
type="text"
id="username"
name="username"
class="form-control"
minlength="3"
maxlength="20"
required
/>
</div>
<div class="mb-3">
<label for="password" class="form-label">Пароль</label>
<input
type="password"
id="password"
name="password"
class="form-control"
minlength="3"
maxlength="60"
required
/>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="remember-me" name="remember-me" checked />
<label class="form-check-label" for="remember-me">Запомнить меня</label>
</div>
<div class="mb-3 d-flex flex-row">
<button class="btn btn-primary me-2" type="submit">Войти</button>
<a class="btn btn-secondary" th:href="@{/signup}">Регистрация</a>
</div>
</form>
</div>
</main>
</body>
</html>

View File

@@ -1,86 +0,0 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<title>Редактировать исполнителя</title>
</head>
<body>
<main layout:fragment="content">
<div class="row">
<div class="col-md-8">
<form
th:action="@{/page-artists/edit/{id}(id=${artist.id},page=${page})}"
th:object="${artist}"
method="post"
>
<div th:if="${successSave}" class="alert alert-success mb-2" role="alert">
Данные успешно записаны
</div>
<div class="mb-2" th:with="hasError=${#fields.hasErrors('name')}">
<label for="name" class="form-label">Название</label>
<input
type="text"
th:field="*{name}"
id="name"
class="form-control bg-dark text-light"
th:classappend="${hasError ? 'is-invalid' : ''}"
/>
<div th:if="${hasError}" th:errors="*{name}" class="invalid-feedback"></div>
</div>
<div class="mb-2" th:with="hasError=${#fields.hasErrors('description')}">
<label for="description" class="form-label">Описание</label>
<textarea
th:field="*{description}"
id="description"
class="form-control bg-dark text-light"
rows="4"
th:classappend="${hasError ? 'is-invalid' : ''}"
></textarea>
<div th:if="${hasError}" th:errors="*{description}" class="invalid-feedback"></div>
</div>
<div class="mb-2" th:with="hasError=${#fields.hasErrors('epochId')}">
<label for="epochId" class="form-label">Эпоха</label>
<select
th:field="*{epochId}"
id="epochId"
class="form-select bg-dark text-light"
th:classappend="${hasError ? 'is-invalid' : ''}"
>
<option selected value="">Выберите эпоху</option>
<option
th:each="epoch : ${epochs}"
th:value="${epoch.id}"
th:selected="${epoch.id==epochId}"
th:text="${epoch.name}"
></option>
</select>
<div th:if="${hasError}" th:errors="*{epochId}" class="invalid-feedback"></div>
</div>
<div class="mb-2" th:with="hasError=${#fields.hasErrors('countryId')}">
<label for="countryId" class="form-label">Страна</label>
<select
th:field="*{countryId}"
id="countryId"
class="form-select bg-dark text-light"
th:classappend="${hasError ? 'is-invalid' : ''}"
>
<option selected value="">Выберите страну</option>
<option
th:each="country : ${countries}"
th:value="${country.id}"
th:selected="${country.id==countryId}"
th:text="${country.name}"
></option>
</select>
<div th:if="${hasError}" th:errors="*{countryId}" class="invalid-feedback"></div>
</div>
<div class="text-center">
<button class="btn btn-primary" type="submit">Сохранить</button>
<a class="btn btn-secondary mx-2" th:href="@{/page-artists(page=${page})}">Отмена</a>
</div>
</form>
</div>
</div>
</main>
</body>
</html>

View File

@@ -1,59 +0,0 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<title>Исполнители</title>
</head>
<body>
<main layout:fragment="content">
<div class="mb-3">
<a class="btn btn-primary" th:href="@{/page-artists/edit(page=${page})}">
<i class="bi bi-plus-circle-fill me-2"></i>Создать нового исполнителя
</a>
</div>
<div class="card bg-dark border-punk mb-3">
<div class="card-body">
<h5 class="card-title text-punk">Фильтры</h5>
<form th:action="@{/page-artists/filter}" method="post" class="row g-3">
<div class="col-md-4">
<label for="countryId" class="form-label">Страна</label>
<select id="countryId" name="countryId" class="form-select bg-dark text-light">
<option value="">Все страны</option>
<option
th:each="country : ${countries}"
th:value="${country.id}"
th:selected="${country.id == selectedCountryId}"
th:text="${country.name}"
></option>
</select>
</div>
<div class="col-md-4">
<label for="epochId" class="form-label">Эпоха</label>
<select id="epochId" name="epochId" class="form-select bg-dark text-light">
<option value="">Все эпохи</option>
<option
th:each="epoch : ${epochs}"
th:value="${epoch.id}"
th:selected="${epoch.id == selectedEpochId}"
th:text="${epoch.name}"
></option>
</select>
</div>
<div class="col-md-4 d-flex align-items-end">
<button type="submit" class="btn btn-outline-primary me-2">Применить</button>
<a th:href="@{/page-artists/filter/clear}" class="btn btn-outline-secondary">Сбросить</a>
</div>
</form>
</div>
</div>
<th:block
th:replace="~{ f-artists :: draw (
items=${artists.items},
totalPages=${artists.totalPages},
currentPage=${artists.currentPage}) }"
/>
</main>
</body>
</html>

View File

@@ -1,39 +0,0 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<title>Редактировать страну</title>
</head>
<body>
<main layout:fragment="content">
<div class="row">
<div class="col-md-6">
<form
th:action="@{/page-countries/edit/{id}(id=${country.id},page=${page})}"
th:object="${country}"
method="post"
>
<div th:if="${successSave}" class="alert alert-success mb-2" role="alert">
Данные успешно записаны
</div>
<div class="mb-2" th:with="hasError=${#fields.hasErrors('name')}">
<label for="name" class="form-label">Название</label>
<input
type="text"
th:field="*{name}"
id="name"
class="form-control bg-dark text-light"
th:classappend="${hasError ? 'is-invalid' : ''}"
/>
<div th:if="${hasError}" th:errors="*{name}" class="invalid-feedback"></div>
</div>
<div class="text-center">
<button class="btn btn-primary" type="submit">Сохранить</button>
<a class="btn btn-secondary mx-2" th:href="@{/page-countries(page=${page})}">Отмена</a>
</div>
</form>
</div>
</div>
</main>
</body>
</html>

View File

@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<title>Страны</title>
</head>
<body>
<main layout:fragment="content">
<div class="mb-2">
<a class="btn btn-primary" th:href="@{/page-countries/edit(page=${page})}">
<i class="bi bi-plus-circle-fill me-2"></i>Создать новую страну
</a>
</div>
<th:block
th:replace="~{ f-countries :: draw (
items=${countries.items},
totalPages=${countries.totalPages},
currentPage=${countries.currentPage}) }"
/>
</main>
</body>
</html>

View File

@@ -1,42 +0,0 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<title>Профиль пользователя</title>
</head>
<body>
<main layout:fragment="content">
<div class="container py-4">
<div class="card bg-dark border-punk">
<div class="card-body">
<h3 class="text-punk mb-4">
<i class="bi bi-person-circle"></i> Профиль пользователя
</h3>
<div class="row">
<div class="col-md-8">
<div class="mb-4">
<label class="form-label text-light fw-bold">
<i class="bi bi-person-fill me-2"></i>Имя пользователя:
</label>
<div class="text-light fs-5 ms-4">
<span th:text="${username}">username</span>
</div>
</div>
<div class="mb-4">
<label class="form-label text-light fw-bold">
<i class="bi bi-shield-check me-2"></i>Роль:
</label>
<div class="ms-4">
<span class="badge bg-punk text-dark fs-6" th:text="${role}">USER</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</body>
</html>

View File

@@ -1,53 +0,0 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
<head>
<title>Регистрация</title>
</head>
<body>
<main layout:fragment="content">
<div class="row justify-content-center">
<form action="#" th:action="@{/signup}" th:object="${user}" method="post" class="col col-md-6">
<div class="mb-3" th:with="hasError=${#fields.hasErrors('login')}">
<label for="login" class="form-label">Имя пользователя</label>
<input
type="text"
th:field="*{login}"
id="login"
class="form-control"
th:classappend="${hasError ? 'is-invalid' : ''}"
/>
<div th:if="${hasError}" th:errors="*{login}" class="invalid-feedback"></div>
</div>
<div class="mb-3" th:with="hasError=${#fields.hasErrors('password')}">
<label for="password" class="form-label">Пароль</label>
<input
type="password"
th:field="*{password}"
id="password"
class="form-control"
th:classappend="${hasError ? 'is-invalid' : ''}"
/>
<div th:if="${hasError}" th:errors="*{password}" class="invalid-feedback"></div>
</div>
<div class="mb-3" th:with="hasError=${#fields.hasErrors('passwordConfirm')}">
<label for="passwordConfirm" class="form-label">Пароль (подтверждение)</label>
<input
type="password"
th:field="*{passwordConfirm}"
id="passwordConfirm"
class="form-control"
th:classappend="${hasError ? 'is-invalid' : ''}"
/>
<div th:if="${hasError}" th:errors="*{passwordConfirm}" class="invalid-feedback"></div>
</div>
<div class="mb-3 d-flex flex-row">
<button class="btn btn-primary me-2" type="submit">Регистрация</button>
<a class="btn btn-secondary" href="/login">Отмена</a>
</div>
</form>
</div>
</main>
</body>
</html>

View File

@@ -7,7 +7,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import com.example.demo.dto.ArtistRs;
import com.example.demo.error.NotFoundException;
@@ -15,7 +14,6 @@ import com.example.demo.mapper.ArtistMapper;
@SpringBootTest
@TestMethodOrder(OrderAnnotation.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
public class ArtistServiceTests {
@Autowired
private ArtistService service;
@@ -27,20 +25,12 @@ public class ArtistServiceTests {
private ArtistMapper mapper;
@Test
@Order(1)
void getTest() {
Assertions.assertThrows(NotFoundException.class, () -> service.get(0L));
}
@Test
@Order(2)
void getAllTest() {
Assertions.assertNotNull(service.getAll());
Assertions.assertTrue(service.getAll().isEmpty());
}
@Test
@Order(3)
@Order(1)
void createTest() {
// Создаем необходимые зависимости
final var epochRq1 = new com.example.demo.dto.EpochRq();
@@ -59,56 +49,52 @@ public class ArtistServiceTests {
countryRq2.setName("США");
final var country2 = countryService.create(countryRq2);
final ArtistRs artist1 = service.create(mapper.toRqDto("Artist 1", "Description 1", epoch1.getId(), country1.getId()));
final ArtistRs artist2 = service.create(mapper.toRqDto("Artist 2", "Description 2", epoch2.getId(), country2.getId()));
final ArtistRs artist3 = service.create(mapper.toRqDto("Artist 3", "Description 3", epoch1.getId(), country1.getId()));
service.create(mapper.toRqDto("Artist 1", "Description 1", epoch1.getId(), country1.getId()));
service.create(mapper.toRqDto("Artist 2", "Description 2", epoch2.getId(), country2.getId()));
final ArtistRs last = service.create(mapper.toRqDto("Artist 3", "Description 3", epoch1.getId(), country1.getId()));
Assertions.assertEquals(3, service.getAll().size());
Assertions.assertNotNull(artist1.getId());
Assertions.assertNotNull(artist2.getId());
Assertions.assertNotNull(artist3.getId());
final ArtistRs cmpEntity = service.get(artist3.getId());
Assertions.assertEquals(artist3.getId(), cmpEntity.getId());
Assertions.assertEquals(artist3.getName(), cmpEntity.getName());
final ArtistRs cmpEntity = service.get(3L);
Assertions.assertEquals(last.getId(), cmpEntity.getId());
Assertions.assertEquals(last.getName(), cmpEntity.getName());
}
@Test
@Order(4)
@Order(2)
void updateTest() {
final var allArtists = service.getAll();
Assertions.assertFalse(allArtists.isEmpty());
final ArtistRs entity = allArtists.get(0);
final Long entityId = entity.getId();
final String test = "TEST ARTIST";
final ArtistRs entity = service.get(3L);
final String oldName = entity.getName();
// Используем эпоху и страну из созданных в createTest
final var epoch = entity.getEpoch();
final var country = entity.getCountry();
final String test = "TEST ARTIST";
final ArtistRs newEntity = service.update(entityId, mapper.toRqDto(test, "New Description", epoch.getId(), country.getId()));
final ArtistRs newEntity = service.update(3L, mapper.toRqDto(test, "New Description", epoch.getId(), country.getId()));
Assertions.assertEquals(3, service.getAll().size());
Assertions.assertEquals(test, newEntity.getName());
Assertions.assertNotEquals(oldName, newEntity.getName());
final ArtistRs cmpEntity = service.get(entityId);
final ArtistRs cmpEntity = service.get(3L);
Assertions.assertEquals(newEntity.getId(), cmpEntity.getId());
Assertions.assertEquals(newEntity.getName(), cmpEntity.getName());
}
@Test
@Order(5)
@Order(3)
void deleteTest() {
final var allArtists = service.getAll();
final int initialSize = allArtists.size();
Assertions.assertTrue(initialSize > 0);
final ArtistRs toDelete = allArtists.get(0);
final Long deleteId = toDelete.getId();
service.delete(deleteId);
Assertions.assertEquals(initialSize - 1, service.getAll().size());
Assertions.assertThrows(NotFoundException.class, () -> service.get(deleteId));
service.delete(3L);
Assertions.assertEquals(2, service.getAll().size());
final ArtistRs last = service.get(2L);
Assertions.assertEquals(2L, last.getId());
// Используем эпоху и страну из существующего артиста
final var epoch = last.getEpoch();
final var country = last.getCountry();
final ArtistRs newEntity = service.create(mapper.toRqDto("Artist 4", "Description 4", epoch.getId(), country.getId()));
Assertions.assertEquals(3, service.getAll().size());
Assertions.assertEquals(4L, newEntity.getId());
}
}

View File

@@ -7,7 +7,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import com.example.demo.dto.CountryRs;
import com.example.demo.error.NotFoundException;
@@ -15,7 +14,6 @@ import com.example.demo.mapper.CountryMapper;
@SpringBootTest
@TestMethodOrder(OrderAnnotation.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
public class CountryServiceTests {
@Autowired
private CountryService service;
@@ -23,68 +21,52 @@ public class CountryServiceTests {
private CountryMapper mapper;
@Test
@Order(1)
void getTest() {
Assertions.assertThrows(NotFoundException.class, () -> service.get(0L));
}
@Test
@Order(2)
void getAllTest() {
Assertions.assertNotNull(service.getAll());
Assertions.assertTrue(service.getAll().isEmpty());
}
@Test
@Order(3)
@Order(1)
void createTest() {
final CountryRs country1 = service.create(mapper.toRqDto("Россия"));
final CountryRs country2 = service.create(mapper.toRqDto("США"));
final CountryRs country3 = service.create(mapper.toRqDto("Тайга"));
service.create(mapper.toRqDto("Россия"));
service.create(mapper.toRqDto("США"));
final CountryRs last = service.create(mapper.toRqDto("Тайга"));
Assertions.assertEquals(3, service.getAll().size());
Assertions.assertNotNull(country1.getId());
Assertions.assertNotNull(country2.getId());
Assertions.assertNotNull(country3.getId());
final CountryRs cmpEntity = service.get(country3.getId());
Assertions.assertEquals(country3.getId(), cmpEntity.getId());
Assertions.assertEquals(country3.getName(), cmpEntity.getName());
final CountryRs cmpEntity = service.get(3L);
Assertions.assertEquals(last.getId(), cmpEntity.getId());
Assertions.assertEquals(last.getName(), cmpEntity.getName());
}
@Test
@Order(4)
@Order(2)
void updateTest() {
final var allCountries = service.getAll();
Assertions.assertFalse(allCountries.isEmpty());
final CountryRs entity = allCountries.get(0);
final Long entityId = entity.getId();
final String oldName = entity.getName();
final String test = "TEST";
final CountryRs newEntity = service.update(entityId, mapper.toRqDto(test));
final CountryRs entity = service.get(3L);
final String oldName = entity.getName();
final CountryRs newEntity = service.update(3L, mapper.toRqDto(test));
Assertions.assertEquals(3, service.getAll().size());
Assertions.assertEquals(test, newEntity.getName());
Assertions.assertNotEquals(oldName, newEntity.getName());
final CountryRs cmpEntity = service.get(entityId);
final CountryRs cmpEntity = service.get(3L);
Assertions.assertEquals(newEntity.getId(), cmpEntity.getId());
Assertions.assertEquals(newEntity.getName(), cmpEntity.getName());
}
@Test
@Order(5)
@Order(3)
void deleteTest() {
final var allCountries = service.getAll();
final int initialSize = allCountries.size();
Assertions.assertTrue(initialSize > 0);
final CountryRs toDelete = allCountries.get(0);
final Long deleteId = toDelete.getId();
service.delete(deleteId);
Assertions.assertEquals(initialSize - 1, service.getAll().size());
Assertions.assertThrows(NotFoundException.class, () -> service.get(deleteId));
service.delete(3L);
Assertions.assertEquals(2, service.getAll().size());
final CountryRs last = service.get(2L);
Assertions.assertEquals(2L, last.getId());
final CountryRs newEntity = service.create(mapper.toRqDto("Германия"));
Assertions.assertEquals(3, service.getAll().size());
Assertions.assertEquals(4L, newEntity.getId());
}
}

View File

@@ -7,7 +7,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import com.example.demo.dto.EpochRs;
import com.example.demo.error.NotFoundException;
@@ -15,7 +14,6 @@ import com.example.demo.mapper.EpochMapper;
@SpringBootTest
@TestMethodOrder(OrderAnnotation.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
public class EpochServiceTests {
@Autowired
private EpochService service;
@@ -23,69 +21,53 @@ public class EpochServiceTests {
private EpochMapper mapper;
@Test
@Order(1)
void getTest() {
Assertions.assertThrows(NotFoundException.class, () -> service.get(0L));
}
@Test
@Order(2)
void getAllTest() {
Assertions.assertNotNull(service.getAll());
Assertions.assertTrue(service.getAll().isEmpty());
}
@Test
@Order(3)
@Order(1)
void createTest() {
final EpochRs epoch1 = service.create(mapper.toRqDto("1980-е"));
final EpochRs epoch2 = service.create(mapper.toRqDto("1990-е"));
final EpochRs epoch3 = service.create(mapper.toRqDto("2000-е"));
service.create(mapper.toRqDto("1980-е"));
service.create(mapper.toRqDto("1990-е"));
final EpochRs last = service.create(mapper.toRqDto("2000-е"));
Assertions.assertEquals(3, service.getAll().size());
Assertions.assertNotNull(epoch1.getId());
Assertions.assertNotNull(epoch2.getId());
Assertions.assertNotNull(epoch3.getId());
final EpochRs cmpEntity = service.get(epoch3.getId());
Assertions.assertEquals(epoch3.getId(), cmpEntity.getId());
Assertions.assertEquals(epoch3.getName(), cmpEntity.getName());
final EpochRs cmpEntity = service.get(3L);
Assertions.assertEquals(last.getId(), cmpEntity.getId());
Assertions.assertEquals(last.getName(), cmpEntity.getName());
}
@Test
@Order(4)
@Order(2)
void updateTest() {
final var allEpochs = service.getAll();
Assertions.assertFalse(allEpochs.isEmpty());
final EpochRs entity = allEpochs.get(0);
final Long entityId = entity.getId();
final String oldName = entity.getName();
final String test = "TEST";
final EpochRs newEntity = service.update(entityId, mapper.toRqDto(test));
final EpochRs entity = service.get(3L);
final String oldName = entity.getName();
final EpochRs newEntity = service.update(3L, mapper.toRqDto(test));
Assertions.assertEquals(3, service.getAll().size());
Assertions.assertEquals(test, newEntity.getName());
Assertions.assertNotEquals(oldName, newEntity.getName());
final EpochRs cmpEntity = service.get(entityId);
final EpochRs cmpEntity = service.get(3L);
Assertions.assertEquals(newEntity.getId(), cmpEntity.getId());
Assertions.assertEquals(newEntity.getName(), cmpEntity.getName());
}
@Test
@Order(5)
@Order(3)
void deleteTest() {
final var allEpochs = service.getAll();
final int initialSize = allEpochs.size();
Assertions.assertTrue(initialSize > 0);
final EpochRs toDelete = allEpochs.get(0);
final Long deleteId = toDelete.getId();
service.delete(deleteId);
Assertions.assertEquals(initialSize - 1, service.getAll().size());
Assertions.assertThrows(NotFoundException.class, () -> service.get(deleteId));
service.delete(3L);
Assertions.assertEquals(2, service.getAll().size());
final EpochRs last = service.get(2L);
Assertions.assertEquals(2L, last.getId());
final EpochRs newEntity = service.create(mapper.toRqDto("2010-е"));
Assertions.assertEquals(3, service.getAll().size());
Assertions.assertEquals(4L, newEntity.getId());
}
}

View File

@@ -1,5 +0,0 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver

View File

@@ -1,7 +0,0 @@
spring:
datasource:
url: jdbc:postgresql://127.0.0.1/demo_test
username: postgres
password: postgres
driver-class-name: org.postgresql.Driver

View File

@@ -1,2 +0,0 @@
spring.datasource.url=jdbc:h2:mem:testdb

View File

@@ -1,14 +0,0 @@
logging:
level:
com:
example:
demo: DEBUG
spring:
profiles:
active: prod
jpa:
hibernate:
ddl-auto: create-drop
liquibase:
enabled: false

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,6 @@
"bootstrap": "^5.3.6",
"bootstrap-icons": "^1.13.1",
"react": "^18.2.0",
"react-bootstrap": "^2.10.2",
"react-bootstrap-icons": "^1.11.6",
"react-dom": "^18.2.0",
"react-router-dom": "^7.6.0",

View File

@@ -9,14 +9,16 @@ const ArtistForm = ({ countries = [], epochs = [], onSubmit, artist }) => {
});
useEffect(() => {
console.log('Received countries:', countries);
console.log('Received epochs:', epochs);
console.log('ArtistForm - Received countries:', countries);
console.log('ArtistForm - Received epochs:', epochs);
console.log('ArtistForm - Countries array length:', countries?.length);
console.log('ArtistForm - Epochs array length:', epochs?.length);
if (artist) {
setFormData({
name: artist.name,
description: artist.description,
epochId: artist.epoch?.id || '',
countryId: artist.country?.id || ''
epochId: artist.epoch?.id ? String(artist.epoch.id) : '',
countryId: artist.country?.id ? String(artist.country.id) : ''
});
} else {
setFormData({
@@ -45,8 +47,8 @@ const ArtistForm = ({ countries = [], epochs = [], onSubmit, artist }) => {
onSubmit({
name: formData.name.trim(),
description: formData.description.trim(),
epochId: Number(formData.epochId),
countryId: Number(formData.countryId)
epochId: formData.epochId ? Number(formData.epochId) : null,
countryId: formData.countryId ? Number(formData.countryId) : null
});
if (!artist) {
setFormData({
@@ -100,12 +102,12 @@ const ArtistForm = ({ countries = [], epochs = [], onSubmit, artist }) => {
required
>
<option value="">Выберите эпоху</option>
{epochs.length > 0 ? (
{epochs && epochs.length > 0 ? (
epochs.map(epoch => (
<option key={epoch.id} value={epoch.id}>{epoch.name}</option>
<option key={epoch.id} value={String(epoch.id)}>{epoch.name}</option>
))
) : (
<option disabled>Нет данных</option>
<option disabled>Загрузка эпох... ({epochs?.length || 0} загружено)</option>
)}
</select>
</div>
@@ -120,12 +122,12 @@ const ArtistForm = ({ countries = [], epochs = [], onSubmit, artist }) => {
required
>
<option value="">Выберите страну</option>
{countries.length > 0 ? (
{countries && countries.length > 0 ? (
countries.map(country => (
<option key={country.id} value={country.id}>{country.name}</option>
<option key={country.id} value={String(country.id)}>{country.name}</option>
))
) : (
<option disabled>Нет данных</option>
<option disabled>Загрузка стран... ({countries?.length || 0} загружено)</option>
)}
</select>
</div>

View File

@@ -1,72 +0,0 @@
.punk-pagination {
--bs-pagination-color: var(--punk-primary);
--bs-pagination-bg: transparent;
--bs-pagination-border-color: var(--punk-primary);
--bs-pagination-hover-color: white;
--bs-pagination-hover-bg: var(--punk-primary);
--bs-pagination-hover-border-color: var(--punk-primary);
--bs-pagination-focus-color: white;
--bs-pagination-focus-bg: var(--punk-primary);
--bs-pagination-focus-border-color: var(--punk-primary);
--bs-pagination-active-color: white;
--bs-pagination-active-bg: var(--punk-primary);
--bs-pagination-active-border-color: var(--punk-primary);
--bs-pagination-disabled-color: #6c757d;
--bs-pagination-disabled-bg: transparent;
--bs-pagination-disabled-border-color: #6c757d;
}
.punk-pagination-item {
background-color: transparent !important;
border-color: var(--punk-primary) !important;
color: var(--punk-primary) !important;
transition: all 0.3s ease !important;
}
.punk-pagination-item:hover:not(.active) {
background-color: var(--punk-primary) !important;
color: white !important;
border-color: var(--punk-primary) !important;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(138, 43, 226, 0.4);
}
.punk-pagination-item.active {
background-color: var(--punk-primary) !important;
color: white !important;
border-color: var(--punk-primary) !important;
box-shadow: 0 0 10px rgba(138, 43, 226, 0.6);
}
.punk-pagination-nav {
background-color: transparent !important;
border-color: var(--punk-primary) !important;
color: var(--punk-primary) !important;
transition: all 0.3s ease !important;
}
.punk-pagination-nav:hover:not(:disabled) {
background-color: var(--punk-primary) !important;
color: white !important;
border-color: var(--punk-primary) !important;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(138, 43, 226, 0.4);
}
.punk-pagination-nav:disabled {
opacity: 0.5;
cursor: not-allowed !important;
}
.punk-pagination .page-link {
background-color: transparent;
border-color: var(--punk-primary);
color: var(--punk-primary);
}
.punk-pagination .page-link:hover {
background-color: var(--punk-primary);
color: white;
border-color: var(--punk-primary);
}

View File

@@ -1,40 +0,0 @@
import React from 'react';
import { Pagination as BSPagination } from 'react-bootstrap';
import './Pagination.css';
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
if (totalPages <= 1) return null;
const items = [];
for (let number = 1; number <= totalPages; number++) {
items.push(
<BSPagination.Item
key={number}
active={number === currentPage}
onClick={() => onPageChange(number)}
className="punk-pagination-item"
>
{number}
</BSPagination.Item>,
);
}
return (
<BSPagination className="justify-content-center punk-pagination">
<BSPagination.Prev
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
className="punk-pagination-nav"
/>
{items}
<BSPagination.Next
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="punk-pagination-nav"
/>
</BSPagination>
);
};
export default Pagination;

View File

@@ -3,7 +3,6 @@ import Header from '../components/Header';
import Footer from '../components/Footer';
import ArtistList from '../components/ArtistList';
import ArtistForm from '../components/ArtistForm';
import Pagination from '../components/Pagination';
import { getArtists, getCountries, getEpochs, createArtist, updateArtist, deleteArtist } from '../services/api';
const PunkRockPage = () => {
@@ -12,30 +11,24 @@ const PunkRockPage = () => {
const [epochs, setEpochs] = useState([]);
const [loading, setLoading] = useState(true);
const [editingArtist, setEditingArtist] = useState(null);
const [sortDirection, setSortDirection] = useState('asc');
const [currentPage, setCurrentPage] = useState(1);
const [pageSize] = useState(3);
const [pagination, setPagination] = useState(null);
const [sortDirection, setSortDirection] = useState('asc'); // 'asc' для А-Я, 'desc' для Я-А
useEffect(() => {
const fetchData = async () => {
try {
const [artistsData, countriesData, epochsData] = await Promise.all([
getArtists(currentPage, pageSize),
getArtists(),
getCountries(),
getEpochs()
]);
console.log('Fetched Artists:', artistsData);
console.log('Fetched Countries:', countriesData);
console.log('Fetched Epochs:', epochsData);
setArtists(artistsData.items || []);
setPagination({
currentPage: artistsData.currentPage,
totalPages: artistsData.totalPages,
totalItems: artistsData.totalItems
});
setCountries(countriesData);
setEpochs(epochsData);
console.log('Countries length:', countriesData?.length);
console.log('Epochs length:', epochsData?.length);
setArtists(artistsData || []);
setCountries(countriesData || []);
setEpochs(epochsData || []);
setLoading(false);
} catch (error) {
console.error('Error fetching data:', error);
@@ -43,24 +36,24 @@ const PunkRockPage = () => {
}
};
fetchData();
}, [currentPage, pageSize]);
}, []);
const handleAddArtist = async (artistData) => {
try {
// Проверка уникальности имени
const isDuplicate = artists.some(artist => artist.name.toLowerCase() === artistData.name.toLowerCase());
if (isDuplicate) {
alert('Исполнитель с таким именем уже существует!');
return;
}
const newArtist = await createArtist(artistData);
console.log('Added Artist:', newArtist);
// Получаем обновленные данные для определения последней страницы
const artistsData = await getArtists(1, pageSize);
const newTotalPages = artistsData.totalPages;
// Переходим на последнюю страницу, где будет новый исполнитель
const lastPageData = await getArtists(newTotalPages, pageSize);
setArtists(lastPageData.items || []);
setPagination({
currentPage: lastPageData.currentPage,
totalPages: lastPageData.totalPages,
totalItems: lastPageData.totalItems
setArtists(prevArtists => {
return [...prevArtists, newArtist].sort((a, b) =>
sortDirection === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
);
});
setCurrentPage(newTotalPages);
} catch (error) {
console.error('Error adding artist:', error);
throw error;
@@ -69,15 +62,22 @@ const PunkRockPage = () => {
const handleUpdateArtist = async (artistData) => {
try {
// Проверка уникальности имени (кроме текущего исполнителя)
const isDuplicate = artists.some(artist =>
artist.name.toLowerCase() === artistData.name.toLowerCase() && artist.id !== editingArtist.id
);
if (isDuplicate) {
alert('Исполнитель с таким именем уже существует!');
return;
}
const updatedArtist = await updateArtist(editingArtist.id, artistData);
console.log('Updated Artist:', updatedArtist);
// Перезагружаем текущую страницу
const artistsData = await getArtists(currentPage, pageSize);
setArtists(artistsData.items || []);
setPagination({
currentPage: artistsData.currentPage,
totalPages: artistsData.totalPages,
totalItems: artistsData.totalItems
setArtists(prevArtists => {
return prevArtists.map(a => (a.id === updatedArtist.id ? updatedArtist : a))
.sort((a, b) =>
sortDirection === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
);
});
setEditingArtist(null);
} catch (error) {
@@ -89,18 +89,10 @@ const PunkRockPage = () => {
const handleDeleteArtist = async (id) => {
try {
await deleteArtist(id);
// Перезагружаем текущую страницу
const artistsData = await getArtists(currentPage, pageSize);
setArtists(artistsData.items || []);
setPagination({
currentPage: artistsData.currentPage,
totalPages: artistsData.totalPages,
totalItems: artistsData.totalItems
});
// Если текущая страница стала пустой и это не первая страница, переходим на предыдущую
if (artistsData.items.length === 0 && currentPage > 1) {
setCurrentPage(currentPage - 1);
}
setArtists(artists.filter(artist => artist.id !== id)
.sort((a, b) =>
sortDirection === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
));
} catch (error) {
console.error('Error deleting artist:', error);
throw error;
@@ -111,10 +103,6 @@ const PunkRockPage = () => {
setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc');
};
const handlePageChange = (page) => {
setCurrentPage(page);
};
if (loading) {
return <div className="text-center text-punk my-5">Загрузка...</div>;
}
@@ -147,15 +135,6 @@ const PunkRockPage = () => {
onEdit={setEditingArtist}
onDelete={handleDeleteArtist}
/>
{pagination && (
<div className="mt-4">
<Pagination
currentPage={pagination.currentPage}
totalPages={pagination.totalPages}
onPageChange={handlePageChange}
/>
</div>
)}
</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
const API_URL = 'http://localhost:8080/api';
export const getArtists = async (page = 1, size = 3) => {
const response = await fetch(`${API_URL}/artists?page=${page}&size=${size}`);
export const getArtists = async () => {
const response = await fetch(`${API_URL}/artists`);
if (!response.ok) throw new Error('Ошибка загрузки исполнителей');
return await response.json();
};

Binary file not shown.

BIN
~$bWork2Report.docx Normal file

Binary file not shown.

View File

@@ -50,13 +50,12 @@ out/
# Compiled output
/dist
dist/
/tmp
/out-tsc
/bazel-out
# Node
node_modules/
node_modules
npm-debug.log
yarn-error.log

View File

@@ -6,7 +6,9 @@
"request": "launch",
"cwd": "${workspaceFolder}",
"mainClass": "ru.ulstu.is.server.ServerApplication",
"projectName": "lec6"
"projectName": "lec3",
"envFile": "${workspaceFolder}/.env",
"args": "--populate"
}
]
}

View File

@@ -8,8 +8,8 @@
"editor.tabSize": 4,
"editor.insertSpaces": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
// "source.sortImports": "explicit"
"source.organizeImports": "explicit",
"source.sortImports": "explicit"
},
"editor.snippetSuggestions": "bottom",
"debug.toolBarLocation": "commandCenter",
@@ -36,34 +36,19 @@
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "automatic",
"[java]": {
"editor.semanticHighlighting.enabled": true,
"editor.pasteAs.enabled": false,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
// "source.sortImports": "explicit"
"source.organizeImports": "never",
"source.sortImports": "explicit"
}
},
"gradle.nestedProjects": true,
"java.configuration.updateBuildConfiguration": "automatic",
"java.saveActions.organizeImports": true,
"java.dependency.packagePresentation": "hierarchical",
"java.codeGeneration.hashCodeEquals.useJava7Objects": true,
"java.import.gradle.enabled": true,
"java.import.gradle.offline.enabled": false,
"java.import.gradle.wrapper.enabled": true,
"java.compile.nullAnalysis.mode": "disabled",
"java.import.generatesMetadataFilesAtProjectRoot": false,
"java.completion.favoriteStaticMembers": [
"org.junit.Assert.*",
"org.junit.Assume.*",
"org.junit.jupiter.api.Assertions.*",
"org.mockito.Mockito.*",
"org.mockito.ArgumentMatchers.*",
"org.mockito.Answers.*",
"org.hamcrest.MatcherAssert.*",
"org.hamcrest.Matchers.*"
],
"spring-boot.ls.problem.boot2.JAVA_CONSTRUCTOR_PARAMETER_INJECTION": "WARNING",
"spring.initializr.defaultLanguage": "Java"
}

Some files were not shown because too many files have changed in this diff Show More