From 3b667e35cb39f451b20f874e96f217e86b82a955 Mon Sep 17 00:00:00 2001 From: devfzn Date: Sat, 16 Sep 2023 16:31:56 -0300 Subject: [PATCH] Spring Boot 3: doc, test y prep. para impl.: Aula 1 --- .../security/SecurityConfigurations.java | 6 - .../api/infra/security/SecurityFilter.java | 6 +- 010_spring_boot/api_rest/api3/mvnw | 308 +++++++++ 010_spring_boot/api_rest/api3/mvnw.cmd | 205 ++++++ 010_spring_boot/api_rest/api3/pom.xml | 99 +++ .../java/med/voll/api/ApiApplication.java | 13 + .../controller/AutenticacionController.java | 38 ++ .../api/controller/ConsultaController.java | 32 + .../voll/api/controller/HelloController.java | 16 + .../voll/api/controller/MedicoController.java | 71 ++ .../api/controller/PacienteController.java | 76 +++ .../consulta/AgendaDeConsultaService.java | 45 ++ .../voll/api/domain/consulta/Consulta.java | 35 + .../domain/consulta/ConsultaRepository.java | 8 + .../domain/consulta/DatosAgendarConsulta.java | 14 + .../domain/consulta/DatosDetalleConsulta.java | 12 + .../api/domain/direccion/DatosDireccion.java | 20 + .../voll/api/domain/direccion/Direccion.java | 36 + .../domain/medico/DatosActualizarMedico.java | 8 + .../domain/medico/DatosListadoMedicos.java | 17 + .../domain/medico/DatosRegistroMedico.java | 17 + .../domain/medico/DatosRespuestaMedico.java | 19 + .../voll/api/domain/medico/Especialidad.java | 8 + .../med/voll/api/domain/medico/Medico.java | 57 ++ .../api/domain/medico/MedicoRepository.java | 53 ++ .../paciente/DatosActualizarPaciente.java | 8 + .../paciente/DatosListadoPacientes.java | 12 + .../paciente/DatosRegistroPaciente.java | 16 + .../paciente/DatosRespuestaPaciente.java | 19 + .../voll/api/domain/paciente/Paciente.java | 54 ++ .../domain/paciente/PacienteRepository.java | 9 + .../usuario/DatosAutenticacionUsuario.java | 5 + .../med/voll/api/domain/usuario/Usuario.java | 63 ++ .../api/domain/usuario/UsuarioRepository.java | 9 + .../api/infra/errores/ManejadorDeErrores.java | 36 + .../infra/errores/ValidacionDeIntegridad.java | 7 + .../infra/security/AutenticacionService.java | 20 + .../api/infra/security/DatosJWTtoken.java | 4 + .../security/SecurityConfigurations.java | 49 ++ .../api/infra/security/SecurityFilter.java | 47 ++ .../voll/api/infra/security/TokenService.java | 63 ++ .../src/main/resources/application.properties | 13 + .../db/migration/V1__create-table-medicos.sql | 17 + .../migration/V2__create-table-pacientes.sql | 16 + .../V3__alter-table-medicos-add-activo.sql | 2 + .../V4__alter-table-pacientes-add-activo.sql | 2 + .../migration/V5__create-table-usuarios.sql | 9 + .../migration/V6__create-table-consultas.sql | 12 + .../med/voll/api/ApiApplicationTests.java | 13 + 010_spring_boot/spring_boot_2.md | 4 + 010_spring_boot/spring_boot_3.md | 614 ++++++++++++++++++ README.md | 1 + 52 files changed, 2334 insertions(+), 9 deletions(-) create mode 100755 010_spring_boot/api_rest/api3/mvnw create mode 100644 010_spring_boot/api_rest/api3/mvnw.cmd create mode 100644 010_spring_boot/api_rest/api3/pom.xml create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/ApiApplication.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/AutenticacionController.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/ConsultaController.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/HelloController.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/MedicoController.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/PacienteController.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/AgendaDeConsultaService.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/Consulta.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/ConsultaRepository.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/DatosAgendarConsulta.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/DatosDetalleConsulta.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/direccion/DatosDireccion.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/direccion/Direccion.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/DatosActualizarMedico.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/DatosListadoMedicos.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/DatosRegistroMedico.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/DatosRespuestaMedico.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/Especialidad.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/Medico.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/MedicoRepository.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/DatosActualizarPaciente.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/DatosListadoPacientes.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/DatosRegistroPaciente.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/DatosRespuestaPaciente.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/Paciente.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/PacienteRepository.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/usuario/DatosAutenticacionUsuario.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/usuario/Usuario.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/usuario/UsuarioRepository.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/errores/ManejadorDeErrores.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/errores/ValidacionDeIntegridad.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/security/AutenticacionService.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/security/DatosJWTtoken.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/security/SecurityFilter.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/security/TokenService.java create mode 100644 010_spring_boot/api_rest/api3/src/main/resources/application.properties create mode 100644 010_spring_boot/api_rest/api3/src/main/resources/db/migration/V1__create-table-medicos.sql create mode 100644 010_spring_boot/api_rest/api3/src/main/resources/db/migration/V2__create-table-pacientes.sql create mode 100644 010_spring_boot/api_rest/api3/src/main/resources/db/migration/V3__alter-table-medicos-add-activo.sql create mode 100644 010_spring_boot/api_rest/api3/src/main/resources/db/migration/V4__alter-table-pacientes-add-activo.sql create mode 100644 010_spring_boot/api_rest/api3/src/main/resources/db/migration/V5__create-table-usuarios.sql create mode 100644 010_spring_boot/api_rest/api3/src/main/resources/db/migration/V6__create-table-consultas.sql create mode 100644 010_spring_boot/api_rest/api3/src/test/java/med/voll/api/ApiApplicationTests.java diff --git a/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java b/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java index 3ef98a2..2e20567 100644 --- a/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java +++ b/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java @@ -33,12 +33,6 @@ public class SecurityConfigurations { .and() .addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class) .build(); - //return httpSecurity.csrf().disable() - // .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) - // .and().authorizeHttpRequests() - // .requestMatchers(HttpMethod.POST, "/login").permitAll() - // .anyRequest().authenticated() - // .and().build(); } @Bean diff --git a/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/infra/security/SecurityFilter.java b/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/infra/security/SecurityFilter.java index a6da087..4618062 100644 --- a/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/infra/security/SecurityFilter.java +++ b/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/infra/security/SecurityFilter.java @@ -30,10 +30,10 @@ public class SecurityFilter extends OncePerRequestFilter { if (authHeader != null) { System.out.println("- ".repeat(10) + "filtro no null"+" -".repeat(10)); var token = authHeader.replace("Bearer ", ""); - var subject = tokenService.getSubject(token); - if (subject != null) { + var nombreUsuario = tokenService.getSubject(token); + if (nombreUsuario != null) { // token válido - var usuario = usuarioRepository.findByLogin(subject); + var usuario = usuarioRepository.findByLogin(nombreUsuario); var authentication = new UsernamePasswordAuthenticationToken( usuario, null, diff --git a/010_spring_boot/api_rest/api3/mvnw b/010_spring_boot/api_rest/api3/mvnw new file mode 100755 index 0000000..66df285 --- /dev/null +++ b/010_spring_boot/api_rest/api3/mvnw @@ -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 "$@" diff --git a/010_spring_boot/api_rest/api3/mvnw.cmd b/010_spring_boot/api_rest/api3/mvnw.cmd new file mode 100644 index 0000000..95ba6f5 --- /dev/null +++ b/010_spring_boot/api_rest/api3/mvnw.cmd @@ -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% diff --git a/010_spring_boot/api_rest/api3/pom.xml b/010_spring_boot/api_rest/api3/pom.xml new file mode 100644 index 0000000..cdec0b2 --- /dev/null +++ b/010_spring_boot/api_rest/api3/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.1.3 + + + med.voll + api + 0.0.1-SNAPSHOT + api + API Rest para clínica Voll + + 17 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.flywaydb + flyway-core + + + org.flywaydb + flyway-mysql + + + + com.mysql + mysql-connector-j + runtime + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.security + spring-security-test + test + + + + com.auth0 + java-jwt + 4.4.0 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/ApiApplication.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/ApiApplication.java new file mode 100644 index 0000000..e08ef48 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/ApiApplication.java @@ -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); + } + +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/AutenticacionController.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/AutenticacionController.java new file mode 100644 index 0000000..53d93ba --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/AutenticacionController.java @@ -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)); + } + +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/ConsultaController.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/ConsultaController.java new file mode 100644 index 0000000..14e7caf --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/ConsultaController.java @@ -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)); + } +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/HelloController.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/HelloController.java new file mode 100644 index 0000000..e4d9877 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/HelloController.java @@ -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"; + } + +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/MedicoController.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/MedicoController.java new file mode 100644 index 0000000..6e1f110 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/MedicoController.java @@ -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 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> listadoMedicos( + @PageableDefault(size = 5) Pageable paginacion) { + return ResponseEntity.ok(medicoRepository.findByActivoTrue(paginacion).map(DatosListadoMedicos::new)); + } + + @PutMapping + @Transactional + public ResponseEntity 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 retornaDatosMedico(@PathVariable Long id) { + Medico medico = medicoRepository.getReferenceById(id); + DatosRespuestaMedico datosRespuestaMedico = new DatosRespuestaMedico( + medico, new DatosDireccion(medico.getDireccion()) + ); + return ResponseEntity.ok(datosRespuestaMedico); + } + +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/PacienteController.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/PacienteController.java new file mode 100644 index 0000000..3b978ee --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/PacienteController.java @@ -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 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> listadoPacientes( + @PageableDefault(size = 5) Pageable paginacion) { + return ResponseEntity.ok( + pacienteRepository.findByActivoTrue(paginacion).map(DatosListadoPacientes::new) + ); + } + + @PutMapping + @Transactional + public ResponseEntity 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 retornaDatosPaciente(@PathVariable Long id) { + Paciente paciente = pacienteRepository.getReferenceById(id); + DatosRespuestaPaciente datosRespuestaPaciente = new DatosRespuestaPaciente( + paciente, new DatosDireccion(paciente.getDireccion()) + ); + return ResponseEntity.ok(datosRespuestaPaciente); + } + +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/AgendaDeConsultaService.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/AgendaDeConsultaService.java new file mode 100644 index 0000000..bb154c6 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/AgendaDeConsultaService.java @@ -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()); + } +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/Consulta.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/Consulta.java new file mode 100644 index 0000000..f5f39f8 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/Consulta.java @@ -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; + +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/ConsultaRepository.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/ConsultaRepository.java new file mode 100644 index 0000000..9f6ef4f --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/ConsultaRepository.java @@ -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 { +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/DatosAgendarConsulta.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/DatosAgendarConsulta.java new file mode 100644 index 0000000..abafbca --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/DatosAgendarConsulta.java @@ -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) { +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/DatosDetalleConsulta.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/DatosDetalleConsulta.java new file mode 100644 index 0000000..4c75389 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/DatosDetalleConsulta.java @@ -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()); + } +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/direccion/DatosDireccion.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/direccion/DatosDireccion.java new file mode 100644 index 0000000..5e35446 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/direccion/DatosDireccion.java @@ -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() + ); + } +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/direccion/Direccion.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/direccion/Direccion.java new file mode 100644 index 0000000..1c2073e --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/direccion/Direccion.java @@ -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; + } +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/DatosActualizarMedico.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/DatosActualizarMedico.java new file mode 100644 index 0000000..1eece23 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/DatosActualizarMedico.java @@ -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) { + +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/DatosListadoMedicos.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/DatosListadoMedicos.java new file mode 100644 index 0000000..89c365b --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/DatosListadoMedicos.java @@ -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()); + } + +} \ No newline at end of file diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/DatosRegistroMedico.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/DatosRegistroMedico.java new file mode 100644 index 0000000..5812ccc --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/DatosRegistroMedico.java @@ -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 +) {} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/DatosRespuestaMedico.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/DatosRespuestaMedico.java new file mode 100644 index 0000000..cdf1753 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/DatosRespuestaMedico.java @@ -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); + } + +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/Especialidad.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/Especialidad.java new file mode 100644 index 0000000..44d3d85 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/Especialidad.java @@ -0,0 +1,8 @@ +package med.voll.api.domain.medico; + +public enum Especialidad { + ORTOPEDIA, + CARDIOLOGIA, + GINECOLOGIA, + PEDIATRIA +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/Medico.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/Medico.java new file mode 100644 index 0000000..c5c0d83 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/Medico.java @@ -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; + } + +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/MedicoRepository.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/MedicoRepository.java new file mode 100644 index 0000000..b9efddf --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/MedicoRepository.java @@ -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 { + Page 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); +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/DatosActualizarPaciente.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/DatosActualizarPaciente.java new file mode 100644 index 0000000..fd52cd8 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/DatosActualizarPaciente.java @@ -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) { + +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/DatosListadoPacientes.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/DatosListadoPacientes.java new file mode 100644 index 0000000..cd280bc --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/DatosListadoPacientes.java @@ -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()); + } + +} \ No newline at end of file diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/DatosRegistroPaciente.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/DatosRegistroPaciente.java new file mode 100644 index 0000000..2a0e13f --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/DatosRegistroPaciente.java @@ -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 +) {} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/DatosRespuestaPaciente.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/DatosRespuestaPaciente.java new file mode 100644 index 0000000..9cfba30 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/DatosRespuestaPaciente.java @@ -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); + } + +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/Paciente.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/Paciente.java new file mode 100644 index 0000000..5cea5e6 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/Paciente.java @@ -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; + } + +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/PacienteRepository.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/PacienteRepository.java new file mode 100644 index 0000000..ab69c37 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/PacienteRepository.java @@ -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 { + Page findByActivoTrue(Pageable paginacion); +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/usuario/DatosAutenticacionUsuario.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/usuario/DatosAutenticacionUsuario.java new file mode 100644 index 0000000..6881ca6 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/usuario/DatosAutenticacionUsuario.java @@ -0,0 +1,5 @@ +package med.voll.api.domain.usuario; + +public record DatosAutenticacionUsuario(String login, String clave) { + +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/usuario/Usuario.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/usuario/Usuario.java new file mode 100644 index 0000000..d005768 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/usuario/Usuario.java @@ -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 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; + } +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/usuario/UsuarioRepository.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/usuario/UsuarioRepository.java new file mode 100644 index 0000000..f7c70d8 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/usuario/UsuarioRepository.java @@ -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 { + UserDetails findByLogin(String login); +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/errores/ManejadorDeErrores.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/errores/ManejadorDeErrores.java new file mode 100644 index 0000000..fe6db0c --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/errores/ManejadorDeErrores.java @@ -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()); + } + } +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/errores/ValidacionDeIntegridad.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/errores/ValidacionDeIntegridad.java new file mode 100644 index 0000000..32604b4 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/errores/ValidacionDeIntegridad.java @@ -0,0 +1,7 @@ +package med.voll.api.infra.errores; + +public class ValidacionDeIntegridad extends RuntimeException { + public ValidacionDeIntegridad(String s) { + super(s); + } +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/security/AutenticacionService.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/security/AutenticacionService.java new file mode 100644 index 0000000..14f0add --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/security/AutenticacionService.java @@ -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); + } +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/security/DatosJWTtoken.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/security/DatosJWTtoken.java new file mode 100644 index 0000000..f586187 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/security/DatosJWTtoken.java @@ -0,0 +1,4 @@ +package med.voll.api.infra.security; + +public record DatosJWTtoken(String jwTtoken) { +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java new file mode 100644 index 0000000..2e20567 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java @@ -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(); + } + +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/security/SecurityFilter.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/security/SecurityFilter.java new file mode 100644 index 0000000..2a2be54 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/security/SecurityFilter.java @@ -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); + } + +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/security/TokenService.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/security/TokenService.java new file mode 100644 index 0000000..ea6c199 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/security/TokenService.java @@ -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")); + } +} diff --git a/010_spring_boot/api_rest/api3/src/main/resources/application.properties b/010_spring_boot/api_rest/api3/src/main/resources/application.properties new file mode 100644 index 0000000..88e1c83 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/resources/application.properties @@ -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 \ No newline at end of file diff --git a/010_spring_boot/api_rest/api3/src/main/resources/db/migration/V1__create-table-medicos.sql b/010_spring_boot/api_rest/api3/src/main/resources/db/migration/V1__create-table-medicos.sql new file mode 100644 index 0000000..2601507 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/resources/db/migration/V1__create-table-medicos.sql @@ -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) + +); \ No newline at end of file diff --git a/010_spring_boot/api_rest/api3/src/main/resources/db/migration/V2__create-table-pacientes.sql b/010_spring_boot/api_rest/api3/src/main/resources/db/migration/V2__create-table-pacientes.sql new file mode 100644 index 0000000..540c256 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/resources/db/migration/V2__create-table-pacientes.sql @@ -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) + +); \ No newline at end of file diff --git a/010_spring_boot/api_rest/api3/src/main/resources/db/migration/V3__alter-table-medicos-add-activo.sql b/010_spring_boot/api_rest/api3/src/main/resources/db/migration/V3__alter-table-medicos-add-activo.sql new file mode 100644 index 0000000..7b42ad7 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/resources/db/migration/V3__alter-table-medicos-add-activo.sql @@ -0,0 +1,2 @@ +alter table medicos add activo tinyint; +update medicos set activo=1; \ No newline at end of file diff --git a/010_spring_boot/api_rest/api3/src/main/resources/db/migration/V4__alter-table-pacientes-add-activo.sql b/010_spring_boot/api_rest/api3/src/main/resources/db/migration/V4__alter-table-pacientes-add-activo.sql new file mode 100644 index 0000000..68589f4 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/resources/db/migration/V4__alter-table-pacientes-add-activo.sql @@ -0,0 +1,2 @@ +alter table pacientes add activo tinyint; +update pacientes set activo=1; \ No newline at end of file diff --git a/010_spring_boot/api_rest/api3/src/main/resources/db/migration/V5__create-table-usuarios.sql b/010_spring_boot/api_rest/api3/src/main/resources/db/migration/V5__create-table-usuarios.sql new file mode 100644 index 0000000..bf6e084 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/resources/db/migration/V5__create-table-usuarios.sql @@ -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) + +); \ No newline at end of file diff --git a/010_spring_boot/api_rest/api3/src/main/resources/db/migration/V6__create-table-consultas.sql b/010_spring_boot/api_rest/api3/src/main/resources/db/migration/V6__create-table-consultas.sql new file mode 100644 index 0000000..8e94b48 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/resources/db/migration/V6__create-table-consultas.sql @@ -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) + +); \ No newline at end of file diff --git a/010_spring_boot/api_rest/api3/src/test/java/med/voll/api/ApiApplicationTests.java b/010_spring_boot/api_rest/api3/src/test/java/med/voll/api/ApiApplicationTests.java new file mode 100644 index 0000000..eb360a5 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/test/java/med/voll/api/ApiApplicationTests.java @@ -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() { + } + +} diff --git a/010_spring_boot/spring_boot_2.md b/010_spring_boot/spring_boot_2.md index e7f1d7d..a733dc7 100644 --- a/010_spring_boot/spring_boot_2.md +++ b/010_spring_boot/spring_boot_2.md @@ -951,3 +951,7 @@ Más detalles sobre la función de seguridad del método en la documentación de [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) diff --git a/010_spring_boot/spring_boot_3.md b/010_spring_boot/spring_boot_3.md index e69de29..d9d7944 100644 --- a/010_spring_boot/spring_boot_3.md +++ b/010_spring_boot/spring_boot_3.md @@ -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 {} +``` + +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. + +--- + + diff --git a/README.md b/README.md index fe28296..74c4d7b 100644 --- a/README.md +++ b/README.md @@ -52,3 +52,4 @@ primoridiales en programación con Javascript - [JPA consultas avanzadas, rendimiento y modelos complejos](./010_spring_boot/jpa_avanzado.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) + - [Docs, Tests y preparación para implementacion](./010_spring_boot/spring_boot_3.md)