+ Criterio: ${result.criteria || criteria}
+ Algoritmo: ${result.algorithm || algorithmName}
+ Total de Perros: ${dogs.length}
+
`;
+
+ if (dogs.length > 0) {
+ html += '
Lista Ordenada:
';
+ dogs.forEach((perro, index) => {
+ let value;
+ if (criteria === 'priority') value = perro.priority;
+ else if (criteria === 'age') value = perro.age;
+ else if (criteria === 'weight') value = perro.weight;
+
+ html += `
Sistema de gestión y optimización de adopciones de perros utilizando algoritmos avanzados.
+
+
+
+
+
Refugios
+
-
+
Total de refugios
+
+
+
Perros
+
-
+
Disponibles para adopción
+
+
+
Adoptantes
+
-
+
Personas registradas
+
+
+
+
+
+
Refugios Disponibles
+
+
+
+
Perros Disponibles
+
+
+
+
Adoptantes Registrados
+
+
+
+
+
+
+
+
Algoritmos de Rutas y Grafos
+
+
+
+
🔍 BFS / DFS - Búsqueda de Caminos
+
Encuentra si existe un camino entre dos refugios usando BFS (anchura) o DFS (profundidad).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
📍 Dijkstra - Camino Más Corto
+
Calcula el camino más corto entre dos refugios considerando las distancias.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
🗺️ TSP Branch & Bound - Ruta Óptima
+
Encuentra la ruta más corta que visita todos los refugios seleccionados (Problema del Viajante). Nota: Seleccionar muchos refugios (>8) puede tomar tiempo.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Algoritmos de Redes y MST
+
+
+
+
🗺️ Visualización del Grafo de Shelters
+
Visualiza la red completa de conexiones entre refugios con sus distancias.
+
+
+
+
+
📋 Leyenda
+
+
Nodo (Shelter)
+
Conexión NEAR
+
💡 Tip: Arrastra los nodos para reorganizar
+
🔍 Peso: Distancia en kilómetros
+
+
+
+
+
+
+
+
🌳 MST - Árbol de Expansión Mínima
+
Calcula el árbol de expansión mínima para conectar todos los refugios con la menor distancia total.
+
+
+
+
+
+
+
+
+
+
+
Algoritmos de Matching
+
+
+
+
🎯 Greedy - Selección Voraz
+
Selecciona los mejores perros para un adoptante usando un algoritmo voraz basado en compatibilidad.
+
+
+
+
+
+
+
+
+
+
+
🔄 Backtracking - Asignación con Restricciones
+
Asigna múltiples perros a múltiples adoptantes respetando todas las restricciones (presupuesto, capacidad, compatibilidad).
+
+
+
+
+
+
+
+
Algoritmos de Ordenamiento
+
+
+
📊 Ordenar Perros
+
Ordena la lista de perros según diferentes criterios usando MergeSort o QuickSort.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Optimización de Transporte
+
+
+
📦 Knapsack - Problema de la Mochila
+
Optimiza el transporte de perros maximizando la prioridad total dentro de la capacidad del vehículo (Programación Dinámica).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Procesando...
+
+
+
+
+
diff --git a/jdk_21_maven/cs/rest/adoptme/frontend/styles.css b/jdk_21_maven/cs/rest/adoptme/frontend/styles.css
new file mode 100644
index 000000000..295f1baee
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/frontend/styles.css
@@ -0,0 +1,608 @@
+/* Reset and Base Styles */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ /* Color Palette - NO PURPLE */
+ --primary-blue: #2196F3;
+ --primary-blue-dark: #1976D2;
+ --primary-blue-light: #BBDEFB;
+
+ --success-green: #4CAF50;
+ --success-green-light: #C8E6C9;
+
+ --warning-orange: #FF9800;
+ --warning-orange-light: #FFE0B2;
+
+ --error-red: #F44336;
+ --error-red-light: #FFCDD2;
+
+ --neutral-gray: #757575;
+ --neutral-light: #F5F5F5;
+ --neutral-border: #E0E0E0;
+
+ --background: #FAFAFA;
+ --card-bg: #FFFFFF;
+ --text-primary: #212121;
+ --text-secondary: #757575;
+
+ /* Spacing */
+ --spacing-xs: 0.5rem;
+ --spacing-sm: 1rem;
+ --spacing-md: 1.5rem;
+ --spacing-lg: 2rem;
+ --spacing-xl: 3rem;
+
+ /* Border Radius */
+ --radius-sm: 4px;
+ --radius-md: 8px;
+ --radius-lg: 12px;
+
+ /* Shadows */
+ --shadow-sm: 0 1px 3px rgba(0,0,0,0.12);
+ --shadow-md: 0 4px 6px rgba(0,0,0,0.1);
+ --shadow-lg: 0 10px 20px rgba(0,0,0,0.15);
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ background: var(--background);
+ color: var(--text-primary);
+ line-height: 1.6;
+}
+
+/* App Container */
+.app-container {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+/* Header */
+.header {
+ background: linear-gradient(135deg, var(--primary-blue) 0%, var(--primary-blue-dark) 100%);
+ color: white;
+ padding: var(--spacing-lg) var(--spacing-md);
+ box-shadow: var(--shadow-md);
+}
+
+.header-content {
+ max-width: 1200px;
+ margin: 0 auto;
+ text-align: center;
+}
+
+.header h1 {
+ font-size: 2.5rem;
+ margin-bottom: var(--spacing-xs);
+ font-weight: 700;
+}
+
+.subtitle {
+ font-size: 1.1rem;
+ opacity: 0.95;
+ margin-bottom: var(--spacing-md);
+}
+
+.connection-status {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--spacing-xs);
+ background: rgba(255, 255, 255, 0.2);
+ padding: var(--spacing-xs) var(--spacing-md);
+ border-radius: var(--radius-lg);
+ font-size: 0.9rem;
+}
+
+.status-indicator {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: var(--warning-orange);
+ animation: pulse 2s infinite;
+}
+
+.status-indicator.connected {
+ background: var(--success-green);
+ animation: none;
+}
+
+.status-indicator.error {
+ background: var(--error-red);
+ animation: none;
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.5; }
+}
+
+/* Navigation Tabs */
+.nav-tabs {
+ background: var(--card-bg);
+ border-bottom: 2px solid var(--neutral-border);
+ display: flex;
+ justify-content: center;
+ flex-wrap: wrap;
+ gap: var(--spacing-xs);
+ padding: var(--spacing-sm);
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ box-shadow: var(--shadow-sm);
+}
+
+.tab-button {
+ background: none;
+ border: none;
+ padding: var(--spacing-sm) var(--spacing-md);
+ font-size: 1rem;
+ color: var(--text-secondary);
+ cursor: pointer;
+ border-bottom: 3px solid transparent;
+ transition: all 0.3s ease;
+ font-weight: 500;
+}
+
+.tab-button:hover {
+ color: var(--primary-blue);
+ background: var(--primary-blue-light);
+ border-radius: var(--radius-sm);
+}
+
+.tab-button.active {
+ color: var(--primary-blue);
+ border-bottom-color: var(--primary-blue);
+}
+
+/* Main Content */
+.main-content {
+ flex: 1;
+ max-width: 1200px;
+ width: 100%;
+ margin: 0 auto;
+ padding: var(--spacing-lg);
+}
+
+.tab-content {
+ display: none;
+ animation: fadeIn 0.3s ease-in;
+}
+
+.tab-content.active {
+ display: block;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* Welcome Section */
+.welcome-section {
+ background: linear-gradient(135deg, var(--success-green) 0%, #388E3C 100%);
+ color: white;
+ padding: var(--spacing-xl);
+ border-radius: var(--radius-lg);
+ margin-bottom: var(--spacing-lg);
+ box-shadow: var(--shadow-md);
+ text-align: center;
+}
+
+.welcome-section h2 {
+ font-size: 2rem;
+ margin-bottom: var(--spacing-sm);
+}
+
+/* Stats Grid */
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: var(--spacing-md);
+ margin-bottom: var(--spacing-lg);
+}
+
+.stat-card {
+ background: var(--card-bg);
+ padding: var(--spacing-lg);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-md);
+ text-align: center;
+ transition: transform 0.2s ease;
+ border-left: 4px solid var(--primary-blue);
+}
+
+.stat-card:hover {
+ transform: translateY(-5px);
+ box-shadow: var(--shadow-lg);
+}
+
+.stat-card:nth-child(2) {
+ border-left-color: var(--success-green);
+}
+
+.stat-card:nth-child(3) {
+ border-left-color: var(--warning-orange);
+}
+
+.stat-card h3 {
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ margin-bottom: var(--spacing-sm);
+}
+
+.stat-number {
+ font-size: 3rem;
+ font-weight: 700;
+ color: var(--primary-blue);
+ margin-bottom: var(--spacing-xs);
+}
+
+.stat-card:nth-child(2) .stat-number {
+ color: var(--success-green);
+}
+
+.stat-card:nth-child(3) .stat-number {
+ color: var(--warning-orange);
+}
+
+.stat-label {
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+}
+
+/* Data Display */
+.data-display {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: var(--spacing-md);
+}
+
+.data-section {
+ background: var(--card-bg);
+ padding: var(--spacing-md);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-md);
+}
+
+.data-section h3 {
+ color: var(--primary-blue);
+ margin-bottom: var(--spacing-md);
+ padding-bottom: var(--spacing-sm);
+ border-bottom: 2px solid var(--neutral-border);
+}
+
+.data-list {
+ max-height: 400px;
+ overflow-y: auto;
+}
+
+.data-item {
+ padding: var(--spacing-sm);
+ margin-bottom: var(--spacing-sm);
+ background: var(--neutral-light);
+ border-radius: var(--radius-md);
+ border-left: 3px solid var(--primary-blue);
+}
+
+.data-item strong {
+ color: var(--primary-blue);
+}
+
+.data-item p {
+ font-size: 0.9rem;
+ color: var(--text-secondary);
+ margin-top: var(--spacing-xs);
+}
+
+/* Algorithm Sections */
+.algorithm-section {
+ background: var(--card-bg);
+ padding: var(--spacing-lg);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-md);
+ margin-bottom: var(--spacing-lg);
+}
+
+.algorithm-section h3 {
+ color: var(--primary-blue-dark);
+ margin-bottom: var(--spacing-sm);
+ font-size: 1.5rem;
+}
+
+.algorithm-section > p {
+ color: var(--text-secondary);
+ margin-bottom: var(--spacing-md);
+ line-height: 1.8;
+}
+
+/* Input Groups */
+.input-group {
+ margin-bottom: var(--spacing-md);
+}
+
+.input-group label {
+ display: block;
+ margin-bottom: var(--spacing-xs);
+ color: var(--text-primary);
+ font-weight: 500;
+}
+
+.helper-text {
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ font-weight: normal;
+ font-style: italic;
+}
+
+.input-group select,
+.input-group input {
+ width: 100%;
+ padding: var(--spacing-sm);
+ border: 2px solid var(--neutral-border);
+ border-radius: var(--radius-md);
+ font-size: 1rem;
+ transition: border-color 0.2s ease;
+}
+
+.input-group select:focus,
+.input-group input:focus {
+ outline: none;
+ border-color: var(--primary-blue);
+}
+
+/* Checkbox Group */
+.checkbox-group {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+ gap: var(--spacing-sm);
+ padding: var(--spacing-md);
+ background: var(--neutral-light);
+ border-radius: var(--radius-md);
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.checkbox-group label {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-xs);
+ cursor: pointer;
+ padding: var(--spacing-xs);
+ background: white;
+ border-radius: var(--radius-sm);
+ transition: background-color 0.2s ease;
+}
+
+.checkbox-group label:hover {
+ background: var(--primary-blue-light);
+}
+
+.checkbox-group input[type="checkbox"] {
+ width: auto;
+ cursor: pointer;
+}
+
+.checkbox-group input[type="checkbox"]:checked + label,
+.checkbox-group label:has(input[type="checkbox"]:checked) {
+ background: var(--primary-blue-light);
+ font-weight: 500;
+}
+
+/* Buttons */
+.btn {
+ padding: var(--spacing-sm) var(--spacing-lg);
+ border: none;
+ border-radius: var(--radius-md);
+ font-size: 1rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ box-shadow: var(--shadow-sm);
+}
+
+.btn-primary {
+ background: var(--primary-blue);
+ color: white;
+}
+
+.btn-primary:hover {
+ background: var(--primary-blue-dark);
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-md);
+}
+
+.btn-primary:active {
+ transform: translateY(0);
+}
+
+.btn-secondary {
+ background: var(--neutral-light);
+ color: var(--text-primary);
+ border: 2px solid var(--neutral-border);
+ padding: calc(var(--spacing-sm) - 2px) calc(var(--spacing-lg) - 2px);
+}
+
+.btn-secondary:hover {
+ background: var(--neutral-border);
+ border-color: var(--primary-blue);
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-md);
+}
+
+.btn-secondary:active {
+ transform: translateY(0);
+}
+
+.button-group {
+ display: flex;
+ gap: var(--spacing-sm);
+ flex-wrap: wrap;
+ margin-bottom: var(--spacing-md);
+}
+
+/* Result Box */
+.result-box {
+ margin-top: var(--spacing-md);
+ padding: var(--spacing-md);
+ border-radius: var(--radius-md);
+ display: none;
+}
+
+.result-box.visible {
+ display: block;
+ animation: slideDown 0.3s ease;
+}
+
+@keyframes slideDown {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.result-box.success {
+ background: var(--success-green-light);
+ border-left: 4px solid var(--success-green);
+}
+
+.result-box.error {
+ background: var(--error-red-light);
+ border-left: 4px solid var(--error-red);
+}
+
+.result-box.info {
+ background: var(--primary-blue-light);
+ border-left: 4px solid var(--primary-blue);
+}
+
+.result-box h4 {
+ margin-bottom: var(--spacing-sm);
+ color: var(--text-primary);
+}
+
+.result-box pre {
+ background: rgba(0, 0, 0, 0.05);
+ padding: var(--spacing-sm);
+ border-radius: var(--radius-sm);
+ overflow-x: auto;
+ font-size: 0.9rem;
+}
+
+.result-item {
+ padding: var(--spacing-sm);
+ margin-bottom: var(--spacing-xs);
+ background: rgba(255, 255, 255, 0.7);
+ border-radius: var(--radius-sm);
+}
+
+/* Footer */
+.footer {
+ background: var(--text-primary);
+ color: white;
+ text-align: center;
+ padding: var(--spacing-md);
+ margin-top: var(--spacing-xl);
+}
+
+/* Loading Overlay */
+.loading-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.7);
+ display: none;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ z-index: 9999;
+}
+
+.loading-overlay.active {
+ display: flex;
+}
+
+.spinner {
+ width: 50px;
+ height: 50px;
+ border: 5px solid rgba(255, 255, 255, 0.3);
+ border-top-color: white;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+.loading-overlay p {
+ color: white;
+ margin-top: var(--spacing-md);
+ font-size: 1.1rem;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .header h1 {
+ font-size: 2rem;
+ }
+
+ .nav-tabs {
+ justify-content: flex-start;
+ overflow-x: auto;
+ }
+
+ .tab-button {
+ white-space: nowrap;
+ }
+
+ .main-content {
+ padding: var(--spacing-md);
+ }
+
+ .stats-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .data-display {
+ grid-template-columns: 1fr;
+ }
+
+ .button-group {
+ flex-direction: column;
+ }
+
+ .button-group .btn {
+ width: 100%;
+ }
+}
+
+/* Scrollbar Styling */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--neutral-light);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--neutral-gray);
+ border-radius: var(--radius-sm);
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--primary-blue);
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/mvnw b/jdk_21_maven/cs/rest/adoptme/mvnw
new file mode 100644
index 000000000..bd8896bf2
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/mvnw
@@ -0,0 +1,295 @@
+#!/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
+#
+# http://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.3.4
+#
+# Optional ENV vars
+# -----------------
+# JAVA_HOME - location of a JDK home dir, required when download maven via java source
+# MVNW_REPOURL - repo url base for downloading maven distribution
+# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
+# ----------------------------------------------------------------------------
+
+set -euf
+[ "${MVNW_VERBOSE-}" != debug ] || set -x
+
+# OS specific support.
+native_path() { printf %s\\n "$1"; }
+case "$(uname)" in
+CYGWIN* | MINGW*)
+ [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
+ native_path() { cygpath --path --windows "$1"; }
+ ;;
+esac
+
+# set JAVACMD and JAVACCMD
+set_java_home() {
+ # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
+ 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"
+ JAVACCMD="$JAVA_HOME/jre/sh/javac"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ JAVACCMD="$JAVA_HOME/bin/javac"
+
+ if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
+ echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
+ echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
+ return 1
+ fi
+ fi
+ else
+ JAVACMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v java
+ )" || :
+ JAVACCMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v javac
+ )" || :
+
+ if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
+ echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
+ return 1
+ fi
+ fi
+}
+
+# hash string like Java String::hashCode
+hash_string() {
+ str="${1:-}" h=0
+ while [ -n "$str" ]; do
+ char="${str%"${str#?}"}"
+ h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
+ str="${str#?}"
+ done
+ printf %x\\n $h
+}
+
+verbose() { :; }
+[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
+
+die() {
+ printf %s\\n "$1" >&2
+ exit 1
+}
+
+trim() {
+ # MWRAPPER-139:
+ # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
+ # Needed for removing poorly interpreted newline sequences when running in more
+ # exotic environments such as mingw bash on Windows.
+ printf "%s" "${1}" | tr -d '[:space:]'
+}
+
+scriptDir="$(dirname "$0")"
+scriptName="$(basename "$0")"
+
+# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
+while IFS="=" read -r key value; do
+ case "${key-}" in
+ distributionUrl) distributionUrl=$(trim "${value-}") ;;
+ distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
+ esac
+done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+
+case "${distributionUrl##*/}" in
+maven-mvnd-*bin.*)
+ MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
+ case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
+ *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
+ :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
+ :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
+ :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
+ *)
+ echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
+ distributionPlatform=linux-amd64
+ ;;
+ esac
+ distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
+ ;;
+maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
+*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
+esac
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
+[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
+distributionUrlName="${distributionUrl##*/}"
+distributionUrlNameMain="${distributionUrlName%.*}"
+distributionUrlNameMain="${distributionUrlNameMain%-bin}"
+MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
+MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
+
+exec_maven() {
+ unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
+ exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
+}
+
+if [ -d "$MAVEN_HOME" ]; then
+ verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ exec_maven "$@"
+fi
+
+case "${distributionUrl-}" in
+*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
+*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
+esac
+
+# prepare tmp dir
+if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
+ clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
+ trap clean HUP INT TERM EXIT
+else
+ die "cannot create temp dir"
+fi
+
+mkdir -p -- "${MAVEN_HOME%/*}"
+
+# Download and Install Apache Maven
+verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+verbose "Downloading from: $distributionUrl"
+verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+# select .zip or .tar.gz
+if ! command -v unzip >/dev/null; then
+ distributionUrl="${distributionUrl%.zip}.tar.gz"
+ distributionUrlName="${distributionUrl##*/}"
+fi
+
+# verbose opt
+__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
+[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
+
+# normalize http auth
+case "${MVNW_PASSWORD:+has-password}" in
+'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+esac
+
+if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
+ verbose "Found wget ... using wget"
+ wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
+elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
+ verbose "Found curl ... using curl"
+ curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
+elif set_java_home; then
+ verbose "Falling back to use Java to download"
+ javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
+ targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
+ cat >"$javaSource" <<-END
+ public class Downloader extends java.net.Authenticator
+ {
+ protected java.net.PasswordAuthentication getPasswordAuthentication()
+ {
+ return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
+ }
+ public static void main( String[] args ) throws Exception
+ {
+ setDefault( new Downloader() );
+ java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
+ }
+ }
+ END
+ # For Cygwin/MinGW, switch paths to Windows format before running javac and java
+ verbose " - Compiling Downloader.java ..."
+ "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
+ verbose " - Running Downloader.java ..."
+ "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
+fi
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+if [ -n "${distributionSha256Sum-}" ]; then
+ distributionSha256Result=false
+ if [ "$MVN_CMD" = mvnd.sh ]; then
+ echo "Checksum validation is not supported for maven-mvnd." >&2
+ echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ elif command -v sha256sum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ elif command -v shasum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
+ echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ fi
+ if [ $distributionSha256Result = false ]; then
+ echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
+ echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+# unzip and move
+if command -v unzip >/dev/null; then
+ unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
+else
+ tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
+fi
+
+# Find the actual extracted directory name (handles snapshots where filename != directory name)
+actualDistributionDir=""
+
+# First try the expected directory name (for regular distributions)
+if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
+ if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
+ actualDistributionDir="$distributionUrlNameMain"
+ fi
+fi
+
+# If not found, search for any directory with the Maven executable (for snapshots)
+if [ -z "$actualDistributionDir" ]; then
+ # enable globbing to iterate over items
+ set +f
+ for dir in "$TMP_DOWNLOAD_DIR"/*; do
+ if [ -d "$dir" ]; then
+ if [ -f "$dir/bin/$MVN_CMD" ]; then
+ actualDistributionDir="$(basename "$dir")"
+ break
+ fi
+ fi
+ done
+ set -f
+fi
+
+if [ -z "$actualDistributionDir" ]; then
+ verbose "Contents of $TMP_DOWNLOAD_DIR:"
+ verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
+ die "Could not find Maven distribution directory in extracted archive"
+fi
+
+verbose "Found extracted Maven distribution directory: $actualDistributionDir"
+printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
+
+clean || :
+exec_maven "$@"
diff --git a/jdk_21_maven/cs/rest/adoptme/mvnw.cmd b/jdk_21_maven/cs/rest/adoptme/mvnw.cmd
new file mode 100644
index 000000000..92450f932
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/mvnw.cmd
@@ -0,0 +1,189 @@
+<# : batch portion
+@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 http://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.3.4
+@REM
+@REM Optional ENV vars
+@REM MVNW_REPOURL - repo url base for downloading maven distribution
+@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
+@REM ----------------------------------------------------------------------------
+
+@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
+@SET __MVNW_CMD__=
+@SET __MVNW_ERROR__=
+@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
+@SET PSModulePath=
+@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
+ IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
+)
+@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
+@SET __MVNW_PSMODULEP_SAVE=
+@SET __MVNW_ARG0_NAME__=
+@SET MVNW_USERNAME=
+@SET MVNW_PASSWORD=
+@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
+@echo Cannot start maven from wrapper >&2 && exit /b 1
+@GOTO :EOF
+: end batch / begin powershell #>
+
+$ErrorActionPreference = "Stop"
+if ($env:MVNW_VERBOSE -eq "true") {
+ $VerbosePreference = "Continue"
+}
+
+# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
+$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
+if (!$distributionUrl) {
+ Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+}
+
+switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
+ "maven-mvnd-*" {
+ $USE_MVND = $true
+ $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
+ $MVN_CMD = "mvnd.cmd"
+ break
+ }
+ default {
+ $USE_MVND = $false
+ $MVN_CMD = $script -replace '^mvnw','mvn'
+ break
+ }
+}
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
+if ($env:MVNW_REPOURL) {
+ $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
+ $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
+}
+$distributionUrlName = $distributionUrl -replace '^.*/',''
+$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
+
+$MAVEN_M2_PATH = "$HOME/.m2"
+if ($env:MAVEN_USER_HOME) {
+ $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
+}
+
+if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
+ New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
+}
+
+$MAVEN_WRAPPER_DISTS = $null
+if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
+ $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
+} else {
+ $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
+}
+
+$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
+$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
+
+if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
+ Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+ exit $?
+}
+
+if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
+ Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
+}
+
+# prepare tmp dir
+$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
+$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
+$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
+trap {
+ if ($TMP_DOWNLOAD_DIR.Exists) {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+ }
+}
+
+New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
+
+# Download and Install Apache Maven
+Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+Write-Verbose "Downloading from: $distributionUrl"
+Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+$webclient = New-Object System.Net.WebClient
+if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
+ $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
+}
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
+if ($distributionSha256Sum) {
+ if ($USE_MVND) {
+ Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
+ }
+ Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
+ if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
+ Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
+ }
+}
+
+# unzip and move
+Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
+
+# Find the actual extracted directory name (handles snapshots where filename != directory name)
+$actualDistributionDir = ""
+
+# First try the expected directory name (for regular distributions)
+$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
+$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
+if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
+ $actualDistributionDir = $distributionUrlNameMain
+}
+
+# If not found, search for any directory with the Maven executable (for snapshots)
+if (!$actualDistributionDir) {
+ Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
+ $testPath = Join-Path $_.FullName "bin/$MVN_CMD"
+ if (Test-Path -Path $testPath -PathType Leaf) {
+ $actualDistributionDir = $_.Name
+ }
+ }
+}
+
+if (!$actualDistributionDir) {
+ Write-Error "Could not find Maven distribution directory in extracted archive"
+}
+
+Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
+try {
+ Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
+} catch {
+ if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
+ Write-Error "fail to move MAVEN_HOME"
+ }
+} finally {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+}
+
+Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
diff --git a/jdk_21_maven/cs/rest/adoptme/pom.xml b/jdk_21_maven/cs/rest/adoptme/pom.xml
new file mode 100644
index 000000000..73243da6e
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/pom.xml
@@ -0,0 +1,87 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.5.6
+
+
+ com.programacion3
+ adoptme
+ 0.0.1-SNAPSHOT
+ AdoptM
+ Trabajo práctico Programación 3 - Sistema de adopciones
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 21
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-neo4j
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+
+
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/AdoptMApplication.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/AdoptMApplication.java
new file mode 100644
index 000000000..7e789e112
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/AdoptMApplication.java
@@ -0,0 +1,12 @@
+package com.programacion3.adoptme;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class AdoptMApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(AdoptMApplication.class, args);
+ }
+
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/config/CorsConfig.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/config/CorsConfig.java
new file mode 100644
index 000000000..ee4e15d22
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/config/CorsConfig.java
@@ -0,0 +1,64 @@
+package com.programacion3.adoptme.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Configuración de CORS para permitir requests desde el frontend
+ */
+@Configuration
+public class CorsConfig {
+
+ @Bean
+ public CorsFilter corsFilter() {
+ CorsConfiguration config = new CorsConfiguration();
+
+ // Permitir credenciales
+ config.setAllowCredentials(true);
+
+ // Permitir orígenes del frontend
+ config.setAllowedOriginPatterns(Arrays.asList(
+ "http://localhost:*",
+ "http://127.0.0.1:*",
+ "http://[::1]:*"
+ ));
+
+ // Permitir todos los headers
+ config.setAllowedHeaders(List.of("*"));
+
+ // Permitir todos los métodos HTTP
+ config.setAllowedMethods(Arrays.asList(
+ "GET",
+ "POST",
+ "PUT",
+ "DELETE",
+ "OPTIONS",
+ "PATCH"
+ ));
+
+ // Exponer headers en la respuesta
+ config.setExposedHeaders(Arrays.asList(
+ "Authorization",
+ "Content-Type",
+ "X-Requested-With",
+ "Accept",
+ "Origin",
+ "Access-Control-Request-Method",
+ "Access-Control-Request-Headers"
+ ));
+
+ // Cachear la respuesta de preflight por 1 hora
+ config.setMaxAge(3600L);
+
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/**", config);
+
+ return new CorsFilter(source);
+ }
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/config/DbSeed.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/config/DbSeed.java
new file mode 100644
index 000000000..8023b7ca9
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/config/DbSeed.java
@@ -0,0 +1,333 @@
+package com.programacion3.adoptme.config;
+
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.data.neo4j.core.Neo4jClient;
+
+@Configuration
+public class DbSeed {
+
+ @Bean
+ CommandLineRunner seed(Neo4jClient neo4j) {
+ return args -> seedIfEmpty(neo4j);
+ }
+
+ @Transactional
+ void seedIfEmpty(Neo4jClient neo4j) {
+ // Check if data is complete (15 shelters expected)
+ long shelterCount = neo4j.query("MATCH (s:Shelter) RETURN count(s) AS c")
+ .fetchAs(Long.class)
+ .mappedBy((t,r) -> r.get("c").asLong())
+ .one()
+ .orElse(0L);
+
+ long dogCount = neo4j.query("MATCH (d:Dog) RETURN count(d) AS c")
+ .fetchAs(Long.class)
+ .mappedBy((t,r) -> r.get("c").asLong())
+ .one()
+ .orElse(0L);
+
+ long adopterCount = neo4j.query("MATCH (a:Adopter) RETURN count(a) AS c")
+ .fetchAs(Long.class)
+ .mappedBy((t,r) -> r.get("c").asLong())
+ .one()
+ .orElse(0L);
+
+ // Check if database has complete test data
+ boolean hasCompleteData = shelterCount == 15 && dogCount == 40 && adopterCount == 15;
+
+ if (hasCompleteData) {
+ System.out.println("[SEED] Database already has complete test data:");
+ System.out.println("[SEED] - 15 Shelters ✓");
+ System.out.println("[SEED] - 40 Dogs ✓");
+ System.out.println("[SEED] - 15 Adopters ✓");
+ return;
+ }
+
+ // Database is incomplete or empty, re-seed everything
+ System.out.println("[SEED] Database incomplete or empty. Current state:");
+ System.out.println("[SEED] - Shelters: " + shelterCount + "/15");
+ System.out.println("[SEED] - Dogs: " + dogCount + "/40");
+ System.out.println("[SEED] - Adopters: " + adopterCount + "/15");
+ System.out.println("[SEED] Cleaning and re-seeding database...");
+
+ neo4j.query("MATCH (n) DETACH DELETE n").run();
+
+ // ========== SHELTERS (15 total) ==========
+ neo4j.query("""
+CREATE (a:Shelter {id:'A', name:'Refugio A', capacity:20}),
+ (b:Shelter {id:'B', name:'Refugio B', capacity:15}),
+ (c:Shelter {id:'C', name:'Refugio C', capacity:10}),
+ (h:Shelter {id:'H', name:'Central Hub', capacity:40}),
+ (d:Shelter {id:'D', name:'Refugio D', capacity:25}),
+ (e:Shelter {id:'E', name:'Refugio E', capacity:18}),
+ (f:Shelter {id:'F', name:'Refugio F', capacity:12}),
+ (g:Shelter {id:'G', name:'Refugio G', capacity:30}),
+ (i:Shelter {id:'I', name:'Refugio I', capacity:22}),
+ (j:Shelter {id:'J', name:'Refugio J', capacity:16}),
+ (k:Shelter {id:'K', name:'Refugio K', capacity:14}),
+ (l:Shelter {id:'L', name:'Refugio L', capacity:28}),
+ (m:Shelter {id:'M', name:'Refugio M', capacity:20}),
+ (n:Shelter {id:'N', name:'Refugio N', capacity:24}),
+ (o:Shelter {id:'O', name:'Refugio O', capacity:35})
+
+""").run();
+
+ // ========== NETWORK CONNECTIONS (35+ edges) ==========
+ neo4j.query("""
+MATCH (a:Shelter {id:'A'}),(b:Shelter {id:'B'}),(c:Shelter {id:'C'}),(h:Shelter {id:'H'}),
+ (d:Shelter {id:'D'}),(e:Shelter {id:'E'}),(f:Shelter {id:'F'}),(g:Shelter {id:'G'}),
+ (i:Shelter {id:'I'}),(j:Shelter {id:'J'}),(k:Shelter {id:'K'}),(l:Shelter {id:'L'}),
+ (m:Shelter {id:'M'}),(n:Shelter {id:'N'}),(o:Shelter {id:'O'})
+CREATE
+ // Original connections
+ (h)-[:NEAR {distKm:5, timeMin:10}]->(a),
+ (h)-[:NEAR {distKm:7, timeMin:15}]->(b),
+ (h)-[:NEAR {distKm:9, timeMin:18}]->(c),
+ (a)-[:NEAR {distKm:6, timeMin:12}]->(b),
+ (b)-[:NEAR {distKm:8, timeMin:16}]->(c),
+ (a)-[:NEAR {distKm:10, timeMin:20}]->(c),
+ (c)-[:NEAR {distKm:14, timeMin:25}]->(h),
+
+ // Hub connections to new shelters
+ (h)-[:NEAR {distKm:8, timeMin:16}]->(d),
+ (h)-[:NEAR {distKm:12, timeMin:24}]->(g),
+ (h)-[:NEAR {distKm:11, timeMin:22}]->(l),
+ (h)-[:NEAR {distKm:15, timeMin:30}]->(o),
+
+ // Regional cluster 1: A-D-E-I
+ (a)-[:NEAR {distKm:7, timeMin:14}]->(d),
+ (d)-[:NEAR {distKm:9, timeMin:18}]->(e),
+ (e)-[:NEAR {distKm:6, timeMin:12}]->(i),
+ (i)-[:NEAR {distKm:11, timeMin:22}]->(a),
+
+ // Regional cluster 2: B-F-J-K
+ (b)-[:NEAR {distKm:5, timeMin:10}]->(f),
+ (f)-[:NEAR {distKm:8, timeMin:16}]->(j),
+ (j)-[:NEAR {distKm:7, timeMin:14}]->(k),
+ (k)-[:NEAR {distKm:9, timeMin:18}]->(b),
+
+ // Regional cluster 3: C-G-M-N
+ (c)-[:NEAR {distKm:6, timeMin:12}]->(g),
+ (g)-[:NEAR {distKm:10, timeMin:20}]->(m),
+ (m)-[:NEAR {distKm:8, timeMin:16}]->(n),
+ (n)-[:NEAR {distKm:12, timeMin:24}]->(c),
+
+ // Cluster 4: L-O
+ (l)-[:NEAR {distKm:13, timeMin:26}]->(o),
+
+ // Inter-cluster connections
+ (d)-[:NEAR {distKm:15, timeMin:30}]->(g),
+ (e)-[:NEAR {distKm:10, timeMin:20}]->(f),
+ (f)-[:NEAR {distKm:14, timeMin:28}]->(m),
+ (i)-[:NEAR {distKm:16, timeMin:32}]->(j),
+ (j)-[:NEAR {distKm:11, timeMin:22}]->(n),
+ (k)-[:NEAR {distKm:13, timeMin:26}]->(l),
+ (l)-[:NEAR {distKm:9, timeMin:18}]->(m),
+ (n)-[:NEAR {distKm:17, timeMin:34}]->(o),
+ (e)-[:NEAR {distKm:18, timeMin:36}]->(o),
+
+ // Additional shortcuts
+ (a)-[:NEAR {distKm:20, timeMin:40}]->(g),
+ (d)-[:NEAR {distKm:22, timeMin:44}]->(l)
+""").run();
+ // ========== DOGS (40 total) - varied characteristics for testing ==========
+ neo4j.query("""
+CREATE
+ (d1:Dog {id:'D1', name:'Luna', size:'SMALL', weightKg:8, age:2, energy:'LOW', goodWithKids:true, specialNeeds:false, priority:4}),
+ (d2:Dog {id:'D2', name:'Toto', size:'MEDIUM', weightKg:18, age:3, energy:'HIGH', goodWithKids:true, specialNeeds:false, priority:6}),
+ (d3:Dog {id:'D3', name:'Rex', size:'LARGE', weightKg:25, age:5, energy:'MEDIUM', goodWithKids:false, specialNeeds:true, priority:8}),
+ (d4:Dog {id:'D4', name:'Miranda', size:'SMALL', weightKg:10, age:1, energy:'HIGH', goodWithKids:true, specialNeeds:false, priority:5}),
+ (d5:Dog {id:'D5', name:'Perchita', size:'MEDIUM', weightKg:15, age:4, energy:'MEDIUM', goodWithKids:true, specialNeeds:false, priority:3}),
+ (d6:Dog {id:'D6', name:'Lina', size:'LARGE', weightKg:30, age:6, energy:'LOW', goodWithKids:false, specialNeeds:true, priority:7}),
+
+ // New dogs for extensive testing
+ (d7:Dog {id:'D7', name:'Buddy', size:'MEDIUM', weightKg:20, age:4, energy:'HIGH', goodWithKids:true, specialNeeds:false, priority:6}),
+ (d8:Dog {id:'D8', name:'Max', size:'LARGE', weightKg:28, age:7, energy:'LOW', goodWithKids:false, specialNeeds:false, priority:5}),
+ (d9:Dog {id:'D9', name:'Bella', size:'SMALL', weightKg:7, age:1, energy:'MEDIUM', goodWithKids:true, specialNeeds:false, priority:9}),
+ (d10:Dog {id:'D10', name:'Charlie', size:'MEDIUM', weightKg:16, age:3, energy:'HIGH', goodWithKids:true, specialNeeds:false, priority:7}),
+ (d11:Dog {id:'D11', name:'Daisy', size:'SMALL', weightKg:9, age:2, energy:'LOW', goodWithKids:true, specialNeeds:true, priority:8}),
+ (d12:Dog {id:'D12', name:'Rocky', size:'LARGE', weightKg:32, age:6, energy:'MEDIUM', goodWithKids:false, specialNeeds:false, priority:4}),
+ (d13:Dog {id:'D13', name:'Lucy', size:'SMALL', weightKg:6, age:1, energy:'HIGH', goodWithKids:true, specialNeeds:false, priority:10}),
+ (d14:Dog {id:'D14', name:'Cooper', size:'MEDIUM', weightKg:19, age:5, energy:'MEDIUM', goodWithKids:true, specialNeeds:false, priority:5}),
+ (d15:Dog {id:'D15', name:'Sadie', size:'LARGE', weightKg:27, age:4, energy:'LOW', goodWithKids:false, specialNeeds:true, priority:6}),
+ (d16:Dog {id:'D16', name:'Duke', size:'LARGE', weightKg:35, age:8, energy:'LOW', goodWithKids:false, specialNeeds:true, priority:3}),
+ (d17:Dog {id:'D17', name:'Lola', size:'SMALL', weightKg:8, age:2, energy:'MEDIUM', goodWithKids:true, specialNeeds:false, priority:7}),
+ (d18:Dog {id:'D18', name:'Bailey', size:'MEDIUM', weightKg:17, age:3, energy:'HIGH', goodWithKids:true, specialNeeds:false, priority:8}),
+ (d19:Dog {id:'D19', name:'Maggie', size:'SMALL', weightKg:10, age:6, energy:'LOW', goodWithKids:true, specialNeeds:false, priority:4}),
+ (d20:Dog {id:'D20', name:'Bear', size:'LARGE', weightKg:33, age:7, energy:'MEDIUM', goodWithKids:false, specialNeeds:false, priority:5}),
+ (d21:Dog {id:'D21', name:'Molly', size:'MEDIUM', weightKg:16, age:2, energy:'HIGH', goodWithKids:true, specialNeeds:false, priority:9}),
+ (d22:Dog {id:'D22', name:'Jack', size:'SMALL', weightKg:7, age:1, energy:'HIGH', goodWithKids:true, specialNeeds:false, priority:6}),
+ (d23:Dog {id:'D23', name:'Sophie', size:'MEDIUM', weightKg:18, age:4, energy:'MEDIUM', goodWithKids:true, specialNeeds:true, priority:7}),
+ (d24:Dog {id:'D24', name:'Zeus', size:'LARGE', weightKg:34, age:5, energy:'HIGH', goodWithKids:false, specialNeeds:false, priority:8}),
+ (d25:Dog {id:'D25', name:'Chloe', size:'SMALL', weightKg:9, age:3, energy:'LOW', goodWithKids:true, specialNeeds:false, priority:5}),
+ (d26:Dog {id:'D26', name:'Bentley', size:'MEDIUM', weightKg:21, age:4, energy:'MEDIUM', goodWithKids:false, specialNeeds:false, priority:6}),
+ (d27:Dog {id:'D27', name:'Zoe', size:'SMALL', weightKg:6, age:1, energy:'HIGH', goodWithKids:true, specialNeeds:false, priority:10}),
+ (d28:Dog {id:'D28', name:'Gus', size:'LARGE', weightKg:29, age:6, energy:'LOW', goodWithKids:false, specialNeeds:true, priority:4}),
+ (d29:Dog {id:'D29', name:'Penny', size:'MEDIUM', weightKg:17, age:2, energy:'HIGH', goodWithKids:true, specialNeeds:false, priority:8}),
+ (d30:Dog {id:'D30', name:'Milo', size:'SMALL', weightKg:8, age:3, energy:'MEDIUM', goodWithKids:true, specialNeeds:false, priority:7}),
+ (d31:Dog {id:'D31', name:'Ruby', size:'LARGE', weightKg:26, age:5, energy:'LOW', goodWithKids:false, specialNeeds:false, priority:5}),
+ (d32:Dog {id:'D32', name:'Oscar', size:'MEDIUM', weightKg:19, age:4, energy:'HIGH', goodWithKids:true, specialNeeds:true, priority:9}),
+ (d33:Dog {id:'D33', name:'Stella', size:'SMALL', weightKg:7, age:2, energy:'LOW', goodWithKids:true, specialNeeds:false, priority:6}),
+ (d34:Dog {id:'D34', name:'Tucker', size:'LARGE', weightKg:31, age:7, energy:'MEDIUM', goodWithKids:false, specialNeeds:false, priority:4}),
+ (d35:Dog {id:'D35', name:'Rosie', size:'SMALL', weightKg:9, age:1, energy:'HIGH', goodWithKids:true, specialNeeds:false, priority:8}),
+ (d36:Dog {id:'D36', name:'Harley', size:'MEDIUM', weightKg:20, age:5, energy:'MEDIUM', goodWithKids:false, specialNeeds:true, priority:5}),
+ (d37:Dog {id:'D37', name:'Ellie', size:'SMALL', weightKg:6, age:2, energy:'LOW', goodWithKids:true, specialNeeds:false, priority:7}),
+ (d38:Dog {id:'D38', name:'Leo', size:'LARGE', weightKg:30, age:6, energy:'HIGH', goodWithKids:false, specialNeeds:false, priority:6}),
+ (d39:Dog {id:'D39', name:'Lily', size:'MEDIUM', weightKg:18, age:3, energy:'MEDIUM', goodWithKids:true, specialNeeds:false, priority:8}),
+ (d40:Dog {id:'D40', name:'Ace', size:'LARGE', weightKg:28, age:4, energy:'LOW', goodWithKids:false, specialNeeds:true, priority:7})
+""").run();
+
+ // ========== SHELTER-DOG ASSIGNMENTS (40 dogs distributed across 15 shelters) ==========
+ neo4j.query("""
+MATCH
+ (a:Shelter {id:'A'}), (b:Shelter {id:'B'}), (c:Shelter {id:'C'}), (h:Shelter {id:'H'}),
+ (d:Shelter {id:'D'}), (e:Shelter {id:'E'}), (f:Shelter {id:'F'}), (g:Shelter {id:'G'}),
+ (i:Shelter {id:'I'}), (j:Shelter {id:'J'}), (k:Shelter {id:'K'}), (l:Shelter {id:'L'}),
+ (m:Shelter {id:'M'}), (n:Shelter {id:'N'}), (o:Shelter {id:'O'}),
+ (d1:Dog {id:'D1'}), (d2:Dog {id:'D2'}), (d3:Dog {id:'D3'}), (d4:Dog {id:'D4'}), (d5:Dog {id:'D5'}),
+ (d6:Dog {id:'D6'}), (d7:Dog {id:'D7'}), (d8:Dog {id:'D8'}), (d9:Dog {id:'D9'}), (d10:Dog {id:'D10'}),
+ (d11:Dog {id:'D11'}), (d12:Dog {id:'D12'}), (d13:Dog {id:'D13'}), (d14:Dog {id:'D14'}), (d15:Dog {id:'D15'}),
+ (d16:Dog {id:'D16'}), (d17:Dog {id:'D17'}), (d18:Dog {id:'D18'}), (d19:Dog {id:'D19'}), (d20:Dog {id:'D20'}),
+ (d21:Dog {id:'D21'}), (d22:Dog {id:'D22'}), (d23:Dog {id:'D23'}), (d24:Dog {id:'D24'}), (d25:Dog {id:'D25'}),
+ (d26:Dog {id:'D26'}), (d27:Dog {id:'D27'}), (d28:Dog {id:'D28'}), (d29:Dog {id:'D29'}), (d30:Dog {id:'D30'}),
+ (d31:Dog {id:'D31'}), (d32:Dog {id:'D32'}), (d33:Dog {id:'D33'}), (d34:Dog {id:'D34'}), (d35:Dog {id:'D35'}),
+ (d36:Dog {id:'D36'}), (d37:Dog {id:'D37'}), (d38:Dog {id:'D38'}), (d39:Dog {id:'D39'}), (d40:Dog {id:'D40'})
+CREATE
+ // Shelter A (3 dogs)
+ (a)-[:HAS_DOG]->(d1),
+ (a)-[:HAS_DOG]->(d2),
+ (a)-[:HAS_DOG]->(d9),
+
+ // Shelter B (3 dogs)
+ (b)-[:HAS_DOG]->(d3),
+ (b)-[:HAS_DOG]->(d10),
+ (b)-[:HAS_DOG]->(d17),
+
+ // Shelter C (3 dogs)
+ (c)-[:HAS_DOG]->(d4),
+ (c)-[:HAS_DOG]->(d11),
+ (c)-[:HAS_DOG]->(d18),
+
+ // Shelter H - Central Hub (4 dogs)
+ (h)-[:HAS_DOG]->(d5),
+ (h)-[:HAS_DOG]->(d6),
+ (h)-[:HAS_DOG]->(d7),
+ (h)-[:HAS_DOG]->(d8),
+
+ // Shelter D (3 dogs)
+ (d)-[:HAS_DOG]->(d12),
+ (d)-[:HAS_DOG]->(d13),
+ (d)-[:HAS_DOG]->(d19),
+
+ // Shelter E (3 dogs)
+ (e)-[:HAS_DOG]->(d14),
+ (e)-[:HAS_DOG]->(d15),
+ (e)-[:HAS_DOG]->(d20),
+
+ // Shelter F (2 dogs)
+ (f)-[:HAS_DOG]->(d16),
+ (f)-[:HAS_DOG]->(d21),
+
+ // Shelter G (3 dogs)
+ (g)-[:HAS_DOG]->(d22),
+ (g)-[:HAS_DOG]->(d23),
+ (g)-[:HAS_DOG]->(d24),
+
+ // Shelter I (3 dogs)
+ (i)-[:HAS_DOG]->(d25),
+ (i)-[:HAS_DOG]->(d26),
+ (i)-[:HAS_DOG]->(d27),
+
+ // Shelter J (2 dogs)
+ (j)-[:HAS_DOG]->(d28),
+ (j)-[:HAS_DOG]->(d29),
+
+ // Shelter K (2 dogs)
+ (k)-[:HAS_DOG]->(d30),
+ (k)-[:HAS_DOG]->(d31),
+
+ // Shelter L (3 dogs)
+ (l)-[:HAS_DOG]->(d32),
+ (l)-[:HAS_DOG]->(d33),
+ (l)-[:HAS_DOG]->(d34),
+
+ // Shelter M (3 dogs)
+ (m)-[:HAS_DOG]->(d35),
+ (m)-[:HAS_DOG]->(d36),
+ (m)-[:HAS_DOG]->(d37),
+
+ // Shelter N (2 dogs)
+ (n)-[:HAS_DOG]->(d38),
+ (n)-[:HAS_DOG]->(d39),
+
+ // Shelter O (2 dogs)
+ (o)-[:HAS_DOG]->(d40),
+ (o)-[:HAS_DOG]->(d1)
+""").run();
+
+ // ========== ADOPTERS (15 total) - varied profiles for matching algorithms ==========
+ neo4j.query("""
+CREATE
+ (p1:Adopter {id:'P1', name:'Camila', budget:25000, hasYard:true, hasKids:true, maxDogs:2}),
+ (p2:Adopter {id:'P2', name:'Lucas', budget:18000, hasYard:false, hasKids:false, maxDogs:1}),
+ (p3:Adopter {id:'P3', name:'Daniela', budget:30000, hasYard:true, hasKids:false, maxDogs:3}),
+
+ // New adopters with varied profiles
+ (p4:Adopter {id:'P4', name:'Martin', budget:22000, hasYard:true, hasKids:true, maxDogs:2}),
+ (p5:Adopter {id:'P5', name:'Sofia', budget:15000, hasYard:false, hasKids:false, maxDogs:1}),
+ (p6:Adopter {id:'P6', name:'Roberto', budget:35000, hasYard:true, hasKids:false, maxDogs:4}),
+ (p7:Adopter {id:'P7', name:'Ana', budget:20000, hasYard:true, hasKids:true, maxDogs:1}),
+ (p8:Adopter {id:'P8', name:'Diego', budget:28000, hasYard:false, hasKids:false, maxDogs:2}),
+ (p9:Adopter {id:'P9', name:'Julia', budget:40000, hasYard:true, hasKids:true, maxDogs:3}),
+ (p10:Adopter {id:'P10', name:'Carlos', budget:12000, hasYard:false, hasKids:false, maxDogs:1}),
+ (p11:Adopter {id:'P11', name:'Valeria', budget:32000, hasYard:true, hasKids:false, maxDogs:5}),
+ (p12:Adopter {id:'P12', name:'Pedro', budget:24000, hasYard:true, hasKids:true, maxDogs:2}),
+ (p13:Adopter {id:'P13', name:'Laura', budget:16000, hasYard:false, hasKids:true, maxDogs:1}),
+ (p14:Adopter {id:'P14', name:'Andres', budget:45000, hasYard:true, hasKids:false, maxDogs:4}),
+ (p15:Adopter {id:'P15', name:'Monica', budget:27000, hasYard:true, hasKids:true, maxDogs:3})
+""").run();
+
+ // ========== INITIAL ADOPTIONS (10 adoptions, 30 dogs still available) ==========
+ neo4j.query("""
+MATCH
+ (p1:Adopter {id:'P1'}), (p2:Adopter {id:'P2'}), (p3:Adopter {id:'P3'}),
+ (p4:Adopter {id:'P4'}), (p5:Adopter {id:'P5'}), (p6:Adopter {id:'P6'}),
+ (d1:Dog {id:'D1'}), (d2:Dog {id:'D2'}), (d3:Dog {id:'D3'}), (d4:Dog {id:'D4'}), (d5:Dog {id:'D5'}),
+ (d13:Dog {id:'D13'}), (d22:Dog {id:'D22'}), (d27:Dog {id:'D27'}), (d35:Dog {id:'D35'}), (d9:Dog {id:'D9'})
+CREATE
+ // P1: Camila adopts 2 dogs (has kids, needs goodWithKids dogs)
+ (p1)-[:ADOPTS]->(d1),
+ (p1)-[:ADOPTS]->(d2),
+
+ // P2: Lucas adopts 1 dog (no kids, can handle any)
+ (p2)-[:ADOPTS]->(d3),
+
+ // P3: Daniela adopts 2 dogs (no kids, prefers low maintenance)
+ (p3)-[:ADOPTS]->(d4),
+ (p3)-[:ADOPTS]->(d5),
+
+ // P4: Martin adopts 1 dog (has kids)
+ (p4)-[:ADOPTS]->(d9),
+
+ // P5: Sofia adopts 1 dog (single, small space)
+ (p5)-[:ADOPTS]->(d13),
+
+ // P6: Roberto adopts 3 dogs (large budget, no kids)
+ (p6)-[:ADOPTS]->(d22),
+ (p6)-[:ADOPTS]->(d27),
+ (p6)-[:ADOPTS]->(d35)
+""").run();
+
+ System.out.println("[SEED] ===== DATABASE SEEDED SUCCESSFULLY =====");
+ System.out.println("[SEED] - 15 Shelters (A-O) with varied capacities");
+ System.out.println("[SEED] - 40 Dogs with diverse characteristics for algorithm testing");
+ System.out.println("[SEED] - 15 Adopters with varied profiles (budget, yard, kids, maxDogs)");
+ System.out.println("[SEED] - 39 NEAR relationships creating a dense shelter network");
+ System.out.println("[SEED] - 40 HAS_DOG relationships distributing dogs across shelters");
+ System.out.println("[SEED] - 10 initial ADOPTS relationships (30 dogs available for matching)");
+ System.out.println("[SEED] =========================================");
+ }
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/AdopterController.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/AdopterController.java
new file mode 100644
index 000000000..3ccfb7d55
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/AdopterController.java
@@ -0,0 +1,27 @@
+package com.programacion3.adoptme.controller;
+
+import com.programacion3.adoptme.domain.Adopter;
+import com.programacion3.adoptme.repo.AdopterRepository; // usa 'repo' si ese es tu package
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/adopters")
+@RequiredArgsConstructor
+public class AdopterController {
+
+ private final AdopterRepository adopterRepository;
+
+ @GetMapping
+ public List getAll() {
+ return adopterRepository.findAll();
+ }
+
+
+ @PostMapping
+ public Adopter create(@RequestBody Adopter adopter) {
+ return adopterRepository.save(adopter);
+ }
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/AdoptionsController.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/AdoptionsController.java
new file mode 100644
index 000000000..b70bd2859
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/AdoptionsController.java
@@ -0,0 +1,249 @@
+package com.programacion3.adoptme.controller;
+
+import com.programacion3.adoptme.domain.Dog;
+import com.programacion3.adoptme.exception.ResourceNotFoundException;
+import com.programacion3.adoptme.repo.AdopterRepository;
+import com.programacion3.adoptme.repo.DogRepository;
+import com.programacion3.adoptme.service.ScorerService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@RestController
+@RequestMapping("/adoptions")
+@RequiredArgsConstructor
+public class AdoptionsController {
+
+ private final AdopterRepository adopterRepository;
+ private final DogRepository dogRepository;
+ private final ScorerService scorerService;
+ private final com.programacion3.adoptme.service.BacktrackingService backtrackingService;
+
+ /**
+ * Algoritmo Greedy: Asigna perros a un adoptante maximizando el score
+ * Considera: presupuesto, jardín, niños, energía del perro
+ * GET /adoptions/greedy?adopterId=P1
+ */
+ @GetMapping("/greedy")
+ public ResponseEntity greedyAdoption(
+ @RequestParam String adopterId
+ ) {
+ // Buscar adoptante
+ var adopter = adopterRepository.findById(adopterId)
+ .orElseThrow(() -> new ResourceNotFoundException("Adopter not found: " + adopterId));
+
+ // Obtener todos los perros disponibles
+ List allDogs = dogRepository.findAll();
+
+ if (allDogs.isEmpty()) {
+ return ResponseEntity.ok(new GreedyResponse(
+ "No dogs available for adoption",
+ adopterId,
+ adopter.getName(),
+ List.of(),
+ 0.0,
+ 0.0
+ ));
+ }
+
+ // Convertir a formato del ScorerService
+ List candidates = allDogs.stream()
+ .map(d -> new ScorerService.Dog(
+ d.getId(),
+ d.getGoodWithKids() != null && d.getGoodWithKids(),
+ "LARGE".equalsIgnoreCase(d.getSize()), // perros grandes necesitan jardín
+ mapEnergy(d.getEnergy()),
+ mapSize(d.getSize()),
+ estimateCost(d) // costo estimado por adopción
+ ))
+ .collect(Collectors.toList());
+
+ // Ejecutar algoritmo Greedy
+ var result = scorerService.scoreAndAssign(
+ candidates,
+ adopter.getHasKids() != null && adopter.getHasKids(),
+ adopter.getHasYard() != null && adopter.getHasYard(),
+ adopter.getMaxDogs() != null ? adopter.getMaxDogs() : 1,
+ adopter.getBudget() != null ? adopter.getBudget() : 20000.0
+ );
+
+ // Formatear respuesta
+ List assigned = result.assigned.stream()
+ .map(d -> new AssignedDog(d.id, findDogName(allDogs, d.id), d.cost))
+ .collect(Collectors.toList());
+
+ return ResponseEntity.ok(new GreedyResponse(
+ "Greedy algorithm executed successfully",
+ adopterId,
+ adopter.getName(),
+ assigned,
+ result.totalScore,
+ result.totalCost
+ ));
+ }
+
+ /**
+ * Algoritmo Backtracking: Asigna múltiples perros a múltiples adoptantes
+ * respetando TODAS las restricciones y maximizando satisfacción total.
+ *
+ * Considera restricciones de:
+ * - Presupuesto
+ * - Capacidad máxima de perros
+ * - Compatibilidad con niños
+ * - Necesidad de jardín
+ * - Preferencia de energía
+ *
+ * GET /adoptions/constraints/backtracking
+ */
+ @GetMapping("/constraints/backtracking")
+ public ResponseEntity backtrackingAdoption() {
+ // Obtener todos los perros y adoptantes
+ List allDogs = dogRepository.findAll();
+ var allAdopters = adopterRepository.findAll();
+
+ if (allDogs.isEmpty()) {
+ return ResponseEntity.ok(new BacktrackingResponse(
+ "No dogs available for adoption",
+ Map.of(),
+ 0.0
+ ));
+ }
+
+ if (allAdopters.isEmpty()) {
+ return ResponseEntity.ok(new BacktrackingResponse(
+ "No adopters available",
+ Map.of(),
+ 0.0
+ ));
+ }
+
+ // Convertir perros al formato del servicio
+ List dogs = allDogs.stream()
+ .map(d -> new com.programacion3.adoptme.service.BacktrackingService.Dog(
+ d.getId(),
+ d.getGoodWithKids() != null && d.getGoodWithKids(),
+ "LARGE".equalsIgnoreCase(d.getSize()),
+ mapEnergy(d.getEnergy()),
+ estimateCost(d)
+ ))
+ .toList();
+
+ // Convertir adoptantes al formato del servicio
+ List adopters = allAdopters.stream()
+ .map(a -> new com.programacion3.adoptme.service.BacktrackingService.Adopter(
+ a.getId(),
+ a.getName(),
+ a.getHasKids() != null && a.getHasKids(),
+ a.getHasYard() != null && a.getHasYard(),
+ a.getMaxDogs() != null ? a.getMaxDogs() : 1,
+ a.getBudget() != null ? a.getBudget() : 20000.0,
+ 5 // energía preferida default (media)
+ ))
+ .toList();
+
+ // Ejecutar algoritmo de backtracking
+ var result = backtrackingService.findBestAssignment(dogs, adopters);
+
+ // Formatear respuesta
+ Map assignments = new HashMap<>();
+
+ for (var adopter : allAdopters) {
+ List dogIds = result.assignments.getOrDefault(adopter.getId(), List.of());
+
+ if (!dogIds.isEmpty()) {
+ List assignedDogs = dogIds.stream()
+ .map(dogId -> {
+ String dogName = findDogName(allDogs, dogId);
+ double cost = estimateCost(allDogs.stream()
+ .filter(d -> d.getId().equals(dogId))
+ .findFirst()
+ .orElse(null));
+ return new AssignedDog(dogId, dogName, cost);
+ })
+ .toList();
+
+ assignments.put(adopter.getId(), new AdopterAssignment(
+ adopter.getId(),
+ adopter.getName(),
+ assignedDogs
+ ));
+ }
+ }
+
+ return ResponseEntity.ok(new BacktrackingResponse(
+ "Backtracking algorithm completed successfully",
+ assignments,
+ result.totalScore
+ ));
+ }
+
+ // Métodos auxiliares
+ private int mapEnergy(String energy) {
+ if (energy == null) return 5;
+ return switch (energy.toUpperCase()) {
+ case "LOW" -> 2;
+ case "MEDIUM" -> 5;
+ case "HIGH" -> 8;
+ default -> 5;
+ };
+ }
+
+ private int mapSize(String size) {
+ if (size == null) return 2;
+ return switch (size.toUpperCase()) {
+ case "SMALL" -> 1;
+ case "MEDIUM" -> 2;
+ case "LARGE" -> 3;
+ default -> 2;
+ };
+ }
+
+ private double estimateCost(Dog dog) {
+ // Costo base + extra por tamaño y necesidades especiales
+ double baseCost = 5000.0;
+ double sizeCost = mapSize(dog.getSize()) * 2000.0;
+ double specialNeedsCost = (dog.getSpecialNeeds() != null && dog.getSpecialNeeds()) ? 5000.0 : 0.0;
+ return baseCost + sizeCost + specialNeedsCost;
+ }
+
+ private String findDogName(List dogs, String id) {
+ return dogs.stream()
+ .filter(d -> d.getId().equals(id))
+ .map(Dog::getName)
+ .findFirst()
+ .orElse("Unknown");
+ }
+
+ // DTOs
+ record GreedyResponse(
+ String message,
+ String adopterId,
+ String adopterName,
+ List assignedDogs,
+ double totalScore,
+ double totalCost
+ ) {}
+
+ record BacktrackingResponse(
+ String message,
+ Map assignments,
+ double totalScore
+ ) {}
+
+ record AdopterAssignment(
+ String adopterId,
+ String adopterName,
+ List assignedDogs
+ ) {}
+
+ record AssignedDog(
+ String dogId,
+ String dogName,
+ double cost
+ ) {}
+}
\ No newline at end of file
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/DogController.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/DogController.java
new file mode 100644
index 000000000..e77f905ed
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/DogController.java
@@ -0,0 +1,83 @@
+package com.programacion3.adoptme.controller;
+
+import com.programacion3.adoptme.domain.Dog;
+import com.programacion3.adoptme.repo.DogRepository;
+import com.programacion3.adoptme.service.SortService;
+import org.springframework.web.bind.annotation.*;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/dogs")
+@RequiredArgsConstructor
+public class DogController {
+ private final DogRepository dogRepository;
+ private final SortService sortService;
+
+ /**
+ * Lista todos los perros
+ * GET /dogs
+ */
+ @GetMapping
+ public List all() {
+ return dogRepository.findAll();
+ }
+
+ /**
+ * Ordena perros usando MergeSort o QuickSort (Divide y Vencerás)
+ * GET /dogs/sort?criteria=priority&algorithm=quicksort
+ * GET /dogs/sort?criteria=age&algorithm=mergesort
+ * GET /dogs/sort?criteria=weight
+ */
+ @GetMapping("/sort")
+ public ResponseEntity> sortDogs(
+ @RequestParam(defaultValue = "priority") String criteria,
+ @RequestParam(defaultValue = "mergesort") String algorithm
+ ) {
+ try {
+ // Obtener todos los perros
+ List dogs = dogRepository.findAll();
+
+ if (dogs.isEmpty()) {
+ return ResponseEntity.ok(new SortResponse(
+ "No dogs found",
+ criteria,
+ algorithm,
+ List.of()
+ ));
+ }
+
+ // Ordenar usando el servicio y algoritmo seleccionado
+ sortService.sortDogs(dogs, criteria, algorithm);
+
+ String algorithmName = "quicksort".equalsIgnoreCase(algorithm) ? "QuickSort" : "MergeSort";
+
+ return ResponseEntity.ok(new SortResponse(
+ "Dogs sorted by " + criteria + " using " + algorithmName,
+ criteria,
+ algorithm,
+ dogs
+ ));
+
+ } catch (IllegalArgumentException e) {
+ return ResponseEntity.badRequest()
+ .body(Map.of(
+ "error", e.getMessage(),
+ "validCriteria", List.of("priority", "age", "weight"),
+ "validAlgorithms", List.of("mergesort", "quicksort")
+ ));
+ }
+ }
+
+ // DTO para respuesta
+ record SortResponse(
+ String message,
+ String criteria,
+ String algorithm,
+ List dogs
+ ) {}
+}
\ No newline at end of file
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/GraphController.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/GraphController.java
new file mode 100644
index 000000000..9dc90d679
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/GraphController.java
@@ -0,0 +1,41 @@
+package com.programacion3.adoptme.controller;
+
+import com.programacion3.adoptme.dto.PathResponse;
+import com.programacion3.adoptme.service.GraphService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/graph")
+@RequiredArgsConstructor
+public class GraphController {
+
+ private final GraphService graphService;
+
+ /**
+ * Ejemplo:
+ * GET /graph/reachable?from=A&to=D
+ * GET /graph/reachable?from=A&to=D&method=dfs
+ */
+ @GetMapping("/reachable")
+ public ResponseEntity reachable(
+ @RequestParam String from,
+ @RequestParam String to,
+ @RequestParam(defaultValue = "bfs") String method
+ ) {
+ var m = method.trim().toLowerCase();
+ switch (m) {
+ case "dfs" -> {
+ var path = graphService.dfsPath(from, to);
+ var steps = path.isEmpty() ? 0 : path.size() - 1;
+ return ResponseEntity.ok(new PathResponse(!path.isEmpty(), "DFS", path, steps, 0));
+ }
+ default -> {
+ var path = graphService.bfsPath(from, to);
+ var steps = path.isEmpty() ? 0 : path.size() - 1;
+ return ResponseEntity.ok(new PathResponse(!path.isEmpty(), "BFS", path, steps, 0));
+ }
+ }
+ }
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/HealthController.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/HealthController.java
new file mode 100644
index 000000000..77a4219d6
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/HealthController.java
@@ -0,0 +1,12 @@
+package com.programacion3.adoptme.controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class HealthController {
+
+ @GetMapping("/ping")
+ public String ping() {
+ return "OK 🐶 AdoptMe funcionando!";
+ }
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/NetworkController.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/NetworkController.java
new file mode 100644
index 000000000..c14185b50
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/NetworkController.java
@@ -0,0 +1,125 @@
+package com.programacion3.adoptme.controller;
+
+import com.programacion3.adoptme.service.GraphLoader;
+import com.programacion3.adoptme.service.MSTService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@RestController
+@RequestMapping("/network")
+@RequiredArgsConstructor
+public class NetworkController {
+
+ private final MSTService mstService;
+ private final GraphLoader graphLoader;
+
+ /**
+ * Obtiene todas las conexiones entre shelters para visualización del grafo
+ * GET /network/graph
+ */
+ @GetMapping("/graph")
+ public ResponseEntity getGraph() {
+ // Cargar todos los shelters
+ var shelterIds = graphLoader.loadAllShelterIds();
+
+ if (shelterIds.isEmpty()) {
+ return ResponseEntity.ok(new GraphResponse(
+ "No shelters found",
+ List.of(),
+ List.of()
+ ));
+ }
+
+ // Cargar todas las aristas
+ var edges = graphLoader.loadMSTEdges();
+
+ // Formatear aristas (evitar duplicados para grafo no dirigido)
+ List formattedEdges = edges.stream()
+ .map(e -> new EdgeDTO(e.a, e.b, e.weight))
+ .distinct()
+ .collect(Collectors.toList());
+
+ // Convertir shelterIds a lista
+ List shelterList = List.copyOf(shelterIds);
+
+ return ResponseEntity.ok(new GraphResponse(
+ "Graph data loaded successfully",
+ shelterList,
+ formattedEdges
+ ));
+ }
+
+ /**
+ * Minimum Spanning Tree usando Kruskal o Prim
+ * Conecta todos los refugios con la menor distancia total
+ * GET /network/mst?algorithm=kruskal|prim (default: kruskal)
+ */
+ @GetMapping("/mst")
+ public ResponseEntity mst(
+ @RequestParam(defaultValue = "kruskal") String algorithm
+ ) {
+ // Cargar todos los shelters
+ var shelterIds = graphLoader.loadAllShelterIds();
+
+ if (shelterIds.isEmpty()) {
+ return ResponseEntity.ok(new MSTResponse(
+ "No shelters found",
+ List.of(),
+ 0.0,
+ algorithm
+ ));
+ }
+
+ // Cargar aristas
+ var edges = graphLoader.loadMSTEdges();
+
+ // Ejecutar algoritmo seleccionado
+ MSTService.MSTResult result;
+ String algorithmUsed;
+
+ if ("prim".equalsIgnoreCase(algorithm)) {
+ result = mstService.computeWithPrim(shelterIds, edges);
+ algorithmUsed = "Prim";
+ } else {
+ result = mstService.compute(shelterIds, edges);
+ algorithmUsed = "Kruskal";
+ }
+
+ // Formatear respuesta
+ List formattedEdges = result.edges.stream()
+ .map(e -> new EdgeDTO(e.a, e.b, e.weight))
+ .collect(Collectors.toList());
+
+ return ResponseEntity.ok(new MSTResponse(
+ "MST computed using " + algorithmUsed + "'s algorithm",
+ formattedEdges,
+ result.totalWeight,
+ algorithmUsed
+ ));
+ }
+
+ // DTOs para respuesta
+ record GraphResponse(
+ String message,
+ List nodes,
+ List edges
+ ) {}
+
+ record MSTResponse(
+ String message,
+ List edges,
+ double totalWeight,
+ String algorithm
+ ) {}
+
+ record EdgeDTO(
+ String from,
+ String to,
+ double weight
+ ) {}
+}
\ No newline at end of file
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/RoutesController.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/RoutesController.java
new file mode 100644
index 000000000..19558e938
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/RoutesController.java
@@ -0,0 +1,144 @@
+package com.programacion3.adoptme.controller;
+
+import com.programacion3.adoptme.dto.PathResponse;
+import com.programacion3.adoptme.dto.TspResponse;
+import com.programacion3.adoptme.service.GraphLoader;
+import com.programacion3.adoptme.service.ShortestPathService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@RestController
+@RequestMapping("/routes")
+@RequiredArgsConstructor
+public class RoutesController {
+
+ private final ShortestPathService shortestPathService;
+ private final GraphLoader graphLoader;
+ private final com.programacion3.adoptme.service.TSPService tspService;
+
+ /**
+ * Dijkstra - Encuentra el camino más corto considerando distancias
+ * GET /routes/shortest?from=A&to=C
+ */
+ @GetMapping("/shortest")
+ public ResponseEntity shortestRoute(
+ @RequestParam String from,
+ @RequestParam String to
+ ) {
+ // Validaciones
+ if (from == null || to == null || from.isBlank() || to.isBlank()) {
+ return ResponseEntity.badRequest()
+ .body(new PathResponse(false, "Dijkstra", null, 0, 0.0));
+ }
+
+ // Cargar aristas con pesos desde Neo4j
+ var edges = graphLoader.loadWeightedEdges();
+
+ // Ejecutar Dijkstra
+ var result = shortestPathService.shortestPath(from, to, edges);
+
+ // Si no hay camino
+ if (result.path.isEmpty() || Double.isInfinite(result.cost)) {
+ return ResponseEntity.ok(new PathResponse(
+ false,
+ "Dijkstra",
+ null,
+ 0,
+ 0.0
+ ));
+ }
+
+ // Respuesta exitosa
+ return ResponseEntity.ok(new PathResponse(
+ true,
+ "Dijkstra",
+ result.path,
+ result.path.size() - 1,
+ result.cost
+ ));
+ }
+
+ /**
+ * TSP (Travelling Salesman Problem) usando Branch & Bound
+ *
+ * Encuentra la ruta más corta para visitar todos los refugios especificados
+ * exactamente una vez y regresar al punto de inicio.
+ *
+ * GET /routes/tsp/bnb?nodes=A,B,C,H
+ * Si no se especifican nodos, usa todos los refugios disponibles.
+ *
+ * @param nodes lista de IDs de nodos separados por comas (opcional)
+ * @return tour óptimo y distancia total
+ */
+ @GetMapping("/tsp/bnb")
+ public ResponseEntity tspBranchBound(
+ @RequestParam(required = false) String nodes
+ ) {
+ // Determinar qué nodos visitar
+ List nodeList;
+
+ if (nodes != null && !nodes.isBlank()) {
+ // Usar nodos especificados
+ nodeList = Arrays.asList(nodes.split(","));
+ nodeList = nodeList.stream().map(String::trim).toList();
+ } else {
+ // Usar todos los shelters
+ nodeList = new ArrayList<>(graphLoader.loadAllShelterIds());
+ }
+
+ if (nodeList.isEmpty()) {
+ return ResponseEntity.ok(
+ TspResponse.builder()
+ .route(List.of())
+ .totalDistanceKm(0)
+ .build()
+ );
+ }
+
+ if (nodeList.size() == 1) {
+ return ResponseEntity.ok(
+ TspResponse.builder()
+ .route(nodeList)
+ .totalDistanceKm(0)
+ .build()
+ );
+ }
+
+ // Cargar aristas con distancias
+ var rawEdges = graphLoader.loadWeightedEdges();
+
+ // Convertir al formato del TSPService
+ List edges = rawEdges.stream()
+ .map(e -> new com.programacion3.adoptme.service.TSPService.Edge(
+ e.from,
+ e.to,
+ e.weight
+ ))
+ .toList();
+
+ // Ejecutar algoritmo de Branch & Bound
+ var result = tspService.solveTSP(nodeList, edges);
+
+ // Verificar si se encontró una solución
+ if (result.route.isEmpty() || Double.isInfinite(result.totalDistance)) {
+ return ResponseEntity.ok(
+ TspResponse.builder()
+ .route(null)
+ .totalDistanceKm(null)
+ .build()
+ );
+ }
+
+ return ResponseEntity.ok(
+ TspResponse.builder()
+ .route(result.route)
+ .totalDistanceKm((int) Math.round(result.totalDistance))
+ .build()
+ );
+ }
+}
\ No newline at end of file
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/ShelterController.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/ShelterController.java
new file mode 100644
index 000000000..9ddd57560
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/ShelterController.java
@@ -0,0 +1,16 @@
+package com.programacion3.adoptme.controller;
+
+import com.programacion3.adoptme.domain.Shelter;
+import com.programacion3.adoptme.repo.ShelterRepository;
+import org.springframework.web.bind.annotation.*;
+import java.util.List;
+
+@RestController
+@RequestMapping("/shelters")
+public class ShelterController {
+ private final ShelterRepository repo;
+ public ShelterController(ShelterRepository repo) { this.repo = repo; }
+
+ @GetMapping
+ public List all() { return repo.findAll(); }
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/TransportController.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/TransportController.java
new file mode 100644
index 000000000..8f7bf52e5
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/controller/TransportController.java
@@ -0,0 +1,101 @@
+package com.programacion3.adoptme.controller;
+
+import com.programacion3.adoptme.domain.Dog;
+import com.programacion3.adoptme.repo.DogRepository;
+import com.programacion3.adoptme.service.TransportService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * Controlador para optimización de transporte de perros.
+ * Usa Programación Dinámica (problema de la mochila 0/1) para seleccionar
+ * el mejor conjunto de perros dado una capacidad de vehículo limitada.
+ */
+@RestController
+@RequestMapping("/transport")
+@RequiredArgsConstructor
+public class TransportController {
+
+ private final TransportService transportService;
+ private final DogRepository dogRepository;
+
+ /**
+ * Optimiza el transporte de perros usando Programación Dinámica (Knapsack).
+ *
+ * Dado un vehículo con capacidad limitada, selecciona el conjunto óptimo
+ * de perros que maximiza la prioridad total sin exceder la capacidad.
+ *
+ * GET /transport/optimal-dp?capacityKg=50
+ *
+ * @param capacityKg capacidad del vehículo en kilogramos
+ * @return conjunto óptimo de perros para transportar
+ */
+ @GetMapping("/optimal-dp")
+ public ResponseEntity> optimalTransport(
+ @RequestParam(defaultValue = "50") int capacityKg
+ ) {
+ if (capacityKg <= 0) {
+ return ResponseEntity.badRequest()
+ .body(new TransportResponse(
+ "Invalid capacity: must be greater than 0",
+ capacityKg,
+ List.of(),
+ 0,
+ 0
+ ));
+ }
+
+ // Obtener todos los perros disponibles
+ List allDogs = dogRepository.findAll();
+
+ if (allDogs.isEmpty()) {
+ return ResponseEntity.ok(new TransportResponse(
+ "No dogs available for transport",
+ capacityKg,
+ List.of(),
+ 0,
+ 0
+ ));
+ }
+
+ // Ejecutar algoritmo de programación dinámica
+ TransportService.KnapsackResult result = transportService.optimizeTransport(allDogs, capacityKg);
+
+ // Convertir perros a DTO simple
+ List selectedDogs = result.selectedDogs.stream()
+ .map(dog -> new DogDTO(
+ dog.getId(),
+ dog.getName(),
+ dog.getWeight(),
+ dog.getPriority()
+ ))
+ .toList();
+
+ return ResponseEntity.ok(new TransportResponse(
+ "Optimal transport computed using Dynamic Programming (Knapsack)",
+ capacityKg,
+ selectedDogs,
+ result.totalPriority,
+ result.totalWeight
+ ));
+ }
+
+ // DTOs para respuesta
+ record TransportResponse(
+ String message,
+ int vehicleCapacityKg,
+ List selectedDogs,
+ int totalPriority,
+ int totalWeightKg
+ ) {}
+
+ record DogDTO(
+ String id,
+ String name,
+ int weightKg,
+ int priority
+ ) {}
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/domain/Adopter.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/domain/Adopter.java
new file mode 100644
index 000000000..2d5e4f20c
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/domain/Adopter.java
@@ -0,0 +1,17 @@
+package com.programacion3.adoptme.domain;
+
+import org.springframework.data.neo4j.core.schema.Id;
+import org.springframework.data.neo4j.core.schema.Node;
+import lombok.*;
+
+@Node("Adopter")
+@Data @NoArgsConstructor @AllArgsConstructor @Builder
+public class Adopter {
+ @Id
+ private String id;
+ private String name;
+ private Integer budget;
+ private Boolean hasYard;
+ private Boolean hasKids;
+ private Integer maxDogs;
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/domain/Dog.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/domain/Dog.java
new file mode 100644
index 000000000..eda662218
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/domain/Dog.java
@@ -0,0 +1,25 @@
+package com.programacion3.adoptme.domain;
+
+import org.springframework.data.neo4j.core.schema.Id;
+import org.springframework.data.neo4j.core.schema.Node;
+import lombok.*;
+
+@Node("Dog")
+@Data @NoArgsConstructor @AllArgsConstructor @Builder
+public class Dog {
+ @Id
+ private String id;
+ private String name;
+ private String size; // SMALL, MEDIUM, LARGE
+ private Integer weightKg;
+ private Integer age;
+ private String energy; // LOW, MEDIUM, HIGH
+ private Boolean goodWithKids;
+ private Boolean specialNeeds;
+ private Integer priority; // Para priorización de adopción
+
+ // Método personalizado para mantener compatibilidad con código existente
+ public Integer getWeight() {
+ return this.weightKg;
+ }
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/domain/Shelter.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/domain/Shelter.java
new file mode 100644
index 000000000..c764e6f4a
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/domain/Shelter.java
@@ -0,0 +1,14 @@
+package com.programacion3.adoptme.domain;
+
+import org.springframework.data.neo4j.core.schema.Id;
+import org.springframework.data.neo4j.core.schema.Node;
+import lombok.*;
+
+@Node("Shelter")
+@Data @NoArgsConstructor @AllArgsConstructor @Builder
+public class Shelter {
+ @Id
+ private String id;
+ private String name;
+ private Integer capacity;
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/dto/AdoptionResponse.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/dto/AdoptionResponse.java
new file mode 100644
index 000000000..52812a38e
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/dto/AdoptionResponse.java
@@ -0,0 +1,13 @@
+package com.programacion3.adoptme.dto;
+
+import lombok.*;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class AdoptionResponse {
+ private String adopterId;
+ private String dogId;
+ private String message;
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/dto/PathResponse.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/dto/PathResponse.java
new file mode 100644
index 000000000..798b698d9
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/dto/PathResponse.java
@@ -0,0 +1,11 @@
+package com.programacion3.adoptme.dto;
+
+import java.util.List;
+
+public record PathResponse(
+ boolean exists,
+ String method,
+ List path,
+ int steps,
+ double totalWeight
+) {}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/dto/TspResponse.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/dto/TspResponse.java
new file mode 100644
index 000000000..216548f61
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/dto/TspResponse.java
@@ -0,0 +1,14 @@
+package com.programacion3.adoptme.dto;
+
+import lombok.*;
+
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class TspResponse {
+ private List route;
+ private Integer totalDistanceKm;
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/exception/ApiExceptionHandler.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/exception/ApiExceptionHandler.java
new file mode 100644
index 000000000..e403678d1
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/exception/ApiExceptionHandler.java
@@ -0,0 +1,54 @@
+package com.programacion3.adoptme.exception;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import java.time.LocalDateTime;
+
+@RestControllerAdvice
+public class ApiExceptionHandler {
+
+ // Maneja recursos no encontrados
+ @ExceptionHandler(ResourceNotFoundException.class)
+ public ResponseEntity handleNotFound(ResourceNotFoundException ex) {
+ return ResponseEntity.status(HttpStatus.NOT_FOUND)
+ .body(new ErrorResponse(LocalDateTime.now(), HttpStatus.NOT_FOUND.value(), ex.getMessage()));
+ }
+
+ // Maneja errores de validación
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ResponseEntity handleValidation(MethodArgumentNotValidException ex) {
+ String message = ex.getBindingResult().getFieldErrors()
+ .stream()
+ .map(err -> err.getField() + ": " + err.getDefaultMessage())
+ .reduce((m1,m2) -> m1 + "; " + m2)
+ .orElse(ex.getMessage());
+
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST)
+ .body(new ErrorResponse(LocalDateTime.now(), HttpStatus.BAD_REQUEST.value(), message));
+ }
+
+ // Maneja cualquier error no previsto
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity handleGeneric(Exception ex) {
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+ .body(new ErrorResponse(LocalDateTime.now(), HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage()));
+ }
+
+}
+
+// Clase interna para formato de error estandarizado
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+class ErrorResponse {
+ private LocalDateTime timestamp;
+ private int status;
+ private String message;
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/exception/ResourceNotFoundException.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/exception/ResourceNotFoundException.java
new file mode 100644
index 000000000..f2183b8b0
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/exception/ResourceNotFoundException.java
@@ -0,0 +1,7 @@
+package com.programacion3.adoptme.exception;
+
+public class ResourceNotFoundException extends RuntimeException {
+ public ResourceNotFoundException(String msg) {
+ super(msg);
+ }
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/repo/AdopterRepository.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/repo/AdopterRepository.java
new file mode 100644
index 000000000..e6abc7be2
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/repo/AdopterRepository.java
@@ -0,0 +1,6 @@
+package com.programacion3.adoptme.repo;
+
+import com.programacion3.adoptme.domain.Adopter;
+import org.springframework.data.neo4j.repository.Neo4jRepository;
+
+public interface AdopterRepository extends Neo4jRepository {}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/repo/DogRepository.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/repo/DogRepository.java
new file mode 100644
index 000000000..b8b47abf9
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/repo/DogRepository.java
@@ -0,0 +1,6 @@
+package com.programacion3.adoptme.repo;
+
+import com.programacion3.adoptme.domain.Dog;
+import org.springframework.data.neo4j.repository.Neo4jRepository;
+
+public interface DogRepository extends Neo4jRepository {}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/repo/ShelterRepository.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/repo/ShelterRepository.java
new file mode 100644
index 000000000..d8f89102c
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/repo/ShelterRepository.java
@@ -0,0 +1,6 @@
+package com.programacion3.adoptme.repo;
+
+import com.programacion3.adoptme.domain.Shelter;
+import org.springframework.data.neo4j.repository.Neo4jRepository;
+
+public interface ShelterRepository extends Neo4jRepository {}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/BacktrackingService.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/BacktrackingService.java
new file mode 100644
index 000000000..55f83d939
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/BacktrackingService.java
@@ -0,0 +1,265 @@
+package com.programacion3.adoptme.service;
+
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+/**
+ * Servicio que implementa Backtracking para asignar perros a adoptantes
+ * con restricciones de compatibilidad, presupuesto y capacidad.
+ *
+ * Problema: Asignar N perros a M adoptantes de forma que:
+ * - Cada perro se asigna a máximo un adoptante
+ * - Se respetan restricciones de presupuesto, capacidad y compatibilidad
+ * - Se maximiza la satisfacción total
+ */
+@Service
+public class BacktrackingService {
+
+ /**
+ * Información de un perro para asignación
+ */
+ public static class Dog {
+ public final String id;
+ public final boolean goodWithKids;
+ public final boolean needsGarden;
+ public final int energy;
+ public final double cost;
+
+ public Dog(String id, boolean goodWithKids, boolean needsGarden, int energy, double cost) {
+ this.id = id;
+ this.goodWithKids = goodWithKids;
+ this.needsGarden = needsGarden;
+ this.energy = energy;
+ this.cost = cost;
+ }
+ }
+
+ /**
+ * Información de un adoptante
+ */
+ public static class Adopter {
+ public final String id;
+ public final String name;
+ public final boolean hasKids;
+ public final boolean hasGarden;
+ public final int maxDogs;
+ public final double budget;
+ public final int preferredEnergy; // 1-10
+
+ public Adopter(String id, String name, boolean hasKids, boolean hasGarden,
+ int maxDogs, double budget, int preferredEnergy) {
+ this.id = id;
+ this.name = name;
+ this.hasKids = hasKids;
+ this.hasGarden = hasGarden;
+ this.maxDogs = maxDogs;
+ this.budget = budget;
+ this.preferredEnergy = preferredEnergy;
+ }
+ }
+
+ /**
+ * Asignación: adopterId -> lista de dogIds
+ */
+ public static class Assignment {
+ public final Map> assignments; // adopterId -> [dogIds]
+ public final double totalScore;
+
+ public Assignment(Map> assignments, double totalScore) {
+ this.assignments = assignments;
+ this.totalScore = totalScore;
+ }
+ }
+
+ /**
+ * Encuentra la mejor asignación usando backtracking.
+ *
+ * @param dogs lista de perros disponibles
+ * @param adopters lista de adoptantes
+ * @return mejor asignación encontrada
+ */
+ public Assignment findBestAssignment(List dogs, List adopters) {
+ if (dogs.isEmpty() || adopters.isEmpty()) {
+ return new Assignment(new HashMap<>(), 0.0);
+ }
+
+ // OPTIMIZATION: Limit to first 20 dogs to keep execution time reasonable
+ // With 30+ dogs and 15 adopters, complexity becomes too high
+ List limitedDogs = dogs.size() > 20 ? dogs.subList(0, 20) : dogs;
+
+ // Estado inicial: ningún perro asignado
+ Map> currentAssignment = new HashMap<>();
+ for (Adopter a : adopters) {
+ currentAssignment.put(a.id, new ArrayList<>());
+ }
+
+ Map currentCost = new HashMap<>();
+ for (Adopter a : adopters) {
+ currentCost.put(a.id, 0.0);
+ }
+
+ // Variables para la mejor solución encontrada
+ BestSolution best = new BestSolution();
+ best.startTime = System.currentTimeMillis();
+ best.timeoutMs = 5000; // 5 second timeout
+
+ // Iniciar backtracking
+ backtrack(0, limitedDogs, adopters, currentAssignment, currentCost, 0.0, best);
+
+ System.out.println("[BACKTRACKING] Explored " + best.nodesExplored + " nodes");
+ System.out.println("[BACKTRACKING] Best score: " + best.score);
+
+ return new Assignment(best.assignments, best.score);
+ }
+
+ /**
+ * Clase auxiliar para mantener la mejor solución
+ */
+ private static class BestSolution {
+ Map> assignments = new HashMap<>();
+ double score = 0.0;
+ long startTime = 0;
+ long timeoutMs = 5000;
+ int nodesExplored = 0;
+
+ boolean isTimeout() {
+ return System.currentTimeMillis() - startTime > timeoutMs;
+ }
+ }
+
+ /**
+ * Algoritmo de backtracking recursivo.
+ *
+ * @param dogIndex índice del perro actual a asignar
+ * @param dogs lista de perros
+ * @param adopters lista de adoptantes
+ * @param currentAssignment asignación actual
+ * @param currentCost costo acumulado por adoptante
+ * @param currentScore score total actual
+ * @param best mejor solución encontrada hasta ahora
+ */
+ private void backtrack(
+ int dogIndex,
+ List dogs,
+ List adopters,
+ Map> currentAssignment,
+ Map currentCost,
+ double currentScore,
+ BestSolution best
+ ) {
+ // Check timeout
+ if (best.isTimeout()) {
+ return;
+ }
+
+ best.nodesExplored++;
+
+ // Caso base: todos los perros fueron considerados
+ if (dogIndex == dogs.size()) {
+ // Si esta solución es mejor, guardarla
+ if (currentScore > best.score) {
+ best.score = currentScore;
+ best.assignments = deepCopy(currentAssignment);
+ }
+ return;
+ }
+
+ Dog dog = dogs.get(dogIndex);
+
+ // Opción 1: No asignar este perro a nadie (puede quedar sin adoptar)
+ backtrack(dogIndex + 1, dogs, adopters, currentAssignment, currentCost, currentScore, best);
+
+ // Opción 2: Intentar asignar este perro a cada adoptante
+ for (Adopter adopter : adopters) {
+ // Check timeout periodically
+ if (best.nodesExplored % 1000 == 0 && best.isTimeout()) {
+ return;
+ }
+
+ // Verificar si es viable asignar este perro a este adoptante
+ if (canAssign(dog, adopter, currentAssignment, currentCost)) {
+ // Calcular score de esta asignación
+ double score = calculateScore(dog, adopter);
+
+ // Hacer asignación (forward)
+ currentAssignment.get(adopter.id).add(dog.id);
+ currentCost.put(adopter.id, currentCost.get(adopter.id) + dog.cost);
+
+ // Recursión
+ backtrack(dogIndex + 1, dogs, adopters, currentAssignment, currentCost,
+ currentScore + score, best);
+
+ // Deshacer asignación (backtrack)
+ currentAssignment.get(adopter.id).remove(currentAssignment.get(adopter.id).size() - 1);
+ currentCost.put(adopter.id, currentCost.get(adopter.id) - dog.cost);
+ }
+ }
+ }
+
+ /**
+ * Verifica si se puede asignar un perro a un adoptante respetando restricciones.
+ */
+ private boolean canAssign(
+ Dog dog,
+ Adopter adopter,
+ Map> currentAssignment,
+ Map currentCost
+ ) {
+ // Restricción 1: No exceder capacidad máxima de perros
+ if (currentAssignment.get(adopter.id).size() >= adopter.maxDogs) {
+ return false;
+ }
+
+ // Restricción 2: No exceder presupuesto
+ if (currentCost.get(adopter.id) + dog.cost > adopter.budget) {
+ return false;
+ }
+
+ // Restricción 3: Si el perro no es bueno con niños y el adoptante tiene niños, no asignar
+ if (adopter.hasKids && !dog.goodWithKids) {
+ return false;
+ }
+
+ // Restricción 4: Si el perro necesita jardín y el adoptante no tiene, no asignar
+ if (dog.needsGarden && !adopter.hasGarden) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Calcula el score de asignar un perro específico a un adoptante específico.
+ */
+ private double calculateScore(Dog dog, Adopter adopter) {
+ double score = 0.0;
+
+ // +5 puntos si es compatible con niños y el adoptante tiene niños
+ if (adopter.hasKids && dog.goodWithKids) {
+ score += 5.0;
+ }
+
+ // +3 puntos si el perro necesita jardín y el adoptante tiene jardín
+ if (dog.needsGarden && adopter.hasGarden) {
+ score += 3.0;
+ }
+
+ // +0 a +5 puntos por compatibilidad de energía (menor diferencia = mejor)
+ int energyDiff = Math.abs(dog.energy - adopter.preferredEnergy);
+ score += Math.max(0, 5.0 - energyDiff);
+
+ return score;
+ }
+
+ /**
+ * Crea una copia profunda del mapa de asignaciones.
+ */
+ private Map> deepCopy(Map> original) {
+ Map> copy = new HashMap<>();
+ for (Map.Entry> entry : original.entrySet()) {
+ copy.put(entry.getKey(), new ArrayList<>(entry.getValue()));
+ }
+ return copy;
+ }
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/GraphLoader.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/GraphLoader.java
new file mode 100644
index 000000000..2cc595b76
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/GraphLoader.java
@@ -0,0 +1,93 @@
+package com.programacion3.adoptme.service;
+
+import com.programacion3.adoptme.service.ShortestPathService.Edge;
+import org.springframework.data.neo4j.core.Neo4jClient;
+import org.springframework.stereotype.Component;
+
+import java.util.*;
+
+/**
+ * Carga la lista de adyacencias y aristas con pesos desde Neo4j
+ */
+@Component
+public class GraphLoader {
+
+ private final Neo4jClient neo4j;
+
+ public GraphLoader(Neo4jClient neo4j) {
+ this.neo4j = neo4j;
+ }
+
+ /**
+ * Carga adyacencias simples (sin pesos) para BFS/DFS
+ */
+ public Map> loadAdjacency() {
+ String q = """
+ MATCH (a:Shelter)-[r:NEAR]->(b:Shelter)
+ RETURN a.id AS from, b.id AS to
+ """;
+
+ Map> g = new HashMap<>();
+ neo4j.query(q).fetch().all().forEach(row -> {
+ String from = (String) row.get("from");
+ String to = (String) row.get("to");
+ g.computeIfAbsent(from, k -> new ArrayList<>()).add(to);
+ });
+ return g;
+ }
+
+ /**
+ * Carga aristas con pesos (distKm) para Dijkstra y MST
+ * Crea aristas bidireccionales (grafo no dirigido)
+ */
+ public List loadWeightedEdges() {
+ String q = """
+ MATCH (a:Shelter)-[r:NEAR]->(b:Shelter)
+ RETURN a.id AS from, b.id AS to, r.distKm AS weight
+ """;
+
+ List edges = new ArrayList<>();
+ neo4j.query(q).fetch().all().forEach(row -> {
+ String from = (String) row.get("from");
+ String to = (String) row.get("to");
+ double weight = ((Number) row.get("weight")).doubleValue();
+
+ // Agregamos la arista en ambas direcciones (grafo no dirigido)
+ edges.add(new Edge(from, to, weight));
+ edges.add(new Edge(to, from, weight));
+ });
+ return edges;
+ }
+
+ /**
+ * Obtiene todos los IDs de shelters para MST
+ */
+ public Set loadAllShelterIds() {
+ String q = "MATCH (s:Shelter) RETURN s.id AS id";
+ Set ids = new HashSet<>();
+ neo4j.query(q).fetch().all().forEach(row -> {
+ ids.add((String) row.get("id"));
+ });
+ return ids;
+ }
+
+ /**
+ * Carga aristas como MSTService.Edge para compatibilidad
+ */
+ public List loadMSTEdges() {
+ String q = """
+ MATCH (a:Shelter)-[r:NEAR]->(b:Shelter)
+ RETURN a.id AS from, b.id AS to, r.distKm AS weight
+ """;
+
+ List edges = new ArrayList<>();
+ neo4j.query(q).fetch().all().forEach(row -> {
+ String from = (String) row.get("from");
+ String to = (String) row.get("to");
+ double weight = ((Number) row.get("weight")).doubleValue();
+
+ edges.add(new MSTService.Edge(from, to, weight, "NEAR"));
+ });
+ return edges;
+ }
+}
\ No newline at end of file
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/GraphService.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/GraphService.java
new file mode 100644
index 000000000..9ccae1964
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/GraphService.java
@@ -0,0 +1,70 @@
+package com.programacion3.adoptme.service;
+
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+@Service
+public class GraphService {
+
+ private final GraphLoader loader;
+
+ public GraphService(GraphLoader loader) {
+ this.loader = loader;
+ }
+
+ /** Camino más corto en cantidad de pasos (BFS). Devuelve vacío si no hay. */
+ public List bfsPath(String from, String to) {
+ Map> g = loader.loadAdjacency();
+ if (from == null || to == null || from.isBlank() || to.isBlank()) return List.of();
+ if (from.equals(to)) return List.of(from);
+
+ Queue q = new ArrayDeque<>();
+ Map prev = new HashMap<>();
+ Set vis = new HashSet<>();
+
+ q.add(from);
+ vis.add(from);
+
+ while (!q.isEmpty()) {
+ String u = q.poll();
+ if (u.equals(to)) break;
+ for (String v : g.getOrDefault(u, List.of())) {
+ if (!vis.contains(v)) {
+ vis.add(v);
+ prev.put(v, u);
+ q.add(v);
+ }
+ }
+ }
+
+ if (!vis.contains(to)) return List.of(); // no hay camino
+
+ LinkedList path = new LinkedList<>();
+ for (String cur = to; cur != null; cur = prev.get(cur)) path.addFirst(cur);
+ return path;
+ }
+
+ /** DFS: intenta encontrar algún camino; no garantiza mínimo en pasos. */
+ public List dfsPath(String from, String to) {
+ Map> g = loader.loadAdjacency();
+ List path = new ArrayList<>();
+ Set vis = new HashSet<>();
+ if (dfs(g, from, to, vis, path)) return path;
+ return List.of();
+ }
+
+ private boolean dfs(Map> g, String u, String to, Set vis, List path) {
+ if (u == null) return false;
+ path.add(u);
+ if (u.equals(to)) return true;
+ vis.add(u);
+ for (String v : g.getOrDefault(u, List.of())) {
+ if (!vis.contains(v)) {
+ if (dfs(g, v, to, vis, path)) return true;
+ }
+ }
+ path.remove(path.size() - 1);
+ return false;
+ }
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/MSTService.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/MSTService.java
new file mode 100644
index 000000000..27b15d5bf
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/MSTService.java
@@ -0,0 +1,169 @@
+package com.programacion3.adoptme.service;
+
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+
+/*
+ Kruskal simple que opera sobre un subconjunto de nodos (p. ej. shelters/hubs)
+ y aristas filtradas por tipo "NEAR". Devuelve lista de aristas del MST y costo total.
+*/
+@Service
+public class MSTService {
+
+ public static class Edge {
+ public final String a;
+ public final String b;
+ public final double weight;
+ public final String type; // p.ej. "NEAR"
+
+ public Edge(String a, String b, double weight, String type) {
+ this.a = a;
+ this.b = b;
+ this.weight = weight;
+ this.type = type;
+ }
+ }
+
+ public static class MSTResult {
+ public final List edges;
+ public final double totalWeight;
+ public MSTResult(List edges, double totalWeight) {
+ this.edges = edges; this.totalWeight = totalWeight;
+ }
+ }
+
+ /**
+ * Algoritmo de Kruskal para MST.
+ * Ordena todas las aristas por peso y las agrega greedily evitando ciclos (Union-Find).
+ */
+ public MSTResult compute(Collection nodesOfInterest, Collection allEdges) {
+ // Filtrar aristas NEAR que conecten nodos de interés
+ List edges = new ArrayList<>();
+ for (Edge e : allEdges) {
+ if (!"NEAR".equalsIgnoreCase(e.type)) continue;
+ if (nodesOfInterest.contains(e.a) && nodesOfInterest.contains(e.b)) edges.add(e);
+ }
+ edges.sort(Comparator.comparingDouble(e -> e.weight));
+ UnionFind uf = new UnionFind(nodesOfInterest);
+ List mst = new ArrayList<>();
+ double total = 0.0;
+ for (Edge e : edges) {
+ if (uf.union(e.a, e.b)) {
+ mst.add(e);
+ total += e.weight;
+ }
+ }
+ return new MSTResult(mst, total);
+ }
+
+ /**
+ * Algoritmo de Prim para MST.
+ * Empieza desde un nodo inicial y expande el árbol agregando la arista de menor peso
+ * que conecta un nodo del árbol con uno fuera del árbol.
+ */
+ public MSTResult computeWithPrim(Collection nodesOfInterest, Collection allEdges) {
+ if (nodesOfInterest.isEmpty()) {
+ return new MSTResult(new ArrayList<>(), 0.0);
+ }
+
+ // Filtrar aristas NEAR que conecten nodos de interés
+ List edges = new ArrayList<>();
+ for (Edge e : allEdges) {
+ if (!"NEAR".equalsIgnoreCase(e.type)) continue;
+ if (nodesOfInterest.contains(e.a) && nodesOfInterest.contains(e.b)) edges.add(e);
+ }
+
+ // Construir lista de adyacencia con pesos
+ Map> adj = new HashMap<>();
+ for (String node : nodesOfInterest) {
+ adj.put(node, new ArrayList<>());
+ }
+ for (Edge e : edges) {
+ adj.get(e.a).add(e);
+ // Agregar arista inversa para grafo no dirigido
+ adj.get(e.b).add(new Edge(e.b, e.a, e.weight, e.type));
+ }
+
+ // Prim: empezar desde un nodo arbitrario
+ String start = nodesOfInterest.iterator().next();
+ Set inMST = new HashSet<>();
+ inMST.add(start);
+
+ // Cola de prioridad de aristas: (peso, arista)
+ PriorityQueue pq = new PriorityQueue<>(Comparator.comparingDouble(e -> e.weight));
+
+ // Agregar todas las aristas del nodo inicial
+ for (Edge e : adj.get(start)) {
+ pq.offer(new EdgeWithPriority(e.weight, e));
+ }
+
+ List mst = new ArrayList<>();
+ double total = 0.0;
+
+ while (!pq.isEmpty() && inMST.size() < nodesOfInterest.size()) {
+ EdgeWithPriority current = pq.poll();
+ Edge edge = current.edge;
+
+ // Si ambos extremos ya están en el MST, saltar (evita ciclos)
+ if (inMST.contains(edge.b)) {
+ continue;
+ }
+
+ // Agregar arista al MST
+ mst.add(edge);
+ total += edge.weight;
+ inMST.add(edge.b);
+
+ // Agregar todas las aristas del nuevo nodo
+ for (Edge e : adj.get(edge.b)) {
+ if (!inMST.contains(e.b)) {
+ pq.offer(new EdgeWithPriority(e.weight, e));
+ }
+ }
+ }
+
+ return new MSTResult(mst, total);
+ }
+
+ // Clase auxiliar para la cola de prioridad de Prim
+ private static class EdgeWithPriority {
+ final double weight;
+ final Edge edge;
+
+ EdgeWithPriority(double weight, Edge edge) {
+ this.weight = weight;
+ this.edge = edge;
+ }
+ }
+
+ private static class UnionFind {
+ private final Map parent;
+ private final Map rank;
+
+ UnionFind(Collection nodes) {
+ parent = new HashMap<>();
+ rank = new HashMap<>();
+ for (String n : nodes) { parent.put(n, n); rank.put(n, 0); }
+ }
+
+ String find(String x) {
+ String p = parent.get(x);
+ if (p == null) return x;
+ if (!p.equals(x)) parent.put(x, find(p));
+ return parent.get(x);
+ }
+
+ boolean union(String a, String b) {
+ String ra = find(a), rb = find(b);
+ if (ra.equals(rb)) return false;
+ int rka = rank.getOrDefault(ra, 0), rkb = rank.getOrDefault(rb, 0);
+ if (rka < rkb) parent.put(ra, rb);
+ else if (rka > rkb) parent.put(rb, ra);
+ else { parent.put(rb, ra); rank.put(ra, rka + 1); }
+ return true;
+ }
+ }
+}
+
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/ScorerService.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/ScorerService.java
new file mode 100644
index 000000000..7fd442e8c
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/ScorerService.java
@@ -0,0 +1,80 @@
+// java
+package com.programacion3.adoptme.service;
+
+import java.util.*;
+import java.util.stream.Collectors;
+import org.springframework.stereotype.Service;
+
+
+
+/*
+ Scorer simple: calcula score por perro según criterios (children/garden/energy/size),
+ ordena por score descendente y asigna hasta maxDogs o presupuesto.
+*/
+@Service
+public class ScorerService {
+ public static class Dog {
+ public final String id;
+ public final boolean goodWithKids;
+ public final boolean hasGardenNeeded;
+ public final int energy; // 1..10
+ public final int size; // 1 small .. 3 large
+ public final double cost; // asumido costo por adopción
+
+ public Dog(String id, boolean goodWithKids, boolean hasGardenNeeded, int energy, int size, double cost) {
+ this.id = id; this.goodWithKids = goodWithKids; this.hasGardenNeeded = hasGardenNeeded;
+ this.energy = energy; this.size = size; this.cost = cost;
+ }
+ }
+
+ public static class AssignmentResult {
+ public final List assigned;
+ public final double totalScore;
+ public final double totalCost;
+ public AssignmentResult(List assigned, double totalScore, double totalCost) {
+ this.assigned = assigned; this.totalScore = totalScore; this.totalCost = totalCost;
+ }
+ }
+
+ // Scoring configurable simple ejemplo:
+ private double scoreFor(Dog d, boolean adopterHasKids, boolean adopterHasGarden, double weights[]) {
+ double s = 0;
+ if (adopterHasKids && d.goodWithKids) s += weights[0]; // peso niños
+ if (adopterHasGarden && d.hasGardenNeeded) s += weights[1]; // jardín
+ // prefiero energía moderada: penaliza extremos
+ s += weights[2] * (1.0 - Math.abs(d.energy - 5) / 5.0);
+ // tamaño preferencia neutra -> pequeño un poco mejor
+ s += weights[3] * (3.0 - d.size) / 2.0;
+ return s;
+ }
+
+ public AssignmentResult scoreAndAssign(List candidates,
+ boolean adopterHasKids,
+ boolean adopterHasGarden,
+ int maxDogs,
+ double budget) {
+ double[] weights = {3.0, 2.0, 2.0, 1.0}; // niños, jardín, energía, tamaño
+ List scored = candidates.stream()
+ .map(d -> new ScoredDog(d, scoreFor(d, adopterHasKids, adopterHasGarden, weights)))
+ .sorted(Comparator.comparingDouble((ScoredDog sd) -> sd.score).reversed())
+ .collect(Collectors.toList());
+
+ List assigned = new ArrayList<>();
+ double totalCost = 0, totalScore = 0;
+ for (ScoredDog sd : scored) {
+ if (assigned.size() >= maxDogs) break;
+ if (totalCost + sd.dog.cost > budget) continue;
+ assigned.add(sd.dog);
+ totalCost += sd.dog.cost;
+ totalScore += sd.score;
+ }
+ return new AssignmentResult(assigned, totalScore, totalCost);
+ }
+
+ private static class ScoredDog {
+ final Dog dog;
+ final double score;
+ ScoredDog(Dog dog, double score) { this.dog = dog; this.score = score; }
+ }
+}
+
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/ShortestPathService.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/ShortestPathService.java
new file mode 100644
index 000000000..e334801b5
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/ShortestPathService.java
@@ -0,0 +1,84 @@
+package com.programacion3.adoptme.service;
+import org.springframework.stereotype.Service;
+import java.util.*;
+/*
+ Minimal Dijkstra que usa PriorityQueue y devuelve costo + camino.
+ Entrada: colección de aristas dirigidas/indirectas (Edge), ids de nodo como String.
+
+*/
+@Service
+public class ShortestPathService {
+
+ public static class Edge {
+ public final String from;
+ public final String to;
+ public final double weight;
+
+ public Edge(String from, String to, double weight) {
+ this.from = from;
+ this.to = to;
+ this.weight = weight;
+ }
+ }
+
+ public static class PathResult {
+ public final double cost;
+ public final List path;
+
+ public PathResult(double cost, List path) {
+ this.cost = cost;
+ this.path = path;
+ }
+ }
+
+ public PathResult shortestPath(String start, String goal, Collection edges) {
+ Map> adj = buildAdj(edges);
+ Map dist = new HashMap<>();
+ Map prev = new HashMap<>();
+ PriorityQueue pq = new PriorityQueue<>(Comparator.comparingDouble(n -> n.dist));
+ dist.put(start, 0.0);
+ pq.add(new Node(start, 0.0));
+
+ while (!pq.isEmpty()) {
+ Node cur = pq.poll();
+ if (cur.dist > dist.getOrDefault(cur.id, Double.POSITIVE_INFINITY)) continue;
+ if (cur.id.equals(goal)) break;
+ for (Edge e : adj.getOrDefault(cur.id, Collections.emptyList())) {
+ double nd = cur.dist + e.weight;
+ if (nd < dist.getOrDefault(e.to, Double.POSITIVE_INFINITY)) {
+ dist.put(e.to, nd);
+ prev.put(e.to, cur.id);
+ pq.add(new Node(e.to, nd));
+ }
+ }
+ }
+
+ if (!dist.containsKey(goal)) return new PathResult(Double.POSITIVE_INFINITY, Collections.emptyList());
+ List path = new ArrayList<>();
+ String cur = goal;
+ while (cur != null) {
+ path.add(cur);
+ cur = prev.get(cur);
+ }
+ Collections.reverse(path);
+ return new PathResult(dist.get(goal), path);
+ }
+
+ private Map> buildAdj(Collection edges) {
+ Map> adj = new HashMap<>();
+ for (Edge e : edges) {
+ adj.computeIfAbsent(e.from, k -> new ArrayList<>()).add(e);
+ // si el grafo es no dirigido, añadir la inversa:
+ adj.computeIfAbsent(e.to, k -> new ArrayList<>()).add(new Edge(e.to, e.from, e.weight));
+ }
+ return adj;
+ }
+
+ private static class Node {
+ final String id;
+ final double dist;
+ Node(String id, double dist) { this.id = id; this.dist = dist; }
+ }
+ }
+
+
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/SortService.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/SortService.java
new file mode 100644
index 000000000..51f9c74b0
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/SortService.java
@@ -0,0 +1,115 @@
+package com.programacion3.adoptme.service;
+
+import java.util.Comparator;
+import java.util.List;
+import org.springframework.stereotype.Service;
+import com.programacion3.adoptme.domain.Dog;
+
+
+@Service
+public class SortService {
+
+ /**
+ * Ordena perros usando TimSort (default) o QuickSort.
+ * @param dogs lista de perros a ordenar (se modifica in-place)
+ * @param criteria criterio de ordenamiento (priority, age, weight)
+ * @param algorithm algoritmo a usar (mergesort, quicksort)
+ */
+ public void sortDogs(List dogs, String criteria, String algorithm) {
+ if (algorithm == null || algorithm.isEmpty()) {
+ algorithm = "mergesort";
+ }
+
+ if ("quicksort".equalsIgnoreCase(algorithm)) {
+ quickSortDogs(dogs, criteria, 0, dogs.size() - 1);
+ } else {
+ // MergeSort (TimSort)
+ Comparator comparator = getComparator(criteria);
+ dogs.sort(comparator);
+ }
+ }
+
+ /**
+ * Versión legacy que usa MergeSort por defecto
+ */
+ public void sortDogs(List dogs, String criteria) {
+ sortDogs(dogs, criteria, "mergesort");
+ }
+
+ /**
+ * Implementación de QuickSort para perros
+ * Algoritmo divide y vencerás que particiona la lista recursivamente
+ */
+ private void quickSortDogs(List dogs, String criteria, int low, int high) {
+ if (low < high) {
+ int pivotIndex = partition(dogs, criteria, low, high);
+ quickSortDogs(dogs, criteria, low, pivotIndex - 1);
+ quickSortDogs(dogs, criteria, pivotIndex + 1, high);
+ }
+ }
+
+ /**
+ * Particiona la lista usando el último elemento como pivote
+ */
+ private int partition(List dogs, String criteria, int low, int high) {
+ Dog pivot = dogs.get(high);
+ int i = low - 1;
+
+ for (int j = low; j < high; j++) {
+ if (compare(dogs.get(j), pivot, criteria) <= 0) {
+ i++;
+ swap(dogs, i, j);
+ }
+ }
+
+ swap(dogs, i + 1, high);
+ return i + 1;
+ }
+
+ /**
+ * Compara dos perros según el criterio especificado
+ * @return negativo si a < b, 0 si a == b, positivo si a > b
+ */
+ private int compare(Dog a, Dog b, String criteria) {
+ switch (criteria.toLowerCase()) {
+ case "priority":
+ // Orden ascendente (menor prioridad primero)
+ return Integer.compare(a.getPriority(), b.getPriority());
+ case "age":
+ return Integer.compare(a.getAge(), b.getAge());
+ case "weight":
+ return Integer.compare(a.getWeight(), b.getWeight());
+ default:
+ throw new IllegalArgumentException("Criterio de orden no válido: " + criteria);
+ }
+ }
+
+ /**
+ * Intercambia dos elementos en la lista
+ */
+ private void swap(List dogs, int i, int j) {
+ Dog temp = dogs.get(i);
+ dogs.set(i, dogs.get(j));
+ dogs.set(j, temp);
+ }
+
+ /**
+ * Obtiene el comparador para un criterio dado
+ */
+ private Comparator getComparator(String criteria) {
+ switch (criteria.toLowerCase()) {
+ case "priority":
+ // Orden ascendente (menor prioridad primero)
+ return Comparator.comparingInt(Dog::getPriority);
+ case "age":
+ return Comparator.comparingInt(Dog::getAge);
+ case "weight":
+ return Comparator.comparingDouble(Dog::getWeight);
+ default:
+ throw new IllegalArgumentException("Criterio de orden no válido: " + criteria);
+ }
+ }
+}
+
+
+
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/TSPService.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/TSPService.java
new file mode 100644
index 000000000..88a6516cc
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/TSPService.java
@@ -0,0 +1,363 @@
+package com.programacion3.adoptme.service;
+
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+/**
+ * Servicio que implementa Branch & Bound para el Problema del Viajante (TSP).
+ *
+ * Encuentra la ruta más corta que visita todos los nodos exactamente una vez
+ * y regresa al nodo de inicio.
+ */
+@Service
+public class TSPService {
+
+ /**
+ * Representa una arista con peso
+ */
+ public static class Edge {
+ public final String from;
+ public final String to;
+ public final double weight;
+
+ public Edge(String from, String to, double weight) {
+ this.from = from;
+ this.to = to;
+ this.weight = weight;
+ }
+ }
+
+ /**
+ * Resultado del TSP
+ */
+ public static class TSPResult {
+ public final List route; // Secuencia de nodos en el tour óptimo
+ public final double totalDistance; // Distancia total del tour
+
+ public TSPResult(List route, double totalDistance) {
+ this.route = route;
+ this.totalDistance = totalDistance;
+ }
+ }
+
+ /**
+ * Resuelve TSP usando Branch & Bound
+ *
+ * @param nodes lista de nodos a visitar
+ * @param edges lista de aristas con distancias
+ * @return tour óptimo y su distancia total
+ */
+ public TSPResult solveTSP(List nodes, List edges) {
+ if (nodes == null || nodes.isEmpty()) {
+ return new TSPResult(new ArrayList<>(), 0.0);
+ }
+
+ if (nodes.size() == 1) {
+ return new TSPResult(new ArrayList<>(nodes), 0.0);
+ }
+
+ // Construir matriz de distancias
+ Map> distMatrix = buildDistanceMatrix(nodes, edges);
+
+ // Verificar si el grafo es conexo
+ if (!isConnected(nodes, distMatrix)) {
+ return new TSPResult(new ArrayList<>(), Double.POSITIVE_INFINITY);
+ }
+
+ // Variables para la mejor solución
+ BestSolution best = new BestSolution();
+ best.cost = Double.POSITIVE_INFINITY;
+
+ // Empezar desde el primer nodo
+ String startNode = nodes.get(0);
+ List currentPath = new ArrayList<>();
+ currentPath.add(startNode);
+
+ Set visited = new HashSet<>();
+ visited.add(startNode);
+
+ // Branch & Bound
+ branchAndBound(startNode, startNode, currentPath, visited, 0.0, nodes, distMatrix, best);
+
+ return new TSPResult(best.route, best.cost);
+ }
+
+ /**
+ * Clase para mantener la mejor solución encontrada
+ */
+ private static class BestSolution {
+ List route = new ArrayList<>();
+ double cost = Double.POSITIVE_INFINITY;
+ }
+
+ /**
+ * Algoritmo de Branch & Bound recursivo
+ *
+ * @param startNode nodo de inicio del tour
+ * @param currentNode nodo actual
+ * @param currentPath camino actual
+ * @param visited conjunto de nodos visitados
+ * @param currentCost costo acumulado
+ * @param allNodes todos los nodos a visitar
+ * @param distMatrix matriz de distancias
+ * @param best mejor solución encontrada
+ */
+ private void branchAndBound(
+ String startNode,
+ String currentNode,
+ List currentPath,
+ Set visited,
+ double currentCost,
+ List allNodes,
+ Map> distMatrix,
+ BestSolution best
+ ) {
+ // Caso base: todos los nodos visitados
+ if (visited.size() == allNodes.size()) {
+ // Agregar costo de regresar al inicio
+ double returnCost = getDistance(currentNode, startNode, distMatrix);
+
+ if (returnCost != Double.POSITIVE_INFINITY) {
+ double totalCost = currentCost + returnCost;
+
+ // Si es mejor que la solución actual, actualizarla
+ if (totalCost < best.cost) {
+ best.cost = totalCost;
+ best.route = new ArrayList<>(currentPath);
+ best.route.add(startNode); // Completar el ciclo
+ }
+ }
+ return;
+ }
+
+ // Calcular bound (cota inferior)
+ double bound = currentCost + calculateBound(currentNode, visited, allNodes, distMatrix);
+
+ // Poda: si el bound supera la mejor solución conocida, no explorar esta rama
+ if (bound >= best.cost) {
+ return;
+ }
+
+ // Explorar todos los nodos no visitados
+ for (String nextNode : allNodes) {
+ if (!visited.contains(nextNode)) {
+ double edgeCost = getDistance(currentNode, nextNode, distMatrix);
+
+ if (edgeCost != Double.POSITIVE_INFINITY) {
+ // Forward
+ currentPath.add(nextNode);
+ visited.add(nextNode);
+
+ // Recursión
+ branchAndBound(
+ startNode,
+ nextNode,
+ currentPath,
+ visited,
+ currentCost + edgeCost,
+ allNodes,
+ distMatrix,
+ best
+ );
+
+ // Backtrack
+ currentPath.remove(currentPath.size() - 1);
+ visited.remove(nextNode);
+ }
+ }
+ }
+ }
+
+ /**
+ * Calcula una cota inferior (bound) para el costo restante.
+ * Usa la suma de las dos aristas más pequeñas de cada nodo no visitado.
+ */
+ private double calculateBound(
+ String currentNode,
+ Set visited,
+ List allNodes,
+ Map> distMatrix
+ ) {
+ double bound = 0.0;
+
+ // 1. Arista mínima desde nodo actual a cualquier no visitado
+ double minFromCurrent = Double.POSITIVE_INFINITY;
+ for (String node : allNodes) {
+ if (!visited.contains(node)) {
+ double dist = getDistance(currentNode, node, distMatrix);
+ minFromCurrent = Math.min(minFromCurrent, dist);
+ }
+ }
+ if (minFromCurrent != Double.POSITIVE_INFINITY) {
+ bound += minFromCurrent;
+ }
+
+ // 2. Costo MST de los nodos no visitados
+ List unvisited = allNodes.stream()
+ .filter(n -> !visited.contains(n))
+ .toList();
+
+ if (unvisited.size() > 1) {
+ bound += calculateMSTCost(unvisited, distMatrix);
+ }
+
+ // 3. Arista mínima desde cualquier no visitado de vuelta al inicio
+ String startNode = allNodes.get(0);
+ if (!visited.contains(startNode) && !unvisited.isEmpty()) {
+ double minToStart = Double.POSITIVE_INFINITY;
+ for (String node : unvisited) {
+ double dist = getDistance(node, startNode, distMatrix);
+ minToStart = Math.min(minToStart, dist);
+ }
+ if (minToStart != Double.POSITIVE_INFINITY) {
+ bound += minToStart;
+ }
+ }
+
+ return bound;
+ }
+
+ // Método auxiliar: Calcular costo MST con algoritmo de Prim
+ private double calculateMSTCost(
+ List nodes,
+ Map> distMatrix
+ ) {
+ if (nodes.isEmpty()) return 0.0;
+ if (nodes.size() == 1) return 0.0;
+
+ Set inMST = new HashSet<>();
+ double mstCost = 0.0;
+
+ inMST.add(nodes.get(0));
+
+ while (inMST.size() < nodes.size()) {
+ double minEdge = Double.POSITIVE_INFINITY;
+ String nextNode = null;
+
+ for (String inNode : inMST) {
+ for (String outNode : nodes) {
+ if (!inMST.contains(outNode)) {
+ double dist = getDistance(inNode, outNode, distMatrix);
+ if (dist < minEdge) {
+ minEdge = dist;
+ nextNode = outNode;
+ }
+ }
+ }
+ }
+
+ if (nextNode != null && minEdge != Double.POSITIVE_INFINITY) {
+ inMST.add(nextNode);
+ mstCost += minEdge;
+ } else {
+ break; // Grafo no conexo
+ }
+ }
+
+ return mstCost;
+ }
+
+ /**
+ * Construye una matriz de distancias a partir de las aristas
+ */
+ private Map> buildDistanceMatrix(List nodes, List edges) {
+ Map> matrix = new HashMap<>();
+
+ // Inicializar con infinito
+ for (String from : nodes) {
+ matrix.put(from, new HashMap<>());
+ for (String to : nodes) {
+ if (from.equals(to)) {
+ matrix.get(from).put(to, 0.0);
+ } else {
+ matrix.get(from).put(to, Double.POSITIVE_INFINITY);
+ }
+ }
+ }
+
+ // Llenar con las distancias de las aristas directas (grafo no dirigido)
+ for (Edge edge : edges) {
+ if (matrix.containsKey(edge.from) && matrix.containsKey(edge.to)) {
+ matrix.get(edge.from).put(edge.to, edge.weight);
+ matrix.get(edge.to).put(edge.from, edge.weight);
+ }
+ }
+
+ // ✅ FLOYD-WARSHALL: Calcular caminos más cortos indirectos
+ for (String k : nodes) {
+ for (String i : nodes) {
+ for (String j : nodes) {
+ double distIK = matrix.get(i).get(k);
+ double distKJ = matrix.get(k).get(j);
+ double distIJ = matrix.get(i).get(j);
+
+ // Si hay un camino más corto vía k, actualizarlo
+ if (distIK + distKJ < distIJ) {
+ matrix.get(i).put(j, distIK + distKJ);
+ }
+ }
+ }
+ }
+
+ return matrix;
+ }
+
+ /**
+ * Obtiene la distancia entre dos nodos
+ */
+ private double getDistance(String from, String to, Map> distMatrix) {
+ if (distMatrix.containsKey(from) && distMatrix.get(from).containsKey(to)) {
+ return distMatrix.get(from).get(to);
+ }
+ return Double.POSITIVE_INFINITY;
+ }
+
+ /**
+ * Verifica si el grafo es conexo (todos los nodos son alcanzables)
+ */
+ private boolean isConnected(List nodes, Map> distMatrix) {
+ if (nodes.isEmpty()) return true;
+ if (nodes.size() == 1) return true;
+
+ // Construir grafo de adyacencia BASADO en la distMatrix
+ Map> adj = new HashMap<>();
+ for (String node : nodes) {
+ adj.put(node, new HashSet<>());
+ }
+
+ // Si existe un camino (distancia finita), son vecinos
+ for (String from : nodes) {
+ for (String to : nodes) {
+ if (!from.equals(to)) {
+ double dist = getDistance(from, to, distMatrix);
+ if (dist != Double.POSITIVE_INFINITY) {
+ adj.get(from).add(to); // Hay camino → son vecinos
+ }
+ }
+ }
+ }
+
+ // BFS para verificar conectividad
+ Set visited = new HashSet<>();
+ Queue queue = new LinkedList<>();
+
+ String start = nodes.get(0);
+ queue.offer(start);
+ visited.add(start);
+
+ while (!queue.isEmpty()) {
+ String current = queue.poll();
+
+ // Explorar vecinos REALES del grafo de adyacencia
+ for (String neighbor : adj.get(current)) {
+ if (!visited.contains(neighbor)) {
+ visited.add(neighbor);
+ queue.offer(neighbor);
+ }
+ }
+ }
+
+ return visited.size() == nodes.size();
+ }
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/TransportService.java b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/TransportService.java
new file mode 100644
index 000000000..4b675f34b
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/java/com/programacion3/adoptme/service/TransportService.java
@@ -0,0 +1,87 @@
+package com.programacion3.adoptme.service;
+
+import com.programacion3.adoptme.domain.Dog;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Servicio para optimizar el transporte de perros usando Programación Dinámica (Knapsack).
+ *
+ * Problema: Dado un vehículo con capacidad limitada en kg, seleccionar el mejor conjunto
+ * de perros para transportar maximizando la prioridad total.
+ */
+@Service
+public class TransportService {
+
+ /**
+ * Resuelve el problema de la mochila 0/1 usando Programación Dinámica.
+ *
+ * @param dogs lista de perros disponibles para transporte
+ * @param capacityKg capacidad del vehículo en kilogramos
+ * @return resultado con perros seleccionados y valor total
+ */
+ public KnapsackResult optimizeTransport(List dogs, int capacityKg) {
+ if (dogs == null || dogs.isEmpty() || capacityKg <= 0) {
+ return new KnapsackResult(new ArrayList<>(), 0, 0);
+ }
+
+ int n = dogs.size();
+
+ // Tabla DP: dp[i][w] = máxima prioridad con primeros i perros y capacidad w
+ int[][] dp = new int[n + 1][capacityKg + 1];
+
+ // Llenar tabla usando programación dinámica
+ for (int i = 1; i <= n; i++) {
+ Dog dog = dogs.get(i - 1);
+ int weight = dog.getWeight();
+ int priority = dog.getPriority();
+
+ for (int w = 0; w <= capacityKg; w++) {
+ // Opción 1: No incluir este perro
+ dp[i][w] = dp[i - 1][w];
+
+ // Opción 2: Incluir este perro (si cabe)
+ if (weight <= w) {
+ int valueWithDog = dp[i - 1][w - weight] + priority;
+ dp[i][w] = Math.max(dp[i][w], valueWithDog);
+ }
+ }
+ }
+
+ // Reconstruir solución: qué perros fueron seleccionados
+ List selectedDogs = new ArrayList<>();
+ int w = capacityKg;
+ int totalWeight = 0;
+
+ for (int i = n; i > 0 && w > 0; i--) {
+ // Si el valor cambió, significa que incluimos este perro
+ if (dp[i][w] != dp[i - 1][w]) {
+ Dog dog = dogs.get(i - 1);
+ selectedDogs.add(dog);
+ w -= dog.getWeight();
+ totalWeight += dog.getWeight();
+ }
+ }
+
+ int totalPriority = dp[n][capacityKg];
+
+ return new KnapsackResult(selectedDogs, totalPriority, totalWeight);
+ }
+
+ /**
+ * Resultado del problema de la mochila
+ */
+ public static class KnapsackResult {
+ public final List selectedDogs;
+ public final int totalPriority;
+ public final int totalWeight;
+
+ public KnapsackResult(List selectedDogs, int totalPriority, int totalWeight) {
+ this.selectedDogs = selectedDogs;
+ this.totalPriority = totalPriority;
+ this.totalWeight = totalWeight;
+ }
+ }
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/resources/application.properties b/jdk_21_maven/cs/rest/adoptme/src/main/resources/application.properties
new file mode 100644
index 000000000..7e45430a4
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/resources/application.properties
@@ -0,0 +1,5 @@
+spring.application.name=AdoptM
+spring.data.neo4j.database=neo4j
+spring.neo4j.authentication.username=neo4j
+spring.neo4j.authentication.password=neo4j123
+spring.neo4j.uri=neo4j://127.0.0.1:7687
\ No newline at end of file
diff --git a/jdk_21_maven/cs/rest/adoptme/src/main/resources/application.yml b/jdk_21_maven/cs/rest/adoptme/src/main/resources/application.yml
new file mode 100644
index 000000000..1385eceac
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/main/resources/application.yml
@@ -0,0 +1,12 @@
+spring:
+ neo4j:
+ uri: bolt://localhost:7687
+ authentication:
+ username: neo4j
+ password: neo4j123
+ data:
+ neo4j:
+ database: neo4j
+
+server:
+ port: 8080
diff --git a/jdk_21_maven/cs/rest/adoptme/src/test/java/com/programacion3/adoptme/AdoptMApplicationTests.java b/jdk_21_maven/cs/rest/adoptme/src/test/java/com/programacion3/adoptme/AdoptMApplicationTests.java
new file mode 100644
index 000000000..f7a9b1a01
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/test/java/com/programacion3/adoptme/AdoptMApplicationTests.java
@@ -0,0 +1,13 @@
+package com.programacion3.adoptme;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class AdoptMApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/test/java/com/programacion3/adoptme/service/BacktrackingServiceTest.java b/jdk_21_maven/cs/rest/adoptme/src/test/java/com/programacion3/adoptme/service/BacktrackingServiceTest.java
new file mode 100644
index 000000000..03c21033d
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/test/java/com/programacion3/adoptme/service/BacktrackingServiceTest.java
@@ -0,0 +1,474 @@
+package com.programacion3.adoptme.service;
+
+import com.programacion3.adoptme.service.BacktrackingService.Dog;
+import com.programacion3.adoptme.service.BacktrackingService.Adopter;
+import com.programacion3.adoptme.service.BacktrackingService.Assignment;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.DisplayName;
+
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("BacktrackingService (Constraint Satisfaction) Unit Tests")
+class BacktrackingServiceTest {
+
+ private BacktrackingService backtrackingService;
+
+ @BeforeEach
+ void setUp() {
+ backtrackingService = new BacktrackingService();
+ }
+
+ private List createSimpleDogs() {
+ List dogs = new ArrayList<>();
+ dogs.add(new Dog("D1", true, false, 5, 5000.0)); // Good with kids, no garden, moderate energy
+ dogs.add(new Dog("D2", false, true, 7, 8000.0)); // Not good with kids, needs garden
+ dogs.add(new Dog("D3", true, true, 3, 10000.0)); // Good with kids, needs garden, low energy
+ return dogs;
+ }
+
+ private List createSimpleAdopters() {
+ List adopters = new ArrayList<>();
+ adopters.add(new Adopter("A1", "Alice", true, true, 2, 20000.0, 5)); // Has kids, has garden
+ adopters.add(new Adopter("A2", "Bob", false, false, 1, 6000.0, 7)); // No kids, no garden
+ return adopters;
+ }
+
+ // ==================== Basic Assignment Tests ====================
+
+ @Test
+ @DisplayName("Backtracking: Empty dogs returns empty assignment")
+ void testEmptyDogs() {
+ // Arrange
+ List dogs = new ArrayList<>();
+ List adopters = createSimpleAdopters();
+
+ // Act
+ Assignment result = backtrackingService.findBestAssignment(dogs, adopters);
+
+ // Assert
+ assertNotNull(result);
+ assertTrue(result.assignments.isEmpty() || allAssignmentsEmpty(result.assignments));
+ assertEquals(0.0, result.totalScore, 0.01);
+ }
+
+ @Test
+ @DisplayName("Backtracking: Empty adopters returns empty assignment")
+ void testEmptyAdopters() {
+ // Arrange
+ List dogs = createSimpleDogs();
+ List adopters = new ArrayList<>();
+
+ // Act
+ Assignment result = backtrackingService.findBestAssignment(dogs, adopters);
+
+ // Assert
+ assertNotNull(result);
+ assertTrue(result.assignments.isEmpty());
+ assertEquals(0.0, result.totalScore, 0.01);
+ }
+
+ @Test
+ @DisplayName("Backtracking: Single dog and single adopter")
+ void testSingleDogSingleAdopter() {
+ // Arrange
+ List dogs = Arrays.asList(new Dog("D1", true, false, 5, 5000.0));
+ List adopters = Arrays.asList(new Adopter("A1", "Alice", true, true, 2, 10000.0, 5));
+
+ // Act
+ Assignment result = backtrackingService.findBestAssignment(dogs, adopters);
+
+ // Assert
+ assertNotNull(result);
+ assertTrue(result.totalScore > 0, "Should find a valid assignment");
+
+ // Check that dog was assigned
+ boolean dogAssigned = false;
+ for (List dogList : result.assignments.values()) {
+ if (dogList.contains("D1")) {
+ dogAssigned = true;
+ break;
+ }
+ }
+ assertTrue(dogAssigned, "Dog should be assigned to adopter");
+ }
+
+ // ==================== Constraint Tests ====================
+
+ @Test
+ @DisplayName("Backtracking: Respects kids constraint")
+ void testKidsConstraint() {
+ // Arrange
+ List dogs = Arrays.asList(
+ new Dog("GoodWithKids", true, false, 5, 5000.0),
+ new Dog("NotGoodWithKids", false, false, 5, 5000.0)
+ );
+ List adopters = Arrays.asList(
+ new Adopter("HasKids", "Parent", true, true, 5, 50000.0, 5)
+ );
+
+ // Act
+ Assignment result = backtrackingService.findBestAssignment(dogs, adopters);
+
+ // Assert
+ List assignedToParent = result.assignments.get("HasKids");
+ if (assignedToParent != null) {
+ // Should only assign GoodWithKids
+ assertFalse(assignedToParent.contains("NotGoodWithKids"),
+ "Dog not good with kids should not be assigned to adopter with kids");
+ }
+ }
+
+ @Test
+ @DisplayName("Backtracking: Respects garden constraint")
+ void testGardenConstraint() {
+ // Arrange
+ List dogs = Arrays.asList(
+ new Dog("NeedsGarden", true, true, 5, 5000.0),
+ new Dog("NoGardenNeeded", true, false, 5, 5000.0)
+ );
+ List adopters = Arrays.asList(
+ new Adopter("NoGarden", "Bob", false, false, 5, 50000.0, 5)
+ );
+
+ // Act
+ Assignment result = backtrackingService.findBestAssignment(dogs, adopters);
+
+ // Assert
+ List assignedToBob = result.assignments.get("NoGarden");
+ if (assignedToBob != null) {
+ // Should not assign dog that needs garden
+ assertFalse(assignedToBob.contains("NeedsGarden"),
+ "Dog needing garden should not be assigned to adopter without garden");
+ }
+ }
+
+ @Test
+ @DisplayName("Backtracking: Respects maxDogs constraint")
+ void testMaxDogsConstraint() {
+ // Arrange
+ List dogs = new ArrayList<>();
+ for (int i = 1; i <= 10; i++) {
+ dogs.add(new Dog("D" + i, true, false, 5, 1000.0));
+ }
+
+ List adopters = Arrays.asList(
+ new Adopter("A1", "Alice", true, true, 2, 50000.0, 5) // Max 2 dogs
+ );
+
+ // Act
+ Assignment result = backtrackingService.findBestAssignment(dogs, adopters);
+
+ // Assert
+ List assignedToAlice = result.assignments.get("A1");
+ if (assignedToAlice != null) {
+ assertTrue(assignedToAlice.size() <= 2,
+ "Should not exceed maxDogs limit of 2");
+ }
+ }
+
+ @Test
+ @DisplayName("Backtracking: Respects budget constraint")
+ void testBudgetConstraint() {
+ // Arrange
+ List dogs = Arrays.asList(
+ new Dog("Cheap", true, false, 5, 5000.0),
+ new Dog("Expensive", true, false, 5, 20000.0)
+ );
+ List adopters = Arrays.asList(
+ new Adopter("LowBudget", "Bob", true, true, 5, 6000.0, 5)
+ );
+
+ // Act
+ Assignment result = backtrackingService.findBestAssignment(dogs, adopters);
+
+ // Assert
+ List assignedToBob = result.assignments.get("LowBudget");
+ if (assignedToBob != null) {
+ // Calculate total cost
+ double totalCost = 0.0;
+ for (String dogId : assignedToBob) {
+ for (Dog dog : dogs) {
+ if (dog.id.equals(dogId)) {
+ totalCost += dog.cost;
+ break;
+ }
+ }
+ }
+ assertTrue(totalCost <= 6000.0, "Total cost should not exceed budget");
+
+ // Should not include expensive dog
+ assertFalse(assignedToBob.contains("Expensive"),
+ "Expensive dog should not be assigned when budget insufficient");
+ }
+ }
+
+ // ==================== Score Maximization Tests ====================
+
+ @Test
+ @DisplayName("Backtracking: Maximizes total score")
+ void testMaximizesScore() {
+ // Arrange
+ List dogs = createSimpleDogs();
+ List adopters = createSimpleAdopters();
+
+ // Act
+ Assignment result = backtrackingService.findBestAssignment(dogs, adopters);
+
+ // Assert
+ assertTrue(result.totalScore >= 0, "Total score should be non-negative");
+ assertNotNull(result.assignments);
+ }
+
+ @Test
+ @DisplayName("Backtracking: Prefers better matches")
+ void testPrefersBetterMatches() {
+ // Arrange
+ // Dog with kids + garden compatibility
+ List dogs = Arrays.asList(
+ new Dog("Perfect", true, true, 5, 5000.0), // Perfect match
+ new Dog("OK", false, false, 8, 5000.0) // Poor match
+ );
+
+ // Adopter with kids and garden
+ List adopters = Arrays.asList(
+ new Adopter("A1", "Alice", true, true, 1, 10000.0, 5)
+ );
+
+ // Act
+ Assignment result = backtrackingService.findBestAssignment(dogs, adopters);
+
+ // Assert
+ List assignedToAlice = result.assignments.get("A1");
+ if (assignedToAlice != null && !assignedToAlice.isEmpty()) {
+ // Should prefer Perfect match over OK
+ assertTrue(assignedToAlice.contains("Perfect"),
+ "Should assign better matching dog");
+ }
+ }
+
+ // ==================== Multiple Adopters Tests ====================
+
+ @Test
+ @DisplayName("Backtracking: Distributes dogs among multiple adopters")
+ void testMultipleAdopters() {
+ // Arrange
+ List dogs = new ArrayList<>();
+ for (int i = 1; i <= 5; i++) {
+ dogs.add(new Dog("D" + i, true, false, 5, 3000.0));
+ }
+
+ List adopters = new ArrayList<>();
+ adopters.add(new Adopter("A1", "Alice", true, true, 2, 10000.0, 5));
+ adopters.add(new Adopter("A2", "Bob", true, true, 2, 10000.0, 5));
+
+ // Act
+ Assignment result = backtrackingService.findBestAssignment(dogs, adopters);
+
+ // Assert
+ int totalAssigned = 0;
+ for (List dogList : result.assignments.values()) {
+ totalAssigned += dogList.size();
+ }
+
+ assertTrue(totalAssigned > 0, "Should assign at least some dogs");
+ assertTrue(totalAssigned <= dogs.size(), "Should not assign more dogs than available");
+ }
+
+ @Test
+ @DisplayName("Backtracking: Each dog assigned to at most one adopter")
+ void testNoDuplicateAssignments() {
+ // Arrange
+ List dogs = createSimpleDogs();
+ List adopters = createSimpleAdopters();
+
+ // Act
+ Assignment result = backtrackingService.findBestAssignment(dogs, adopters);
+
+ // Assert
+ Set allAssignedDogs = new HashSet<>();
+ for (List dogList : result.assignments.values()) {
+ for (String dogId : dogList) {
+ assertFalse(allAssignedDogs.contains(dogId),
+ "Dog " + dogId + " assigned to multiple adopters");
+ allAssignedDogs.add(dogId);
+ }
+ }
+ }
+
+ // ==================== Energy Compatibility Tests ====================
+
+ @Test
+ @DisplayName("Backtracking: Energy compatibility affects score")
+ void testEnergyCompatibility() {
+ // Arrange
+ List dogs = Arrays.asList(
+ new Dog("MatchingEnergy", true, false, 5, 3000.0), // Energy 5
+ new Dog("MismatchEnergy", true, false, 10, 3000.0) // Energy 10
+ );
+
+ List adopters = Arrays.asList(
+ new Adopter("A1", "Alice", true, true, 1, 10000.0, 5) // Prefers energy 5
+ );
+
+ // Act
+ Assignment result = backtrackingService.findBestAssignment(dogs, adopters);
+
+ // Assert
+ List assignedToAlice = result.assignments.get("A1");
+ if (assignedToAlice != null && !assignedToAlice.isEmpty()) {
+ // Should prefer matching energy
+ assertTrue(assignedToAlice.contains("MatchingEnergy"),
+ "Should prefer dog with matching energy level");
+ }
+ }
+
+ // ==================== Performance and Timeout Tests ====================
+
+ @Test
+ @DisplayName("Backtracking: Completes within reasonable time")
+ void testCompletesInReasonableTime() {
+ // Arrange - 10 dogs and 3 adopters
+ List dogs = new ArrayList<>();
+ for (int i = 1; i <= 10; i++) {
+ dogs.add(new Dog("D" + i, true, false, 5, 5000.0));
+ }
+
+ List adopters = new ArrayList<>();
+ for (int i = 1; i <= 3; i++) {
+ adopters.add(new Adopter("A" + i, "Adopter" + i, true, true, 5, 50000.0, 5));
+ }
+
+ // Act
+ long start = System.currentTimeMillis();
+ Assignment result = backtrackingService.findBestAssignment(dogs, adopters);
+ long duration = System.currentTimeMillis() - start;
+
+ // Assert
+ assertNotNull(result);
+ assertTrue(duration < 10000, "Should complete within 10 seconds");
+ }
+
+ @Test
+ @DisplayName("Backtracking: Handles 20 dogs (service limit)")
+ void testHandles20Dogs() {
+ // Arrange - Exactly 20 dogs (the service's internal limit)
+ List dogs = new ArrayList<>();
+ for (int i = 1; i <= 20; i++) {
+ dogs.add(new Dog("D" + i, true, false, 5, 3000.0));
+ }
+
+ List adopters = Arrays.asList(
+ new Adopter("A1", "Alice", true, true, 10, 100000.0, 5)
+ );
+
+ // Act
+ Assignment result = backtrackingService.findBestAssignment(dogs, adopters);
+
+ // Assert
+ assertNotNull(result);
+ assertTrue(result.totalScore >= 0);
+ }
+
+ @Test
+ @DisplayName("Backtracking: Limits dogs to 20 when more provided")
+ void testLimitsDogs() {
+ // Arrange - 30 dogs (exceeds service's 20-dog limit)
+ List dogs = new ArrayList<>();
+ for (int i = 1; i <= 30; i++) {
+ dogs.add(new Dog("D" + i, true, false, 5, 3000.0));
+ }
+
+ List adopters = Arrays.asList(
+ new Adopter("A1", "Alice", true, true, 30, 100000.0, 5)
+ );
+
+ // Act - Should limit to first 20 dogs internally
+ Assignment result = backtrackingService.findBestAssignment(dogs, adopters);
+
+ // Assert
+ assertNotNull(result);
+ // Count assigned dogs
+ int totalAssigned = 0;
+ for (List dogList : result.assignments.values()) {
+ totalAssigned += dogList.size();
+ }
+ assertTrue(totalAssigned <= 20, "Service should limit to 20 dogs");
+ }
+
+ // ==================== Edge Cases ====================
+
+ @Test
+ @DisplayName("Backtracking: No valid assignments returns zero score")
+ void testNoValidAssignments() {
+ // Arrange - Dog needs garden, adopter has no garden
+ List dogs = Arrays.asList(
+ new Dog("NeedsGarden", true, true, 5, 5000.0)
+ );
+ List adopters = Arrays.asList(
+ new Adopter("NoGarden", "Bob", true, false, 1, 10000.0, 5)
+ );
+
+ // Act
+ Assignment result = backtrackingService.findBestAssignment(dogs, adopters);
+
+ // Assert
+ List assignedToBob = result.assignments.get("NoGarden");
+ if (assignedToBob != null) {
+ assertFalse(assignedToBob.contains("NeedsGarden"),
+ "Should not assign dog needing garden to adopter without garden");
+ }
+ }
+
+ @Test
+ @DisplayName("Backtracking: All dogs too expensive")
+ void testAllDogsTooExpensive() {
+ // Arrange
+ List dogs = Arrays.asList(
+ new Dog("Expensive1", true, false, 5, 50000.0),
+ new Dog("Expensive2", true, false, 5, 60000.0)
+ );
+ List adopters = Arrays.asList(
+ new Adopter("LowBudget", "Bob", true, true, 5, 1000.0, 5)
+ );
+
+ // Act
+ Assignment result = backtrackingService.findBestAssignment(dogs, adopters);
+
+ // Assert
+ List assignedToBob = result.assignments.get("LowBudget");
+ assertTrue(assignedToBob == null || assignedToBob.isEmpty(),
+ "Should not assign any dogs when all exceed budget");
+ }
+
+ @Test
+ @DisplayName("Backtracking: Adopter maxDogs is zero")
+ void testAdopterMaxDogsZero() {
+ // Arrange
+ List dogs = createSimpleDogs();
+ List adopters = Arrays.asList(
+ new Adopter("A1", "Alice", true, true, 0, 50000.0, 5) // Max 0 dogs
+ );
+
+ // Act
+ Assignment result = backtrackingService.findBestAssignment(dogs, adopters);
+
+ // Assert
+ List assignedToAlice = result.assignments.get("A1");
+ assertTrue(assignedToAlice == null || assignedToAlice.isEmpty(),
+ "Should not assign any dogs when maxDogs is 0");
+ }
+
+ // ==================== Helper Methods ====================
+
+ private boolean allAssignmentsEmpty(Map> assignments) {
+ for (List dogList : assignments.values()) {
+ if (!dogList.isEmpty()) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/test/java/com/programacion3/adoptme/service/GraphServiceTest.java b/jdk_21_maven/cs/rest/adoptme/src/test/java/com/programacion3/adoptme/service/GraphServiceTest.java
new file mode 100644
index 000000000..9fc8082f7
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/test/java/com/programacion3/adoptme/service/GraphServiceTest.java
@@ -0,0 +1,391 @@
+package com.programacion3.adoptme.service;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.DisplayName;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+@DisplayName("GraphService Unit Tests")
+class GraphServiceTest {
+
+ private GraphService graphService;
+
+ @Mock
+ private GraphLoader graphLoader;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ graphService = new GraphService(graphLoader);
+ }
+
+ private Map> createTestGraph() {
+ /*
+ * Test graph structure:
+ * A -> B -> C
+ * | |
+ * v v
+ * D -> E -> F
+ * |
+ * v
+ * G (isolated: H)
+ */
+ Map> graph = new HashMap<>();
+ graph.put("A", Arrays.asList("B", "D"));
+ graph.put("B", Arrays.asList("C"));
+ graph.put("C", Arrays.asList("F"));
+ graph.put("D", Arrays.asList("E", "G"));
+ graph.put("E", Arrays.asList("F"));
+ graph.put("F", new ArrayList<>());
+ graph.put("G", new ArrayList<>());
+ graph.put("H", new ArrayList<>()); // Isolated node
+ return graph;
+ }
+
+ private Map> createComplexGraph() {
+ /*
+ * Complex graph with multiple paths:
+ * A -> B -> D
+ * | | |
+ * v v v
+ * C -> E -> F
+ * |
+ * v
+ * G
+ */
+ Map> graph = new HashMap<>();
+ graph.put("A", Arrays.asList("B", "C"));
+ graph.put("B", Arrays.asList("D", "E"));
+ graph.put("C", Arrays.asList("E"));
+ graph.put("D", Arrays.asList("F"));
+ graph.put("E", Arrays.asList("F", "G"));
+ graph.put("F", new ArrayList<>());
+ graph.put("G", new ArrayList<>());
+ return graph;
+ }
+
+ // ==================== BFS Tests ====================
+
+ @Test
+ @DisplayName("BFS: Simple path A->B")
+ void testBfsSimplePath() {
+ // Arrange
+ when(graphLoader.loadAdjacency()).thenReturn(createTestGraph());
+
+ // Act
+ List path = graphService.bfsPath("A", "B");
+
+ // Assert
+ assertNotNull(path);
+ assertEquals(2, path.size());
+ assertEquals("A", path.get(0));
+ assertEquals("B", path.get(1));
+ }
+
+ @Test
+ @DisplayName("BFS: Path A->F (multiple hops)")
+ void testBfsMultipleHops() {
+ // Arrange
+ when(graphLoader.loadAdjacency()).thenReturn(createTestGraph());
+
+ // Act
+ List path = graphService.bfsPath("A", "F");
+
+ // Assert
+ assertNotNull(path);
+ assertFalse(path.isEmpty());
+ assertEquals("A", path.get(0));
+ assertEquals("F", path.get(path.size() - 1));
+
+ // BFS should find shortest path (3 hops: A->B->C->F or A->D->E->F)
+ assertEquals(4, path.size());
+ }
+
+ @Test
+ @DisplayName("BFS: No path to isolated node")
+ void testBfsNoPath() {
+ // Arrange
+ when(graphLoader.loadAdjacency()).thenReturn(createTestGraph());
+
+ // Act
+ List path = graphService.bfsPath("A", "H");
+
+ // Assert
+ assertNotNull(path);
+ assertTrue(path.isEmpty(), "Should return empty list when no path exists");
+ }
+
+ @Test
+ @DisplayName("BFS: Same source and destination")
+ void testBfsSameNode() {
+ // Arrange
+ when(graphLoader.loadAdjacency()).thenReturn(createTestGraph());
+
+ // Act
+ List path = graphService.bfsPath("A", "A");
+
+ // Assert
+ assertNotNull(path);
+ assertEquals(1, path.size());
+ assertEquals("A", path.get(0));
+ }
+
+ @Test
+ @DisplayName("BFS: Null source returns empty")
+ void testBfsNullSource() {
+ // Arrange
+ when(graphLoader.loadAdjacency()).thenReturn(createTestGraph());
+
+ // Act
+ List path = graphService.bfsPath(null, "B");
+
+ // Assert
+ assertNotNull(path);
+ assertTrue(path.isEmpty());
+ }
+
+ @Test
+ @DisplayName("BFS: Null destination returns empty")
+ void testBfsNullDestination() {
+ // Arrange
+ when(graphLoader.loadAdjacency()).thenReturn(createTestGraph());
+
+ // Act
+ List path = graphService.bfsPath("A", null);
+
+ // Assert
+ assertNotNull(path);
+ assertTrue(path.isEmpty());
+ }
+
+ @Test
+ @DisplayName("BFS: Blank source returns empty")
+ void testBfsBlankSource() {
+ // Arrange
+ when(graphLoader.loadAdjacency()).thenReturn(createTestGraph());
+
+ // Act
+ List path = graphService.bfsPath(" ", "B");
+
+ // Assert
+ assertNotNull(path);
+ assertTrue(path.isEmpty());
+ }
+
+ @Test
+ @DisplayName("BFS: Finds shortest path in graph with multiple routes")
+ void testBfsShortestPath() {
+ // Arrange
+ when(graphLoader.loadAdjacency()).thenReturn(createComplexGraph());
+
+ // Act
+ List path = graphService.bfsPath("A", "F");
+
+ // Assert
+ assertNotNull(path);
+ assertEquals("A", path.get(0));
+ assertEquals("F", path.get(path.size() - 1));
+
+ // Should find shortest: A->B->D->F (4 nodes) or A->B->E->F (4 nodes)
+ assertEquals(4, path.size());
+ }
+
+ @Test
+ @DisplayName("BFS: Path from intermediate node")
+ void testBfsFromIntermediateNode() {
+ // Arrange
+ when(graphLoader.loadAdjacency()).thenReturn(createTestGraph());
+
+ // Act
+ List path = graphService.bfsPath("D", "F");
+
+ // Assert
+ assertNotNull(path);
+ assertEquals("D", path.get(0));
+ assertEquals("F", path.get(path.size() - 1));
+ assertEquals(3, path.size()); // D->E->F
+ }
+
+ // ==================== DFS Tests ====================
+
+ @Test
+ @DisplayName("DFS: Simple path A->B")
+ void testDfsSimplePath() {
+ // Arrange
+ when(graphLoader.loadAdjacency()).thenReturn(createTestGraph());
+
+ // Act
+ List path = graphService.dfsPath("A", "B");
+
+ // Assert
+ assertNotNull(path);
+ assertFalse(path.isEmpty());
+ assertEquals("A", path.get(0));
+ assertEquals("B", path.get(path.size() - 1));
+ }
+
+ @Test
+ @DisplayName("DFS: Path A->F (deep search)")
+ void testDfsDeepPath() {
+ // Arrange
+ when(graphLoader.loadAdjacency()).thenReturn(createTestGraph());
+
+ // Act
+ List path = graphService.dfsPath("A", "F");
+
+ // Assert
+ assertNotNull(path);
+ assertFalse(path.isEmpty());
+ assertEquals("A", path.get(0));
+ assertEquals("F", path.get(path.size() - 1));
+
+ // DFS finds A path, not necessarily shortest
+ assertTrue(path.size() >= 4); // At least 4 nodes
+ }
+
+ @Test
+ @DisplayName("DFS: No path to isolated node")
+ void testDfsNoPath() {
+ // Arrange
+ when(graphLoader.loadAdjacency()).thenReturn(createTestGraph());
+
+ // Act
+ List path = graphService.dfsPath("A", "H");
+
+ // Assert
+ assertNotNull(path);
+ assertTrue(path.isEmpty(), "Should return empty list when no path exists");
+ }
+
+ @Test
+ @DisplayName("DFS: Same source and destination")
+ void testDfsSameNode() {
+ // Arrange
+ when(graphLoader.loadAdjacency()).thenReturn(createTestGraph());
+
+ // Act
+ List path = graphService.dfsPath("A", "A");
+
+ // Assert
+ assertNotNull(path);
+ assertEquals(1, path.size());
+ assertEquals("A", path.get(0));
+ }
+
+ @Test
+ @DisplayName("DFS: Null source returns empty")
+ void testDfsNullSource() {
+ // Arrange
+ when(graphLoader.loadAdjacency()).thenReturn(createTestGraph());
+
+ // Act
+ List path = graphService.dfsPath(null, "B");
+
+ // Assert
+ assertNotNull(path);
+ assertTrue(path.isEmpty());
+ }
+
+ @Test
+ @DisplayName("DFS: Path from intermediate node")
+ void testDfsFromIntermediateNode() {
+ // Arrange
+ when(graphLoader.loadAdjacency()).thenReturn(createTestGraph());
+
+ // Act
+ List path = graphService.dfsPath("D", "G");
+
+ // Assert
+ assertNotNull(path);
+ assertEquals("D", path.get(0));
+ assertEquals("G", path.get(path.size() - 1));
+ }
+
+ @Test
+ @DisplayName("DFS: Finds path in complex graph")
+ void testDfsComplexGraph() {
+ // Arrange
+ when(graphLoader.loadAdjacency()).thenReturn(createComplexGraph());
+
+ // Act
+ List path = graphService.dfsPath("A", "G");
+
+ // Assert
+ assertNotNull(path);
+ assertFalse(path.isEmpty());
+ assertEquals("A", path.get(0));
+ assertEquals("G", path.get(path.size() - 1));
+
+ // Verify path is valid (each consecutive pair has edge)
+ Map> graph = createComplexGraph();
+ for (int i = 0; i < path.size() - 1; i++) {
+ String from = path.get(i);
+ String to = path.get(i + 1);
+ assertTrue(graph.get(from).contains(to),
+ "Invalid path: no edge from " + from + " to " + to);
+ }
+ }
+
+ @Test
+ @DisplayName("DFS: Empty graph returns empty path")
+ void testDfsEmptyGraph() {
+ // Arrange
+ when(graphLoader.loadAdjacency()).thenReturn(new HashMap<>());
+
+ // Act
+ List path = graphService.dfsPath("A", "B");
+
+ // Assert
+ assertNotNull(path);
+ assertTrue(path.isEmpty());
+ }
+
+ @Test
+ @DisplayName("BFS: Empty graph returns empty path")
+ void testBfsEmptyGraph() {
+ // Arrange
+ when(graphLoader.loadAdjacency()).thenReturn(new HashMap<>());
+
+ // Act
+ List path = graphService.bfsPath("A", "B");
+
+ // Assert
+ assertNotNull(path);
+ assertTrue(path.isEmpty());
+ }
+
+ // ==================== Comparison Tests ====================
+
+ @Test
+ @DisplayName("BFS vs DFS: Both find path but may differ")
+ void testBfsVsDfs() {
+ // Arrange
+ when(graphLoader.loadAdjacency()).thenReturn(createComplexGraph());
+
+ // Act
+ List bfsPath = graphService.bfsPath("A", "F");
+ List dfsPath = graphService.dfsPath("A", "F");
+
+ // Assert
+ assertNotNull(bfsPath);
+ assertNotNull(dfsPath);
+ assertFalse(bfsPath.isEmpty());
+ assertFalse(dfsPath.isEmpty());
+
+ // Both should start at A and end at F
+ assertEquals("A", bfsPath.get(0));
+ assertEquals("F", bfsPath.get(bfsPath.size() - 1));
+ assertEquals("A", dfsPath.get(0));
+ assertEquals("F", dfsPath.get(dfsPath.size() - 1));
+
+ // BFS should find shortest or equal path
+ assertTrue(bfsPath.size() <= dfsPath.size(),
+ "BFS should find shortest path: BFS=" + bfsPath.size() + " vs DFS=" + dfsPath.size());
+ }
+}
diff --git a/jdk_21_maven/cs/rest/adoptme/src/test/java/com/programacion3/adoptme/service/MSTServiceTest.java b/jdk_21_maven/cs/rest/adoptme/src/test/java/com/programacion3/adoptme/service/MSTServiceTest.java
new file mode 100644
index 000000000..3f8face02
--- /dev/null
+++ b/jdk_21_maven/cs/rest/adoptme/src/test/java/com/programacion3/adoptme/service/MSTServiceTest.java
@@ -0,0 +1,465 @@
+package com.programacion3.adoptme.service;
+
+import com.programacion3.adoptme.service.MSTService.Edge;
+import com.programacion3.adoptme.service.MSTService.MSTResult;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.DisplayName;
+
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("MSTService (Kruskal & Prim) Unit Tests")
+class MSTServiceTest {
+
+ private MSTService mstService;
+
+ @BeforeEach
+ void setUp() {
+ mstService = new MSTService();
+ }
+
+ private List createSimpleGraph() {
+ /*
+ * Simple graph:
+ * A --5-- B
+ * | |
+ * 10 3
+ * | |
+ * C --2-- D
+ *
+ * MST should be: A-B (5), B-D (3), C-D (2) = 10
+ */
+ List edges = new ArrayList<>();
+ edges.add(new Edge("A", "B", 5.0, "NEAR"));
+ edges.add(new Edge("A", "C", 10.0, "NEAR"));
+ edges.add(new Edge("B", "D", 3.0, "NEAR"));
+ edges.add(new Edge("C", "D", 2.0, "NEAR"));
+ return edges;
+ }
+
+ private List createComplexGraph() {
+ /*
+ * Complex graph:
+ * A --1-- B --4-- E
+ * | | |
+ * 2 1 2
+ * | | |
+ * C --3-- D --5-- F
+ * | |
+ * 6-------8-------G
+ *
+ * MST edges: A-B(1), B-D(1), A-C(2), E-F(2), C-D(3), C-G(6) = 15
+ * (or similar with same total weight)
+ */
+ List edges = new ArrayList<>();
+ edges.add(new Edge("A", "B", 1.0, "NEAR"));
+ edges.add(new Edge("A", "C", 2.0, "NEAR"));
+ edges.add(new Edge("B", "D", 1.0, "NEAR"));
+ edges.add(new Edge("B", "E", 4.0, "NEAR"));
+ edges.add(new Edge("C", "D", 3.0, "NEAR"));
+ edges.add(new Edge("C", "G", 6.0, "NEAR"));
+ edges.add(new Edge("D", "F", 5.0, "NEAR"));
+ edges.add(new Edge("E", "F", 2.0, "NEAR"));
+ edges.add(new Edge("F", "G", 8.0, "NEAR"));
+ return edges;
+ }
+
+ private List createDisconnectedGraph() {
+ /*
+ * Disconnected graph:
+ * A --5-- B C --3-- D
+ *
+ * Can only create MST for connected components
+ */
+ List edges = new ArrayList<>();
+ edges.add(new Edge("A", "B", 5.0, "NEAR"));
+ edges.add(new Edge("C", "D", 3.0, "NEAR"));
+ return edges;
+ }
+
+ // ==================== Kruskal Tests ====================
+
+ @Test
+ @DisplayName("Kruskal: Simple graph MST")
+ void testKruskalSimpleGraph() {
+ // Arrange
+ List edges = createSimpleGraph();
+ Set nodes = Set.of("A", "B", "C", "D");
+
+ // Act
+ MSTResult result = mstService.compute(nodes, edges);
+
+ // Assert
+ assertEquals(10.0, result.totalWeight, 0.01, "Total MST weight should be 10");
+ assertEquals(3, result.edges.size(), "MST should have n-1 edges for n nodes");
+
+ // Verify no cycles (MST property)
+ assertTrue(isValidMST(result.edges, nodes), "Result should be a valid spanning tree");
+ }
+
+ @Test
+ @DisplayName("Kruskal: Complex graph MST")
+ void testKruskalComplexGraph() {
+ // Arrange
+ List edges = createComplexGraph();
+ Set nodes = Set.of("A", "B", "C", "D", "E", "F", "G");
+
+ // Act
+ MSTResult result = mstService.compute(nodes, edges);
+
+ // Assert
+ assertEquals(15.0, result.totalWeight, 0.01, "Total MST weight should be 15");
+ assertEquals(6, result.edges.size(), "MST should have 6 edges for 7 nodes");
+ assertTrue(isValidMST(result.edges, nodes), "Result should be a valid spanning tree");
+ }
+
+ @Test
+ @DisplayName("Kruskal: Empty graph")
+ void testKruskalEmptyGraph() {
+ // Arrange
+ List edges = new ArrayList<>();
+ Set