Spring Boot 3: doc, test y prep. para impl.: Aula 1

This commit is contained in:
devfzn 2023-09-16 16:31:56 -03:00
parent dfd811fd81
commit 3b667e35cb
Signed by: devfzn
GPG Key ID: E070ECF4A754FDB1
52 changed files with 2334 additions and 9 deletions

View File

@ -33,12 +33,6 @@ public class SecurityConfigurations {
.and() .and()
.addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class)
.build(); .build();
//return httpSecurity.csrf().disable()
// .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// .and().authorizeHttpRequests()
// .requestMatchers(HttpMethod.POST, "/login").permitAll()
// .anyRequest().authenticated()
// .and().build();
} }
@Bean @Bean

View File

@ -30,10 +30,10 @@ public class SecurityFilter extends OncePerRequestFilter {
if (authHeader != null) { if (authHeader != null) {
System.out.println("- ".repeat(10) + "filtro no null"+" -".repeat(10)); System.out.println("- ".repeat(10) + "filtro no null"+" -".repeat(10));
var token = authHeader.replace("Bearer ", ""); var token = authHeader.replace("Bearer ", "");
var subject = tokenService.getSubject(token); var nombreUsuario = tokenService.getSubject(token);
if (subject != null) { if (nombreUsuario != null) {
// token válido // token válido
var usuario = usuarioRepository.findByLogin(subject); var usuario = usuarioRepository.findByLogin(nombreUsuario);
var authentication = new UsernamePasswordAuthenticationToken( var authentication = new UsernamePasswordAuthenticationToken(
usuario, usuario,
null, null,

308
010_spring_boot/api_rest/api3/mvnw vendored Executable file
View File

@ -0,0 +1,308 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.2.0
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /usr/local/etc/mavenrc ] ; then
. /usr/local/etc/mavenrc
fi
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "$(uname)" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
else
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
fi
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=$(java-config --jre-home)
fi
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
[ -n "$CLASSPATH" ] &&
CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
fi
# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="$(which javac)"
if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=$(which readlink)
if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
if $darwin ; then
javaHome="$(dirname "\"$javaExecutable\"")"
javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
else
javaExecutable="$(readlink -f "\"$javaExecutable\"")"
fi
javaHome="$(dirname "\"$javaExecutable\"")"
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
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
else
JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=$(cd "$wdir/.." || exit 1; pwd)
fi
# end of workaround
done
printf '%s' "$(cd "$basedir" || exit 1; pwd)"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
# Remove \r in case we run on Windows within Git Bash
# and check out the repository with auto CRLF management
# enabled. Otherwise, we may read lines that are delimited with
# \r\n and produce $'-Xarg\r' rather than -Xarg due to word
# splitting rules.
tr -s '\r\n' ' ' < "$1"
fi
}
log() {
if [ "$MVNW_VERBOSE" = true ]; then
printf '%s\n' "$1"
fi
}
BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
log "$MAVEN_PROJECTBASEDIR"
##########################################################################################
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
# This allows using the maven wrapper in projects that prohibit checking in binary data.
##########################################################################################
wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
if [ -r "$wrapperJarPath" ]; then
log "Found $wrapperJarPath"
else
log "Couldn't find $wrapperJarPath, downloading it ..."
if [ -n "$MVNW_REPOURL" ]; then
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
else
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
fi
while IFS="=" read -r key value; do
# Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
safeValue=$(echo "$value" | tr -d '\r')
case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
esac
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
log "Downloading from: $wrapperUrl"
if $cygwin; then
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
fi
if command -v wget > /dev/null; then
log "Found wget ... using wget"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
else
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
fi
elif command -v curl > /dev/null; then
log "Found curl ... using curl"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
else
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
fi
else
log "Falling back to using Java to download"
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
# For Cygwin, switch paths to Windows format before running javac
if $cygwin; then
javaSource=$(cygpath --path --windows "$javaSource")
javaClass=$(cygpath --path --windows "$javaClass")
fi
if [ -e "$javaSource" ]; then
if [ ! -e "$javaClass" ]; then
log " - Compiling MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/javac" "$javaSource")
fi
if [ -e "$javaClass" ]; then
log " - Running MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
fi
fi
fi
fi
##########################################################################################
# End of extension
##########################################################################################
# If specified, validate the SHA-256 sum of the Maven wrapper jar file
wrapperSha256Sum=""
while IFS="=" read -r key value; do
case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
esac
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
if [ -n "$wrapperSha256Sum" ]; then
wrapperSha256Result=false
if command -v sha256sum > /dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
wrapperSha256Result=true
fi
elif command -v shasum > /dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
wrapperSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
exit 1
fi
if [ $wrapperSha256Result = false ]; then
echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
exit 1
fi
fi
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
[ -n "$CLASSPATH" ] &&
CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
fi
# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
export MAVEN_CMD_LINE_ARGS
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
# shellcheck disable=SC2086 # safe args
exec "$JAVACMD" \
$MAVEN_OPTS \
$MAVEN_DEBUG_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

205
010_spring_boot/api_rest/api3/mvnw.cmd vendored Normal file
View File

@ -0,0 +1,205 @@
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM https://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.2.0
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM set title of command window
title %0
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
)
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
if exist %WRAPPER_JAR% (
if "%MVNW_VERBOSE%" == "true" (
echo Found %WRAPPER_JAR%
)
) else (
if not "%MVNW_REPOURL%" == "" (
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
)
if "%MVNW_VERBOSE%" == "true" (
echo Couldn't find %WRAPPER_JAR%, downloading it ...
echo Downloading from: %WRAPPER_URL%
)
powershell -Command "&{"^
"$webclient = new-object System.Net.WebClient;"^
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
"}"^
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
"}"
if "%MVNW_VERBOSE%" == "true" (
echo Finished downloading %WRAPPER_JAR%
)
)
@REM End of extension
@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
SET WRAPPER_SHA_256_SUM=""
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
)
IF NOT %WRAPPER_SHA_256_SUM%=="" (
powershell -Command "&{"^
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
" Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
" Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
" Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
" exit 1;"^
"}"^
"}"
if ERRORLEVEL 1 goto error
)
@REM Provide a "standardized" way to retrieve the CLI args that will
@REM work with both Windows and non-Windows executions.
set MAVEN_CMD_LINE_ARGS=%*
%MAVEN_JAVA_EXE% ^
%JVM_CONFIG_MAVEN_PROPS% ^
%MAVEN_OPTS% ^
%MAVEN_DEBUG_OPTS% ^
-classpath %WRAPPER_JAR% ^
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%"=="on" pause
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
cmd /C exit /B %ERROR_CODE%

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>med.voll</groupId>
<artifactId>api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>api</name>
<description>API Rest para clínica Voll</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,13 @@
package med.voll.api;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ApiApplication {
public static void main(String[] args) {
SpringApplication.run(ApiApplication.class, args);
}
}

View File

@ -0,0 +1,38 @@
package med.voll.api.controller;
import jakarta.validation.Valid;
import med.voll.api.domain.usuario.DatosAutenticacionUsuario;
import med.voll.api.domain.usuario.Usuario;
import med.voll.api.infra.security.DatosJWTtoken;
import med.voll.api.infra.security.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/login")
public class AutenticacionController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenService tokenService;
@PostMapping
public ResponseEntity autenticarUsuario(@RequestBody @Valid DatosAutenticacionUsuario datosAutenticacionUsuario) {
Authentication authtoken = new UsernamePasswordAuthenticationToken(
datosAutenticacionUsuario.login(),
datosAutenticacionUsuario.clave());
var usuarioAutenticado = authenticationManager.authenticate(authtoken);
var JWTtoken = tokenService.generarToken((Usuario) usuarioAutenticado.getPrincipal());
return ResponseEntity.ok(new DatosJWTtoken(JWTtoken));
}
}

View File

@ -0,0 +1,32 @@
package med.voll.api.controller;
import jakarta.validation.Valid;
import med.voll.api.domain.consulta.AgendaDeConsultaService;
import med.voll.api.domain.consulta.DatosAgendarConsulta;
import med.voll.api.domain.consulta.DatosDetalleConsulta;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@ResponseBody
@RequestMapping("/consultas")
public class ConsultaController {
@Autowired
private AgendaDeConsultaService service;
@PostMapping
@Transactional // ojo de donde se importa esta anotación
public ResponseEntity agendar(@RequestBody @Valid DatosAgendarConsulta datos) {
System.out.println(datos);
service.agendar(datos);
return ResponseEntity.ok(new DatosDetalleConsulta(null, null, null, null));
}
}

View File

@ -0,0 +1,16 @@
package med.voll.api.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping
public String helloWorld() {
return "Hello World! Test AutoReload";
}
}

View File

@ -0,0 +1,71 @@
package med.voll.api.controller;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import med.voll.api.domain.direccion.DatosDireccion;
import med.voll.api.domain.medico.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
@RestController
@RequestMapping("/medicos")
public class MedicoController {
@Autowired
private MedicoRepository medicoRepository;
@PostMapping
public ResponseEntity<DatosRespuestaMedico> registrarMedico(
@RequestBody @Valid DatosRegistroMedico datosRegistroMedico,
UriComponentsBuilder uriComponentsBuilder) {
Medico medico = medicoRepository.save(new Medico(datosRegistroMedico));
DatosRespuestaMedico datosRespuestaMedico = new DatosRespuestaMedico(
medico, new DatosDireccion(medico.getDireccion())
);
URI url = uriComponentsBuilder.path("/medicos/{id}") .buildAndExpand(medico.getId()).toUri();
return ResponseEntity.created(url).body(datosRespuestaMedico);
}
@GetMapping
public ResponseEntity<Page<DatosListadoMedicos>> listadoMedicos(
@PageableDefault(size = 5) Pageable paginacion) {
return ResponseEntity.ok(medicoRepository.findByActivoTrue(paginacion).map(DatosListadoMedicos::new));
}
@PutMapping
@Transactional
public ResponseEntity<DatosRespuestaMedico> actualizarMedico(
@RequestBody @Valid DatosActualizarMedico datosActualizarMedico) {
Medico medico = medicoRepository.getReferenceById(datosActualizarMedico.id());
medico.actualizarDatos(datosActualizarMedico);
DatosRespuestaMedico datosRespuestaMedico = new DatosRespuestaMedico(
medico, new DatosDireccion(medico.getDireccion())
);
return ResponseEntity.ok(datosRespuestaMedico);
}
@DeleteMapping("/{id}")
@Transactional
public ResponseEntity eliminarMedico(@PathVariable Long id) {
Medico medico = medicoRepository.getReferenceById(id);
medico.desactivarMedico();
return ResponseEntity.noContent().build();
}
@GetMapping("/{id}")
public ResponseEntity<DatosRespuestaMedico> retornaDatosMedico(@PathVariable Long id) {
Medico medico = medicoRepository.getReferenceById(id);
DatosRespuestaMedico datosRespuestaMedico = new DatosRespuestaMedico(
medico, new DatosDireccion(medico.getDireccion())
);
return ResponseEntity.ok(datosRespuestaMedico);
}
}

View File

@ -0,0 +1,76 @@
package med.voll.api.controller;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import med.voll.api.domain.direccion.DatosDireccion;
import med.voll.api.domain.paciente.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
@RestController
@RequestMapping("/pacientes")
public class PacienteController {
/* No es recomendable usar @Autowired a nivel de declaración pues genera
problemas al realizar pruebas unitarias */
@Autowired
private PacienteRepository pacienteRepository;
@PostMapping
public ResponseEntity<DatosRespuestaPaciente> registrarPaciente(
@RequestBody @Valid DatosRegistroPaciente datosRegistroPaciente,
UriComponentsBuilder uriComponentsBuilder) {
Paciente paciente = pacienteRepository.save(new Paciente(datosRegistroPaciente));
DatosRespuestaPaciente datosRespuestaPaciente = new DatosRespuestaPaciente(
paciente, new DatosDireccion(paciente.getDireccion())
);
URI url = uriComponentsBuilder.path("/pacientes/{id}") .buildAndExpand(paciente.getId()).toUri();
return ResponseEntity.created(url).body(datosRespuestaPaciente);
}
@GetMapping
public ResponseEntity<Page<DatosListadoPacientes>> listadoPacientes(
@PageableDefault(size = 5) Pageable paginacion) {
return ResponseEntity.ok(
pacienteRepository.findByActivoTrue(paginacion).map(DatosListadoPacientes::new)
);
}
@PutMapping
@Transactional
public ResponseEntity<DatosRespuestaPaciente> actualizarPaciente(
@RequestBody @Valid DatosActualizarPaciente datosActualizarPaciente) {
Paciente paciente = pacienteRepository.getReferenceById(datosActualizarPaciente.id());
paciente.actualizarDatos(datosActualizarPaciente);
DatosRespuestaPaciente datosRespuestaPaciente = new DatosRespuestaPaciente(
paciente, new DatosDireccion(paciente.getDireccion())
);
return ResponseEntity.ok(datosRespuestaPaciente);
}
// Desactivar Paciente
@DeleteMapping("/{id}")
@Transactional
public ResponseEntity eliminarPaciente(@PathVariable Long id) {
Paciente paciente = pacienteRepository.getReferenceById(id);
paciente.desactivarPaciente();
return ResponseEntity.noContent().build();
}
@GetMapping("/{id}")
public ResponseEntity<DatosRespuestaPaciente> retornaDatosPaciente(@PathVariable Long id) {
Paciente paciente = pacienteRepository.getReferenceById(id);
DatosRespuestaPaciente datosRespuestaPaciente = new DatosRespuestaPaciente(
paciente, new DatosDireccion(paciente.getDireccion())
);
return ResponseEntity.ok(datosRespuestaPaciente);
}
}

View File

@ -0,0 +1,45 @@
package med.voll.api.domain.consulta;
import med.voll.api.domain.medico.Medico;
import med.voll.api.domain.medico.MedicoRepository;
import med.voll.api.domain.paciente.Paciente;
import med.voll.api.domain.paciente.PacienteRepository;
import med.voll.api.infra.errores.ValidacionDeIntegridad;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AgendaDeConsultaService {
@Autowired
private ConsultaRepository consultaRepository;
@Autowired
private MedicoRepository medicoRepository;
@Autowired
private PacienteRepository pacienteRepository;
public void agendar(DatosAgendarConsulta datos) {
if (!pacienteRepository.findById(datos.idPaciente()).isPresent()) {
throw new ValidacionDeIntegridad("Id de paciente no encontrado");
}
if (datos.idMedico() != null && !medicoRepository.existsById(datos.idMedico())) {
throw new ValidacionDeIntegridad("Id de médico no encontrado");
}
var paciente = pacienteRepository.findById(datos.idPaciente()).get();
var medico = seleccionarMedico(datos);
var consulta = new Consulta(null, medico, paciente, datos.fecha());
consultaRepository.save(consulta);
}
private Medico seleccionarMedico(DatosAgendarConsulta datos) {
if (datos.idMedico() != null) {
return medicoRepository.getReferenceById(datos.idMedico());
}
if (datos.especialidad() == null) {
throw new ValidacionDeIntegridad("Debe indicarse una especialidad médica");
}
return medicoRepository.seleccionarMedicoConEspecialidadEnFecha(datos.especialidad(), datos.fecha());
}
}

View File

@ -0,0 +1,35 @@
package med.voll.api.domain.consulta;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import med.voll.api.domain.medico.Medico;
import med.voll.api.domain.paciente.Paciente;
import java.time.LocalDateTime;
@Table(name = "consultas")
@Entity(name = "Consulta")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Consulta {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "medico_id")
private Medico medico;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "paciente_id")
private Paciente paciente;
private LocalDateTime fecha;
}

View File

@ -0,0 +1,8 @@
package med.voll.api.domain.consulta;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ConsultaRepository extends JpaRepository<Consulta, Long> {
}

View File

@ -0,0 +1,14 @@
package med.voll.api.domain.consulta;
import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.NotNull;
import med.voll.api.domain.medico.Especialidad;
import java.time.LocalDateTime;
public record DatosAgendarConsulta(
@NotNull Long idPaciente,
Long idMedico,
@NotNull @Future LocalDateTime fecha,
Especialidad especialidad) {
}

View File

@ -0,0 +1,12 @@
package med.voll.api.domain.consulta;
import java.time.LocalDateTime;
public record DatosDetalleConsulta(Long id, Long idPaciente, Long idMedico, LocalDateTime fecha) {
public DatosDetalleConsulta(Consulta consulta) {
this(consulta.getId(),
consulta.getPaciente().getId(),
consulta.getMedico().getId(),
consulta.getFecha());
}
}

View File

@ -0,0 +1,20 @@
package med.voll.api.domain.direccion;
import jakarta.validation.constraints.NotBlank;
public record DatosDireccion(
@NotBlank String calle,
@NotBlank String distrito,
@NotBlank String ciudad,
String numero,
String complemento) {
public DatosDireccion(Direccion direccion){
this(direccion.getCalle(),
direccion.getDistrito(),
direccion.getCiudad(),
direccion.getNumero(),
direccion.getComplemento()
);
}
}

View File

@ -0,0 +1,36 @@
package med.voll.api.domain.direccion;
import jakarta.persistence.Embeddable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Embeddable
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Direccion {
private String calle;
private String numero;
private String complemento;
private String distrito;
private String ciudad;
public Direccion(DatosDireccion direccion) {
this.calle = direccion.calle();
this.numero = direccion.numero();
this.complemento = direccion.complemento();
this.distrito = direccion.distrito();
this.ciudad = direccion.ciudad();
}
public Direccion actualizarDatosDireccion(DatosDireccion direccion) {
this.calle = direccion.calle();
this.numero = direccion.numero();
this.complemento = direccion.complemento();
this.distrito = direccion.distrito();
this.ciudad = direccion.ciudad();
return this;
}
}

View File

@ -0,0 +1,8 @@
package med.voll.api.domain.medico;
import jakarta.validation.constraints.NotNull;
import med.voll.api.domain.direccion.DatosDireccion;
public record DatosActualizarMedico(@NotNull Long id, String nombre, String documento, DatosDireccion direccion) {
}

View File

@ -0,0 +1,17 @@
package med.voll.api.domain.medico;
public record DatosListadoMedicos(
Long id,
String nombre,
String especialidad,
String documento, String email) {
public DatosListadoMedicos (Medico medico) {
this(medico.getId(),
medico.getNombre(),
medico.getEspecialidad().toString(),
medico.getDocumento(),
medico.getEmail());
}
}

View File

@ -0,0 +1,17 @@
package med.voll.api.domain.medico;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import med.voll.api.domain.direccion.DatosDireccion;
public record DatosRegistroMedico(
@NotBlank String nombre,
@NotBlank @Email String email,
@NotBlank String telefono,
@NotBlank @Pattern(regexp = "\\d{4,6}") String documento,
@NotNull Especialidad especialidad,
@NotNull @Valid DatosDireccion direccion
) {}

View File

@ -0,0 +1,19 @@
package med.voll.api.domain.medico;
import jakarta.validation.constraints.NotNull;
import med.voll.api.domain.direccion.DatosDireccion;
public record DatosRespuestaMedico(@NotNull Long id, String nombre,
String email, String telefono, String documento,
DatosDireccion direccion) {
public DatosRespuestaMedico(Medico medico, DatosDireccion direccion){
this(medico.getId(),
medico.getNombre(),
medico.getEmail(),
medico.getTelefono(),
medico.getDocumento(),
direccion);
}
}

View File

@ -0,0 +1,8 @@
package med.voll.api.domain.medico;
public enum Especialidad {
ORTOPEDIA,
CARDIOLOGIA,
GINECOLOGIA,
PEDIATRIA
}

View File

@ -0,0 +1,57 @@
package med.voll.api.domain.medico;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import med.voll.api.domain.direccion.Direccion;
@Table(name="medicos")
@Entity(name="Medico")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Medico {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nombre;
private String email;
private String telefono;
private String documento;
private Boolean activo;
@Enumerated(EnumType.STRING)
private Especialidad especialidad;
@Embedded
private Direccion direccion;
public Medico(DatosRegistroMedico datosRegistroMedico) {
this.activo = true;
this.nombre = datosRegistroMedico.nombre();
this.email = datosRegistroMedico.email();
this.documento = datosRegistroMedico.documento();
this.telefono = datosRegistroMedico.telefono();
this.especialidad = datosRegistroMedico.especialidad();
this.direccion = new Direccion(datosRegistroMedico.direccion());
}
public void actualizarDatos(DatosActualizarMedico datosActualizarMedico) {
if (datosActualizarMedico.nombre() != null) {
this.nombre = datosActualizarMedico.nombre();
}
if (datosActualizarMedico.documento() != null) {
this.documento = datosActualizarMedico.documento();
}
if (datosActualizarMedico.direccion() != null) {
this.direccion = direccion.actualizarDatosDireccion(datosActualizarMedico.direccion());
}
}
public void desactivarMedico() {
this.activo = false;
}
}

View File

@ -0,0 +1,53 @@
package med.voll.api.domain.medico;
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 java.time.LocalDateTime;
public interface MedicoRepository extends JpaRepository<Medico, Long> {
Page<Medico> findByActivoTrue(Pageable paginacion);
//@Query("select Medico from Medico m where m.activo=true and m.especialidad=:especialidad")
//@Query("""
// SELECT m FROM Medico m
// WHERE m.activo=1 and
// m.especialidad=:especialidad and
// m.id NOT IN(
// SELECT c.medico.id FROM Consulta c
// WHERE c.fecha=:fecha
// ) ORDER BY RAND() LIMIT 1
// """)
//@Query("""
// SELECT m FROM Medico m
// WHERE m.activo=true
// AND m.especialidad=?1 AND m.id NOT IN(
// SELECT c.medico.id FROM Consulta c
// WHERE c.fecha=?2
// ) ORDER BY RAND() LIMIT 1
// """)
@Query("""
select m from Medico m
where m.activo= true\s
and
m.especialidad=:especialidad\s
and
m.id not in( \s
select c.medico.id from Consulta c
where
c.fecha=:fecha
)
order by rand()
limit 1
""")
Medico seleccionarMedicoConEspecialidadEnFecha(Especialidad especialidad, LocalDateTime fecha);
//@Query("""
// select m.activo
// from Medico m
// where m.id=:idMedico
// """)
//Boolean findActivoById(Long idMedico);
}

View File

@ -0,0 +1,8 @@
package med.voll.api.domain.paciente;
import jakarta.validation.constraints.NotNull;
import med.voll.api.domain.direccion.DatosDireccion;
public record DatosActualizarPaciente(@NotNull Long id, String nombre, String documento, DatosDireccion direccion) {
}

View File

@ -0,0 +1,12 @@
package med.voll.api.domain.paciente;
public record DatosListadoPacientes(Long id, String nombre, String documento, String email) {
public DatosListadoPacientes(Paciente paciente) {
this(paciente.getId(),
paciente.getNombre(),
paciente.getDocumento(),
paciente.getEmail());
}
}

View File

@ -0,0 +1,16 @@
package med.voll.api.domain.paciente;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import med.voll.api.domain.direccion.DatosDireccion;
public record DatosRegistroPaciente(
@NotBlank String nombre,
@NotBlank @Email String email,
@NotBlank String telefono,
@NotBlank @Pattern(regexp = "\\d{4,6}") String documento,
@NotNull @Valid DatosDireccion direccion
) {}

View File

@ -0,0 +1,19 @@
package med.voll.api.domain.paciente;
import jakarta.validation.constraints.NotNull;
import med.voll.api.domain.direccion.DatosDireccion;
public record DatosRespuestaPaciente(@NotNull Long id, String nombre,
String email, String telefono, String documento,
DatosDireccion direccion) {
public DatosRespuestaPaciente(Paciente paciente, DatosDireccion direccion){
this(paciente.getId(),
paciente.getNombre(),
paciente.getEmail(),
paciente.getTelefono(),
paciente.getDocumento(),
direccion);
}
}

View File

@ -0,0 +1,54 @@
package med.voll.api.domain.paciente;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import med.voll.api.domain.direccion.Direccion;
@Table(name="pacientes")
@Entity(name="Paciente")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Paciente {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nombre;
private String email;
private String telefono;
private String documento;
private Boolean activo;
@Embedded
private Direccion direccion;
public Paciente(DatosRegistroPaciente datosRegistroPaciente) {
this.activo = true;
this.nombre = datosRegistroPaciente.nombre();
this.email = datosRegistroPaciente.email();
this.documento = datosRegistroPaciente.documento();
this.telefono = datosRegistroPaciente.telefono();
this.direccion = new Direccion(datosRegistroPaciente.direccion());
}
public void actualizarDatos(DatosActualizarPaciente datosActualizarPaciente) {
if (datosActualizarPaciente.nombre() != null) {
this.nombre = datosActualizarPaciente.nombre();
}
if (datosActualizarPaciente.documento() != null) {
this.documento = datosActualizarPaciente.documento();
}
if (datosActualizarPaciente.direccion() != null) {
this.direccion = direccion.actualizarDatosDireccion(datosActualizarPaciente.direccion());
}
}
public void desactivarPaciente() {
this.activo = false;
}
}

View File

@ -0,0 +1,9 @@
package med.voll.api.domain.paciente;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PacienteRepository extends JpaRepository<Paciente, Long> {
Page<Paciente> findByActivoTrue(Pageable paginacion);
}

View File

@ -0,0 +1,5 @@
package med.voll.api.domain.usuario;
public record DatosAutenticacionUsuario(String login, String clave) {
}

View File

@ -0,0 +1,63 @@
package med.voll.api.domain.usuario;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
@Table(name = "usuarios")
@Entity(name = "Usuario")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Usuario implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String login;
private String clave;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public String getPassword() {
return clave;
}
@Override
public String getUsername() {
return login;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}

View File

@ -0,0 +1,9 @@
package med.voll.api.domain.usuario;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
public interface UsuarioRepository extends JpaRepository<Usuario, Long> {
UserDetails findByLogin(String login);
}

View File

@ -0,0 +1,36 @@
package med.voll.api.infra.errores;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class ManejadorDeErrores {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity manejarError404(){
return ResponseEntity.notFound().build();
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity manejarError400(MethodArgumentNotValidException e){
var errores = e.getFieldErrors().stream().map(DatosErrorValidacion::new).toList();
return ResponseEntity.badRequest().body(errores);
}
@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity manejarError500(DataIntegrityViolationException e) {
var errores = e.getMostSpecificCause().getLocalizedMessage();
return ResponseEntity.badRequest().body(errores);
}
private record DatosErrorValidacion(String campo, String error) {
public DatosErrorValidacion(FieldError error) {
this(error.getField(), error.getDefaultMessage());
}
}
}

View File

@ -0,0 +1,7 @@
package med.voll.api.infra.errores;
public class ValidacionDeIntegridad extends RuntimeException {
public ValidacionDeIntegridad(String s) {
super(s);
}
}

View File

@ -0,0 +1,20 @@
package med.voll.api.infra.security;
import med.voll.api.domain.usuario.UsuarioRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class AutenticacionService implements UserDetailsService {
@Autowired
private UsuarioRepository usuarioRepository;
@Override
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
return usuarioRepository.findByLogin(login);
}
}

View File

@ -0,0 +1,4 @@
package med.voll.api.infra.security;
public record DatosJWTtoken(String jwTtoken) {
}

View File

@ -0,0 +1,49 @@
package med.voll.api.infra.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfigurations {
@Autowired
private SecurityFilter securityFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.csrf().disable().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeHttpRequests()
.requestMatchers(HttpMethod.POST, "/login")
.permitAll()
.anyRequest()
.authenticated()
.and()
.addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder () {
return new BCryptPasswordEncoder();
}
}

View File

@ -0,0 +1,47 @@
package med.voll.api.infra.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import med.voll.api.domain.usuario.UsuarioRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class SecurityFilter extends OncePerRequestFilter {
@Autowired
private TokenService tokenService;
@Autowired
private UsuarioRepository usuarioRepository;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
var authHeader = request.getHeader("Authorization");
if (authHeader != null) {
var token = authHeader.replace("Bearer ", "");
var nombreUsuario = tokenService.getSubject(token);
if (nombreUsuario != null) {
// token válido
var usuario = usuarioRepository.findByLogin(nombreUsuario);
var authentication = new UsernamePasswordAuthenticationToken(
usuario,
null,
usuario.getAuthorities() // forzar inicio de sesión
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}

View File

@ -0,0 +1,63 @@
package med.voll.api.infra.security;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import med.voll.api.domain.usuario.Usuario;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
@Service
public class TokenService {
@Value("${api.security.secret}")
private String apiSecret;
public String generarToken(Usuario usuario) {
try {
Algorithm algorithm = Algorithm.HMAC256(apiSecret) ;
return JWT.create()
.withIssuer("voll med")
.withSubject(usuario.getLogin())
.withClaim("id", usuario.getId())
.withExpiresAt(generarFechaExpiracion())
.sign(algorithm);
} catch (JWTCreationException exception){
throw new RuntimeException();
}
}
public String getSubject(String token) {
if (token == null) {
throw new RuntimeException("token nulo");
}
DecodedJWT verifier = null;
try {
Algorithm algorithm = Algorithm.HMAC256(apiSecret);
verifier = JWT.require(algorithm)
// specify an specific claim validations
.withIssuer("voll med")
// reusable verifier instance
.build()
.verify(token);
verifier.getSubject();
} catch (JWTVerificationException exception) {
// Invalid signature/claims
System.out.println(exception.toString());
}
if (verifier.getSubject() == null) {
throw new RuntimeException("Verifier inválido");
}
return verifier.getSubject();
}
private Instant generarFechaExpiracion() {
return LocalDateTime.now().plusHours(2).toInstant(ZoneOffset.of("-03:00"));
}
}

View File

@ -0,0 +1,13 @@
spring.datasource.url=jdbc:mysql://${DB_URL}
spring.datasource.username=${DB_USER}
spring.datasource.password=${DB_PASS}
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
server.error.include-stacktrace=never
api.security.secret=${JWT_SECRET}
spring.main.banner-mode=CONSOLE
spring.output.ansi.enabled=ALWAYS

View File

@ -0,0 +1,17 @@
create table medicos(
id bigint not null auto_increment,
nombre varchar(100) not null,
email varchar(100) not null unique,
documento varchar(6) not null unique,
especialidad varchar(100) not null,
telefono varchar(20) not null,
calle varchar(100) not null,
distrito varchar(100) not null,
complemento varchar(100),
numero varchar(20),
ciudad varchar(100) not null,
primary key(id)
);

View File

@ -0,0 +1,16 @@
create table pacientes(
id bigint not null auto_increment,
nombre varchar(100) not null,
email varchar(100) not null unique,
documento varchar(6) not null unique,
telefono varchar(20) not null,
calle varchar(100) not null,
distrito varchar(100) not null,
complemento varchar(100),
numero varchar(20),
ciudad varchar(100) not null,
primary key(id)
);

View File

@ -0,0 +1,2 @@
alter table medicos add activo tinyint;
update medicos set activo=1;

View File

@ -0,0 +1,2 @@
alter table pacientes add activo tinyint;
update pacientes set activo=1;

View File

@ -0,0 +1,9 @@
create table usuarios(
id bigint not null auto_increment,
login varchar(100) not null unique,
clave varchar(300) not null,
primary key(id)
);

View File

@ -0,0 +1,12 @@
CREATE TABLE consultas(
id BIGINT NOT NULL AUTO_INCREMENT,
medico_id BIGINT NOT NULL,
paciente_id BIGINT NOT NULL,
fecha DATETIME NOT NULL,
PRIMARY KEY(id),
CONSTRAINT fk_consultas_medico_id FOREIGN KEY(medico_id) REFERENCES medicos(id),
CONSTRAINT fk_consultas_paciente_id FOREIGN KEY(paciente_id) REFERENCES pacientes(id)
);

View File

@ -0,0 +1,13 @@
package med.voll.api;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class ApiApplicationTests {
@Test
void contextLoads() {
}
}

View File

@ -951,3 +951,7 @@ Más detalles sobre la función de seguridad del método en la
documentación de documentación de
[Spring Security](https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html) [Spring Security](https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html)
---
Continua en
[Documentar, probar y preparar una API para su implementación](./spring_boot_3.md)

View File

@ -0,0 +1,614 @@
# API Rest Java - Documentar, Probar y Preparar una API para su Implementación
Continuación de
[Buenas Prácticas y Protección de una API Rest](./spring_boot_2.md) donde se
vio:
- Creación de un API Rest
- Crud (Create, Read, Update, Delete)
- Validaciones
- Paginación y orden
- Buenas prácticas REST
- Tratamiento de errores
- Control de acceso con JWT
### Objetivos
- Funcionalidad agendar consultas
- Documentación de la API
- Tests Automatizados
- Build del proyecto
[trello](https://trello.com/b/NiWWC55L/vollmed-api-3ra-parte) -
[figma](https://www.figma.com/file/tWpylp7pB4n8rX1AsKTsY2/Voll_med-FRONT-Mobile)
## Nuevas Funcionalidades
- Controller
- DTOs
- Entidades JPA
- Repository
- Migration
- Security
- Reglas de negocio
Proyecto [Voll_Med API](./api_rest/api3/)
Para implementar esta u otras funcionalidades, siempre es necesario crear los
siguientes tipos de códigos:
- **Controller**(s), para mapear la solicitud de la nueva funcionalidad
- **DTO**s, que representan los datos que llegan y salen de la API
- **Entidad**(es) **JPA**
- **Repository**(s), para aislar el acceso a la base de datos
- **Migration**, para hacer las alteraciones en la base de datos
Estos son los cinco tipos de código que **siempre** se desarrollan para una nueva
funcionalidad. Esto también se aplica al agendamiento de las consultas,
incluyendo un sexto elemento a la lista, ***las reglas de negocio***.
En este proyecto, se implementan las reglas de negocio con algoritmos más
complejos.
### Implementando la funcionalidad
Se desarrolla la funcionalidad por partes. Empezaremos por los primeros cinco
elementos de la lista, que son más básicos. Luego, la parte de reglas de negocio.
Se crea un nuevo `ConsultaController` en el paquete
[`src.main.java.med.voll.api.controller`](./api_rest/api2/src/main/java/med/voll/api/controller).
La idea es tener un **Controller** para recibir esas solicitudes relacionadas con
el agendado de consultas.
Es una clase Controller, con las ***anotaciones*** de Spring `@Controller`,
`@ResponseBody`, `@RequestMapping("consultas")` o `@RestController`. Mapea las
solicitudes que llegan con la **URI** `/consultas`, que debe llamar a este
controller y no a los otros.
Luego, el método anotado con `@PostMapping`. Entonces, la solicitud para programar
una consulta será del tipo **POST**, como en otras funcionalidades.
[ConsultaController](./api_rest/api3/src/main/java/med/voll/api/controller/ConsultaController.java)
```java
@Controller
@ResponseBody
@RequestMapping("/consultas")
public class ConsultaController {
@Autowired
private AgendaDeConsultaService service;
@PostMapping
@Transactional
public ResponseEntity agendar(
@RequestBody @Valid DatosAgendarConsulta datos) {
System.out.println(datos);
service.agendar(datos);
return ResponseEntity.ok(new DatosDetalleConsulta(null, null, null, null));
}
}
```
[DatosAgendarConsulta](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/DatosAgendarConsulta.java)
se trata de un registro similar a los que visto anteriormente. Tiene los campos
que provienen de la API (`Long idMedico`, `Long idPaciente` y
`LocalDateTime fecha`) y las anotaciones de **BEAN validation** `@NotNull` para el
`Id` del paciente y para la `fecha`, además de que la fecha debe ser en el
**futuro** (`@Future`), es decir, no se podrá programar una consulta en días
pasados.
```java
@Controller
@ResponseBody
@RequestMapping("/consultas")
public class ConsultaController {
@Autowired
private AgendaDeConsultaService service;
@PostMapping
@Transactional
public ResponseEntity agendar(@RequestBody @Valid DatosAgendarConsulta datos) {
System.out.println(datos);
service.agendar(datos);
return ResponseEntity.ok(new DatosDetalleConsulta(null, null, null, null));
}
}
```
Al volver al Controller, el otro DTO es el de respuesta, llamado
`DatosDetalleConsulta`. Devuelve el `ID` de la consulta creada, del **médico**,
del **paciente** y la **fecha de la consulta** registrada en el sistema.
En el paquete `src.main.java.med.voll.api.domain`, se crea el subpaquete
`consulta`, que abarca las clases relacionadas con el dominio de consulta.
Entre ellas, la ***entidad JPA***
[Consulta](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/Consulta.java),
que contiene las anotaciones de ***JPA*** y ***Lombok***, así como la
información de la consulta: `medico`, `paciente` y `fecha`.
```java
@Table(name = "consultas")
@Entity(name = "Consulta")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Consulta {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "medico_id")
private Medico medico;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "paciente_id")
private Paciente paciente;
private LocalDateTime fecha;
}
```
En este caso, `medico` y `paciente` son relaciones con las otras entidades
`Medico` y `Paciente`.
También se crea
[ConsultaRepository](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/ConsultaRepository.java),
que está vacío por el momento.
```java
@Repository
public interface ConsultaRepository extends JpaRepository<Consulta, Long> {}
```
Por último, la migración número 6 (`V6`) en
`src.main.java.med.voll.api.resources.db.migration`, que crea la tabla de
consultas.
```sql
CREATE TABLE consultas(
id BIGINT NOT NULL AUTO_INCREMENT,
medico_id BIGINT NOT NULL,
paciente_id BIGINT NOT NULL,
fecha DATETIME NOT NULL,
PRIMARY KEY(id),
CONSTRAINT fk_consultas_medico_id FOREIGN KEY(medico_id)
REFERENCES medicos(id),
CONSTRAINT fk_consultas_paciente_id FOREIGN KEY(paciente_id)
REFERENCES pacientes(id)
);
```
Los campos de `Id` de **consulta**, `Id` de **paciente**, `Id` de **médico** y
`fecha`, donde `medico_id` y `paciente_id` son ***claves foráneas*** que apuntan
a las tablas `medicos` y `pacientes`.
Estos son los códigos estándar para cualquier funcionalidad, con sus respectivos
cambios de acuerdo con el proyecto. Cada uno creará un controlador o una entidad
distinta, pero el funcionamiento es el mismo.
Ahora se puede intentar enviar una solicitud a la dirección `/consultas` y
verificar si se redirige al `ConsultaController` y comprobando el `System.out`
que muestra los datos que llegaron en el JSON de la solicitud.
```json
{
"idPaciente": 1,
"idMedico": 1,
"fecha": "2023-09-14T10:00"
}
```
Esta es la ipmplementación del esqueleto de la funcionalidad. Ahora se deben
implementar las reglas de negocio.
El trabajo es un poco diferente a lo ya realizado con la validación de campos
de formulario vía ***BEAN validation***. Estas validaciones son más complejas.
***¿Cómo implementarlas?***
Observando `ConsultaController.java`, se podrían hacer todas las validaciones
en el método `agendar()`, antes del retorno. Sin embargo, esa no es una buena
práctica.
La clase controller no debe traer las reglas de negocio de la aplicación.
Es solo una clase que controla el flujo de ejecución: cuando llega una solicitud,
llama a la clase X, devuelve la respuesta Y. Si la condición es Z, devuelve otra
respuesta y así sucesivamente. Es decir, solo controla el flujo de ejecución y,
por lo tanto, no debería tener reglas de negocio.
Así, aislando las reglas de negocio, los algoritmos, los cálculos y las
validaciones en otra clase que será llamada por el Controller.
En el paquete `consulta`. Se creas la clase para contener las reglas de agendado
de consultas llamada
[AgendaDeConsultasService](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/AgendaDeConsultaService.java).
El nombre es muy autoexplicativo, esta clase contendrá la agenda de consultas.
Se pueden tener otras funcionalidades en esta clase, relacionadas con el
agendamiento de consultas.
Esta no es una clase ***Controller*** ni una clase de ***configuraciones***. Esta
clase representa un servicio de la aplicación, el de agendado de consultas. Por
lo tanto, será una ***clase de servicios*** (**Service**) y llevará la anotación
`@Service`. El objetivo de esta anotación es declarar el componente de servicio
a Spring Boot.
Dentro de esta clase, se crear un método `public void agendar()`, que recibe
como parámetro el **DTO** `DatosAgendarConsulta`.
```java
@Service
public class AgendaDeConsultaService {
@Autowired
private ConsultaRepository consultaRepository;
@Autowired
private MedicoRepository medicoRepository;
@Autowired
private PacienteRepository pacienteRepository;
public void agendar(DatosAgendarConsulta datos) {
if (pacienteRepository.findById(datos.idPaciente()).isPresent()) {
throw new ValidacionDeIntegridad("Id de paciente no encontrado");
}
if (datos.idMedico() != null && medicoRepository.existsById(datos.idMedico())) {
throw new ValidacionDeIntegridad("Id de médico no encontrado");
}
var paciente = pacienteRepository.findById(datos.idPaciente()).get();
var medico = medicoRepository.findById(datos.idMedico()).get();
var consulta = new Consulta(null, medico, paciente, datos.fecha());
consultaRepository.save(consulta);
}
}
```
La clase Service ejecuta las reglas de negocio y las validaciones de la aplicación.
Esta clase se utliza en `ConsultaController`, con `@Autowired` se comuníca a
Spring que instancie este objeto
Con esto, se inyecta la clase `AgendaDeConsultas` en el Controller. En el método
`agendar` del `Controller`, se obtiene el objeto `agenda` y se llama al método
`agendar()`, pasando como parámetro los datos que llegan al `Controller`.
Todo esto antes del retorno.
El **Controller** recibe la información, hace solo la validación de
***BIN validation*** y llama a la clase **Service** `AgendaDeConsultas`, que
ejecutará las reglas de negocio. Esta es la forma correcta de manejar las reglas
de negocio.
En la clase `AgendaDeConsultas` y están todas las validaciones para el método
`agendar()`.
El requerimiento especifica que se debe recibir la solicitud con los datos de
agendamiento y se deben guardar en la tabla de consultas.
Por lo tanto, se necesita acceder a la base de datos y a la tabla de consultas
en esta clase. Así que se declara un atributo `ConsultaRepository`, llamándolo
`consultaRepository`.
Se usa la anotación `@Autowired` para que el Spring Boot inyecte este repository
en la clase `Service`.
Al final del método `agendar()`, se inserta `consultaRepository.save()` pasandole
un objeto del tipo consulta, la entidad **JPA**. Obviamente, solo se puede llamar
a este método si todas las validaciones se han ejecutado de acuerdo con las
reglas de negocio.
La entidad `Consulta` está anotada con `@AllArgsConstructor` de ***Lombok***,
que genera un constructor con todos los atributos. Se puede usar este mismo
constructor en `AgendamientoDeConsultas`.
El primer parámetro es el `Id null`, ya que es la base de datos la que pasará
el `Id`. El segundo es `medico`, `paciente` y `fecha`. Esta última viene en el
**DTO**, a través del parámetro `datos`.
Sucede que el médico y el paciente no llegan en la solicitud, sino el `Id` del
médico y el `Id` del paciente. Por lo tanto, es necesario establecer el objeto
completo en la entidad y no solo el `Id`.
Por lo tanto, es necesario cargar médico y paciente desde la base de datos.
Se necesita inyectar, entonces, dos Repositories más en `Service`:
`MedicoRepository` y `PacienteRepository`.
En el método `agendar()`, se crea un objeto paciente también. Usando
`pacienteRepository.findById()` para buscar el objeto por `Id`, que está dentro
del DTO datos.
En la solicitud solo viene el `Id`, pero se necesita cargar el objeto completo.
Por lo tanto, se usa el Repository para cargar por el `Id` de la base de datos.
El médico seguirá la misma dinámica (
[AgendaDeConsultasService](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/AgendaDeConsultaService.java)
).
Aparecerá un error de compilación porque el método `findById()` no devuelve la
entidad, sino un Optional. Por lo tanto, al final de la línea, antes del punto
y coma, es necesario incluir `.get()` junto a `findById()`. Esto hace que tome
la entidad cargada.
El método `agendar()` en la clase `Service` obteniene el `Id`, cargar el paciente
y el médico desde la base de datos creando una entidad consulta pasando el médico,
el paciente y la fecha que viene en el DTO, y se guarda en la base de datos.
Pero antes de esto, se necesita escribir el código para realizar todas las
validaciones que forman parte de las reglas de negocio.
A continuación, se aborda cómo realizar las validaciones de la mejor manera
posible.
### Validaciones
Para verificar el ID del paciente, se usa un `if`. La idea es comprobar si el `Id`
del paciente existe. El Repository es el que consulta la base de datos.
Se puede lanzar una excepción dentro del `if`, que muestre un mensaje indicando
el problema. Incluso se puede crear una excepción personalizada para el proyecto
llamada `ValidacaoException()`. Con un mensaje como
`"El ID del paciente proporcionado no existe"` o similar (
[DatosDetalleConsulta](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/DatosDetalleConsulta.java)
).
En *package* `med.voll.api.infra.errores` se crea la clase
[ValidacionDeIntegridad](./api_rest/api3/src/main/java/med/voll/api/infra/errores/ValidacionDeIntegridad.java)
```java
package med.voll.api.infra.errores;
public class ValidacionDeIntegridad extends RuntimeException {
public ValidacionDeIntegridad(String s) {
super(s);
}
}
```
En la clase
[AgendaDeConsultasService](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/AgendaDeConsultaService.java),
se realiza la verificación con. Primero, se verifica si existe un paciente con
el `Id` que está llegando a la base de datos. Si no existe, se lanzará una
excepción con un mensaje de error.
Se utiliza el método de la ***interfaz Repository*** en Spring Data llamado
`existsById`. Realiza una consulta a la base de datos para verificar si existe
un registro con un determinado `Id` que devuelve un booleano, `true` si existe,
`false` si no.
Pasando el parámetro `datos.idMedico()`. Se niega la expresión. Con esto, si no
hay un paciente con el `Id` en la base de datos, se debe detener la ejecución
del resto del código.
Recordando la última regla de negocio.
***La elección del médico es opcional, y en ese caso el sistema debe elegir
aleatoriamente algún médico disponible en la fecha/hora indicada.***
Por lo tanto, es posible que un `Id` de médico no llegue en la solicitud.
No se puede llamar a `existsById` si el `Id` del médico es nulo. Esto resultará
en un error para **JPA**.
Solo se puede llamar al `if` si el `Id` no es nulo. Por lo tanto, se agrega una
condición al if antes de la condición actual:
```java
if(datos.idMedico()!=null && !medicoRepository.existsById(datos.idMedico())){
throw new ValidacionDeIntegridad("Id de medico no encontrado");
}
```
En el caso del médico, al ser un campo opcional, puede ser que la línea
`var medico = medicoRepository.findById(dados.idMedico()).get()` tenga un
`idMedico` nulo.
De acuerdo con la regla de negocio analizada, se necesita escribir un algoritmo
que elija aleatoriamente un médico en la base de datos. Por lo tanto, la línea
anterior necesita ser reemplazada. Pare ello se llama al método privado
`seleccionarMedico(datos)` que recibe un objeto `DatosAgendarConsulta` como
parametro y retorna un objeto `Medico`.
Esto sirve para aislar el algoritmo y evitar que esté suelto dentro del método
de programación de citas. En el método `seleccionarMedico()`, se necesita
verificar si está llegando el `Id` del médico en la solicitud o no.
Si lo está, se obtiene el médico correspondiente de la base de datos. Si no es
así, se debe elegir aleatoriamente un profesional de la salud, según lo indica
la regla de negocio.
Para implementar el algoritmo que elige al médico de manera aleatoria, se deben
cubrir todos los posibles escenarios. La primera comprobación es que si la
persona eligió un médico al hacer la solicitud, usando
`if (dados.idMedico() != null)`.
En este caso, se carga la información de la base de datos con
`return medicoRepository.getReferenceById(dados.idMedico())`. En lugar de usar
`findById()`, se podemos usar `getReferenceById()` y no es necesario llamar al
`.get()` usamdo anteriormente.
También se puedemo cambiar `findById()` por `getReferenceById()` en la variable
paciente, ya que no se quiere cargar el objeto para manipularlo, sino, solo para
asignarlo a otro objeto.
En el método `seleccionarMedico()` , lo primero es verificar si se realiza la
con un médico específico para su atención. Si es así, simplemente se carga la
información del médico que viene de la base de datos.
[DatosAgendarConsulta](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/DatosAgendarConsulta.java)
```java
public record DatosAgendarConsulta(
@NotNull Long idPaciente,
Long idMedico,
@NotNull @Future LocalDateTime fecha,
Especialidad especialidad) {
}
```
***¿Cómo elegir un médico aleatorio de la especialidad elegida, disponible en
la fecha y hora seleccionadas?***
Existen varias formas de hacer esto. Se podrían cargar los médicos, filtrarlos por
especialidad y comprobar la fecha de la consulta en Java.
Lo ideal es cargar un profesional aleatorio directamente de la base de datos.
Sin embargo, esta consulta es específica para nuestro proyecto, es decir, no está
lista en Spring Data JPA.
Se necesita crear un método para hacer esto:
```java
return medicoRepository.seleccionarMedicoConEspecialidadEnFecha(
datos.especialidad(),datos.fecha()
);
```
Creación del método `seleccionarMedicoConEspecialidadEnFecha(especialidad, fecha)`
en
[MedicoRepository](./api_rest/api3/src/main/java/med/voll/api/domain/medico/MedicoRepository.java).
Como el nombre del método está en español. No se estam siguiendo el estándar
de nomenclatura, como el `findAllByAtivoTrue()`. De esta manera, Spring Data no
podrá construir el SQL automáticamente. La idea es precisamente esa, para este método,
se escribe el comando SQL manualmente.
Para hacerlo, se usa la anotación `@Query()` justo encima del método. Que viene
del paquete `org.springframework.data.jpa`. Y entre paréntesis, se construye la
consulta utilizando la sintaxis del ***Java Persistence Query Language (JPQL)***.
```java
@Query("""
select m from Medico m
where m.activo= true\s
and
m.especialidad=:especialidad\s
and
m.id not in( \s
select c.medico.id from Consulta c
where
c.fecha=:fecha
)
order by rand()
limit 1
""")
Medico seleccionarMedicoConEspecialidadEnFecha(
Especialidad especialidad,
LocalDateTime fecha
);
}
```
### En resumen
Creación de nuevo *package*
[`domain.consulta`](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/)
donde se crean entidad **Consulta**, clases `ConsultaRepository`,
`DatosAgendarConsulta` y `DatosDetalleConsulta`.
- [Consulta](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/Consulta.java)
- [ConsultaRepository](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/ConsultaRepository.java)
- [DatosAgendarConsulta](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/DatosAgendarConsulta.java)
- [DatosDetalleConsulta](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/DatosDetalleConsulta.java)
- [AgendaDeConsultasService](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/AgendaDeConsultaService.java)
- [migración](./api_rest/api3/src/main/resources/db/migration/V6__create-table-consultas.sql)
---
### Anotación @JsonAlias
En caso que uno o mas campos enviados en el formato JSON a la API no correspondan
con los atributos de las clases DTO, se pueden utilizar alias
```java
public record DatosCompra(
@JsonAlias("producto_id") Long idProducto,
@JsonAlias("fecha_compra") LocalDate fechaCompra
){}
```
La anotación `@JsonAlias` sirve para mapear *alias* alternativos para los campos
que se recibirán del JSON, y es posible asignar múltiples alias:
```java
public record DatosCompra(
@JsonAlias({"producto_id", "id_producto"}) Long idProducto,
@JsonAlias({"fecha_compra", "fecha"}) LocalDate fechaCompra
){}
```
### Formato fechas
Spring usa un formato definido para las fechas `LocalDateTime`, sin embargo, estas
se pueden personalizar. Por ejemplo, para aceptar el formato `dd/mm/yyy hh:mm`.
Para ello se utiliza la anotación `@JsonFormat`
```java
@NotNull
@Future
@JsonFormat(pattern = "dd/MM/yyyy HH:mm")
LocalDateTime fecha
```
Esta anotación también se puede utilizar en las clases DTO que representan la
información que devuelve la API, para que el JSON devuelto se formatee de
acuerdo con el patrón configurado.
Además, no se limita solo a la clase `LocalDateTime`, sino que también se puede
utilizar en atributos de tipo `LocalDate` y `LocalTime`.
---
### Patrón Service
El ***Service pattern*** es muy utilizado en la programación y su nombre es muy
conocido. Pero a pesar de ser un nombre único, **Service** puede ser interpretado
de varias maneras:
- Puede ser un caso de uso, **Application Service**
- Un **Domain Service**, que tiene reglas de su dominio
- Un **Infrastructure Service**, que utiliza algún paquete externo para realizar
tareas
- etc
A pesar de que la interpretación puede ocurrir de varias formas, la idea detrás
del patrón es separar las reglas de negocio, las reglas de la aplicación y las
reglas de presentación para que puedan ser fácilmente probadas y reutilizadas
en otras partes del sistema.
Existen dos formas más utilizadas para crear **Services**. Puede crear
**Services** más genéricos, responsables de todas las asignaciones de un
**Controller**. O ser aún más específico, aplicando así la S del ***SOLID***:
***Single Responsibility Principle*** (*Principio de Responsabilidad Única*).
Este principio nos dice que una clase/función/archivo debe tener sólo una
única responsabilidad.
Piense en un sistema de ventas, en el que probablemente tendríamos algunas
funciones como:
- Registrar usuario
- Iniciar sesión
- Buscar productos
- Buscar producto por nombre
- etc
Entonces, se podrían crear los siguientes **Services**:
- RegistroDeUsuarioService
- IniciarSesionService
- BusquedaDeProductosService
- etc
Pero es importante estar atentos, ya que muchas veces no es necesario crear un
**Service** y, por lo tanto, agregar otra capa y complejidad innecesarias a
una aplicación. Una regla que podemos utilizar es la siguiente: si no hay reglas
de negocio, simplemente se puede realizar la comunicación directa entre los
controllers y los repositories de la aplicación.
---

View File

@ -52,3 +52,4 @@ primoridiales en programación con Javascript
- [JPA consultas avanzadas, rendimiento y modelos complejos](./010_spring_boot/jpa_avanzado.md) - [JPA consultas avanzadas, rendimiento y modelos complejos](./010_spring_boot/jpa_avanzado.md)
- [Desarrollo API Rest](./010_spring_boot/spring_boot_1.md) - [Desarrollo API Rest](./010_spring_boot/spring_boot_1.md)
- [Buenas prácticas y protección de API Rest](./010_spring_boot/spring_boot_2.md) - [Buenas prácticas y protección de API Rest](./010_spring_boot/spring_boot_2.md)
- [Docs, Tests y preparación para implementacion](./010_spring_boot/spring_boot_3.md)