167 lines
7.8 KiB
Java
167 lines
7.8 KiB
Java
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
|
|
}
|
|
}
|