package com.petshop.backend; import com.petshop.backend.config.FlywayContextInitializer; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.server.PortInUseException; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.event.ContextClosedEvent; import java.net.BindException; import java.time.Duration; import java.util.Locale; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; public class DevStackApplication { private static final Duration WATCH_INTERVAL = Duration.ofSeconds(2); public static void main(String[] args) { DockerComposeSupport docker = new DockerComposeSupport(); ConfigurableApplicationContext context = null; CountDownLatch shutdownLatch = new CountDownLatch(1); ScheduledExecutorService scheduler = null; AtomicBoolean shuttingDown = new AtomicBoolean(false); try { validateJavaVersion(); docker.ensureDockerAvailable(); docker.startDatabase(); context = new SpringApplicationBuilder(BackendApplication.class) .initializers(new FlywayContextInitializer()) .run(args); context.addApplicationListener(event -> { if (event instanceof ContextClosedEvent) { shutdownLatch.countDown(); } }); scheduler = startDatabaseWatch(docker, context, shuttingDown); shutdownLatch.await(); } catch (RuntimeException ex) { throw new IllegalStateException(describeStartupFailure(ex), ex); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); throw new IllegalStateException("Dev stack interrupted", ex); } finally { if (scheduler != null) { scheduler.shutdownNow(); } if (context != null && context.isActive()) { context.close(); } if (!shuttingDown.get()) { docker.stopDatabase(); } } } private static void validateJavaVersion() { int feature = Runtime.version().feature(); if (feature < 25) { throw new IllegalStateException("JDK 25 or newer is required. IntelliJ is currently using Java " + Runtime.version() + "."); } } private static ScheduledExecutorService startDatabaseWatch(DockerComposeSupport docker, ConfigurableApplicationContext context, AtomicBoolean shuttingDown) { ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleWithFixedDelay(() -> { if (shuttingDown.get()) { return; } if (!docker.isDatabaseRunning()) { shuttingDown.set(true); if (context.isActive()) { context.close(); } } }, WATCH_INTERVAL.toSeconds(), WATCH_INTERVAL.toSeconds(), TimeUnit.SECONDS); return scheduler; } private static String describeStartupFailure(RuntimeException ex) { Throwable cause = deepestCause(ex); String message = cause.getMessage(); String lowerMessage = ""; if (message != null) { lowerMessage = message.toLowerCase(Locale.ROOT); } switch (classifyStartupFailure(cause, lowerMessage)) { case SERVER_PORT_IN_USE: return "Backend startup failed because port 8080 is already in use. Stop the other process or set SERVER_PORT to a different value."; case DOCKER_NOT_RUNNING: return "Backend startup failed because Docker is not available. Start Docker Desktop and try again."; case DOCKER_NOT_INSTALLED: return "Backend startup failed because Docker is not installed or not on PATH. Install Docker Desktop and reopen IntelliJ."; case DATABASE_PORT_IN_USE: return "Backend startup failed because port 3306 is already in use. Stop the conflicting MySQL service or container, then run Reset Database and try again."; case STALE_SCHEMA: return "Backend startup failed because the database schema is stale or incomplete. Run Reset Database and then start Pet Shop Application again."; case FLYWAY_MIGRATION_FAILED: return "Backend startup failed during Flyway migration. Run Reset Database and check the database logs if the problem persists."; case DATASOURCE_CONFIG_MISSING: return "Backend startup failed because datasource configuration was not loaded. Reload the Maven project and rerun Pet Shop Application."; case DATABASE_UNREACHABLE: return "Backend startup failed because it could not connect to MySQL. Make sure Docker Desktop is running, the database container is healthy, and port 3306 is reachable."; case UNKNOWN: if (message == null || message.isBlank()) { return "Backend startup failed: " + ex.getClass().getSimpleName(); } return "Backend startup failed: " + message; default: throw new IllegalStateException("Unhandled startup failure classification"); } } private static StartupFailure classifyStartupFailure(Throwable cause, String lowerMessage) { if (cause instanceof PortInUseException || cause instanceof BindException || lowerMessage.contains("port 8080") && lowerMessage.contains("already in use")) { return StartupFailure.SERVER_PORT_IN_USE; } if (lowerMessage.contains("docker desktop") || lowerMessage.contains("docker daemon") || lowerMessage.contains("docker engine") || lowerMessage.contains("cannot connect to the docker daemon")) { return StartupFailure.DOCKER_NOT_RUNNING; } if (lowerMessage.contains("docker executable") || lowerMessage.contains("no such file or directory") && lowerMessage.contains("docker")) { return StartupFailure.DOCKER_NOT_INSTALLED; } if (lowerMessage.contains("port is already allocated") || lowerMessage.contains("address already in use") && lowerMessage.contains("3306")) { return StartupFailure.DATABASE_PORT_IN_USE; } if (lowerMessage.contains("schema validation: missing table")) { return StartupFailure.STALE_SCHEMA; } if (lowerMessage.contains("flyway") || lowerMessage.contains("migration")) { return StartupFailure.FLYWAY_MIGRATION_FAILED; } if (lowerMessage.contains("datasource properties are required")) { return StartupFailure.DATASOURCE_CONFIG_MISSING; } if (lowerMessage.contains("access denied for user") || lowerMessage.contains("communications link failure") || lowerMessage.contains("connection refused") || lowerMessage.contains("unable to determine dialect without jdbc metadata")) { return StartupFailure.DATABASE_UNREACHABLE; } return StartupFailure.UNKNOWN; } private static Throwable deepestCause(Throwable throwable) { Throwable current = throwable; while (current.getCause() != null && current.getCause() != current) { current = current.getCause(); } return current; } private enum StartupFailure { SERVER_PORT_IN_USE, DOCKER_NOT_RUNNING, DOCKER_NOT_INSTALLED, DATABASE_PORT_IN_USE, STALE_SCHEMA, FLYWAY_MIGRATION_FAILED, DATASOURCE_CONFIG_MISSING, DATABASE_UNREACHABLE, UNKNOWN } }