diff --git a/.gitignore b/.gitignore index fe24357..666447a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ Thumbs.db # Logs *.log +/.classpath +/.project +/.settings/ diff --git a/src/main/java/brendanddev/client/MainClient.java b/src/main/java/brendanddev/client/MainClient.java index 50462db..fac5e28 100644 --- a/src/main/java/brendanddev/client/MainClient.java +++ b/src/main/java/brendanddev/client/MainClient.java @@ -18,7 +18,7 @@ public static void main(String[] args) { } String host = props.getProperty("server.host", "localhost"); - int port = Integer.parseInt(props.getProperty("server.port", "8080")); + int port = Integer.parseInt(props.getProperty("server.port", "9080")); // Create instance of HttpClient with the loaded properties HttpClient client = new HttpClient(host, port); diff --git a/src/main/java/brendanddev/routing/Route.java b/src/main/java/brendanddev/routing/Route.java new file mode 100644 index 0000000..41af8cd --- /dev/null +++ b/src/main/java/brendanddev/routing/Route.java @@ -0,0 +1,57 @@ +package brendanddev.routing; + +import brendanddev.server.HttpHandler; + + +/** + * + * @author albanna +*/ + + +/** + * Represents a single route in the HTTP server. + */ +public class Route { + + private final String method; + private final String path; + private final HttpHandler handler; + private final String description; + + public Route(String method, String path, HttpHandler handler) { + this(method, path, handler, ""); + } + + public Route(String method, String path, HttpHandler handler, String description) { + this.method = method.toUpperCase(); + this.path = path; + this.handler = handler; + this.description = description; + } + + public String getMethod() { + return method; + } + + public String getPath() { + return path; + } + + public HttpHandler getHandler() { + return handler; + } + + public String getDescription() { + return description; + } + + public String getRouteKey() { + return method + " " + path; + } + + @Override + public String toString() { + return String.format("%s %s - %s", method, path, description); + } +} \ No newline at end of file diff --git a/src/main/java/brendanddev/routing/RouteRegistry.java b/src/main/java/brendanddev/routing/RouteRegistry.java new file mode 100644 index 0000000..0159298 --- /dev/null +++ b/src/main/java/brendanddev/routing/RouteRegistry.java @@ -0,0 +1,133 @@ +package brendanddev.routing; + +import brendanddev.server.HttpHandler; +import brendanddev.server.HttpResponse; +import brendanddev.tools.Logger; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * + * @author albanna +*/ + +/** + * Centralized route registry for managing HTTP routes. Provides a clean way to + * register, organize, and document API endpoints. + */ +public class RouteRegistry { + + private final Map routes = new HashMap<>(); + private final Logger logger = Logger.getInstance(); + + /** + * Registers a new route with the registry. + */ + public RouteRegistry register(String method, String path, HttpHandler handler) { + return register(method, path, handler, ""); + } + + /** + * Registers a new route with description. + */ + public RouteRegistry register(String method, String path, HttpHandler handler, String description) { + Route route = new Route(method, path, handler, description); + routes.put(route.getRouteKey(), route); + logger.info("Registered route: " + route); + return this; // For method chaining + } + + /** + * Registers a GET route. + */ + public RouteRegistry get(String path, HttpHandler handler) { + return register("GET", path, handler); + } + + /** + * Registers a GET route with description. + */ + public RouteRegistry get(String path, HttpHandler handler, String description) { + return register("GET", path, handler, description); + } + + /** + * Registers a POST route. + */ + public RouteRegistry post(String path, HttpHandler handler) { + return register("POST", path, handler); + } + + /** + * Registers a POST route with description. + */ + public RouteRegistry post(String path, HttpHandler handler, String description) { + return register("POST", path, handler, description); + } + + /** + * Registers a PUT route. + */ + public RouteRegistry put(String path, HttpHandler handler) { + return register("PUT", path, handler); + } + + /** + * Registers a DELETE route. + */ + public RouteRegistry delete(String path, HttpHandler handler) { + return register("DELETE", path, handler); + } + + /** + * Gets a route handler for the given method and path. + */ + public HttpHandler getHandler(String method, String path) { + Route route = routes.get(method.toUpperCase() + " " + path); + return route != null ? route.getHandler() : null; + } + + /** + * Gets all registered routes. + */ + public List getAllRoutes() { + return new ArrayList<>(routes.values()); + } + + /** + * Creates an API documentation handler that lists all routes. + */ + public HttpHandler createApiDocsHandler() { + return (request, body) -> { + StringBuilder html = new StringBuilder(); + html.append("API Documentation"); + html.append("

API Endpoints

"); + html.append(""); + html.append(""); + + getAllRoutes().forEach(route -> { + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + }); + + html.append("
MethodPathDescription
").append(route.getMethod()).append("").append(route.getPath()).append("").append(route.getDescription()).append("
"); + return new HttpResponse(html.toString(), 200, "OK"); + }; + } + + /** + * Applies all registered routes to an HttpServer. + */ + public void applyTo(brendanddev.server.HttpServer server) { + routes.values().forEach(route -> { + server.addRoute(route.getMethod(), route.getPath(), route.getHandler()); + }); + logger.info("Applied " + routes.size() + " routes to server"); + } +} diff --git a/src/main/java/brendanddev/server/EnhancedMainServer.java b/src/main/java/brendanddev/server/EnhancedMainServer.java new file mode 100644 index 0000000..3115016 --- /dev/null +++ b/src/main/java/brendanddev/server/EnhancedMainServer.java @@ -0,0 +1,114 @@ +package brendanddev.server; + +import brendanddev.routing.RouteRegistry; +import brendanddev.tools.ConfigManager; +import brendanddev.tools.Logger; +import brendanddev.tools.RequestMetrics; + +/** + * @author albanna + */ + +/** + * Enhanced main server class demonstrating the new tools and routing features. + * This shows how to use the new packages without modifying existing code. + */ +public class EnhancedMainServer { + + public static void main(String[] args) { + // Initialize logging + Logger logger = Logger.getInstance(); + logger.setLogLevel(Logger.LogLevel.DEBUG); + logger.setFileLogging(true, "enhanced-server.log"); + logger.info("Starting Enhanced HTTP Server..."); + + // Load configuration + ConfigManager config = ConfigManager.getInstance(); + int port = config.getIntProperty("server.properties", "server.port", 9090); + boolean enableMetrics = config.getBooleanProperty("server.properties", "metrics.enabled", true); + + // Create server + HttpServer server = new HttpServer(port); + + // Initialize metrics if enabled + RequestMetrics metrics = RequestMetrics.getInstance(); + + // Create centralized route registry + RouteRegistry routes = new RouteRegistry(); + + // Register existing routes with descriptions + routes.get("/", (request, body) -> { + long startTime = System.currentTimeMillis(); + HttpResponse response = new HttpResponse(ServerUtils.loadTemplate("index.html"), 200, "OK"); + if (enableMetrics) { + metrics.recordRequest("GET", "/", System.currentTimeMillis() - startTime); + } + return response; + }, "Home page"); + + routes.get("/about", (request, body) -> { + long startTime = System.currentTimeMillis(); + HttpResponse response = new HttpResponse(ServerUtils.loadTemplate("about.html"), 200, "OK"); + if (enableMetrics) { + metrics.recordRequest("GET", "/about", System.currentTimeMillis() - startTime); + } + return response; + }, "About page"); + + routes.post("/submit", (request, body) -> { + long startTime = System.currentTimeMillis(); + logger.info("Received POST data: " + body); + HttpResponse response = new HttpResponse( + ServerUtils.loadTemplate("submit.html").replace("{{message}}", body), 200, "OK"); + if (enableMetrics) { + metrics.recordRequest("POST", "/submit", System.currentTimeMillis() - startTime); + } + return response; + }, "Submit form data"); + + // Register new utility routes + routes.get("/api/docs", routes.createApiDocsHandler(), "API Documentation"); + + routes.get("/api/metrics", (request, body) -> { + if (!enableMetrics) { + return new HttpResponse("Metrics disabled", 404, "Not Found"); + } + + StringBuilder html = new StringBuilder(); + html.append("Server Metrics"); + html.append("

Server Metrics

"); + html.append("

Total Requests: ").append(metrics.getTotalRequests()).append("

"); + html.append("

Endpoint Statistics:

"); + html.append(""); + html.append(""); + + routes.getAllRoutes().forEach(route -> { + long count = metrics.getRequestCount(route.getMethod(), route.getPath()); + double avgTime = metrics.getAverageResponseTime(route.getMethod(), route.getPath()); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + }); + + html.append("
EndpointCountAvg Response Time (ms)
").append(route.getRouteKey()).append("").append(count).append("").append(String.format("%.2f", avgTime)).append("
"); + return new HttpResponse(html.toString(), 200, "OK"); + }, "Server metrics and statistics"); + + routes.get("/api/health", (request, body) -> { + return new HttpResponse("{\"status\":\"OK\",\"timestamp\":" + System.currentTimeMillis() + "}", 200, "OK"); + }, "Health check endpoint"); + + // Apply all routes to the server + routes.applyTo(server); + + // Print registered routes + logger.info("Registered " + routes.getAllRoutes().size() + " routes:"); + routes.getAllRoutes().forEach(route -> logger.info(" " + route)); + + // Start server + logger.info("Server starting on port " + port); + server.start(); + } +} \ No newline at end of file diff --git a/src/main/java/brendanddev/tools/ConfigManager.java b/src/main/java/brendanddev/tools/ConfigManager.java new file mode 100644 index 0000000..b513753 --- /dev/null +++ b/src/main/java/brendanddev/tools/ConfigManager.java @@ -0,0 +1,86 @@ +package brendanddev.tools; + +import java.io.InputStream; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; + +/** + * + * @author albanna +*/ + + +/** + * Singleton Configuration Manager for handling application properties. Supports + * multiple property files and environment-specific configurations. + */ +public class ConfigManager { + + private static ConfigManager instance; + private final ConcurrentHashMap configCache = new ConcurrentHashMap<>(); + private final Logger logger = Logger.getInstance(); + + private ConfigManager() { + } + + /** + * Gets the singleton instance of the ConfigManager. + */ + public static synchronized ConfigManager getInstance() { + if (instance == null) { + instance = new ConfigManager(); + } + return instance; + } + + /** + * Loads a properties file from the classpath. + */ + public Properties loadConfig(String configFileName) { + return configCache.computeIfAbsent(configFileName, this::loadPropertiesFile); + } + + private Properties loadPropertiesFile(String fileName) { + Properties props = new Properties(); + try (InputStream in = getClass().getResourceAsStream("/" + fileName)) { + if (in != null) { + props.load(in); + logger.info("Loaded configuration file: " + fileName); + } else { + logger.warn("Configuration file not found: " + fileName); + } + } catch (Exception e) { + logger.error("Failed to load configuration file: " + fileName, e); + } + return props; + } + + /** + * Gets a property value with a default fallback. + */ + public String getProperty(String configFile, String key, String defaultValue) { + Properties props = loadConfig(configFile); + return props.getProperty(key, defaultValue); + } + + /** + * Gets an integer property with a default fallback. + */ + public int getIntProperty(String configFile, String key, int defaultValue) { + String value = getProperty(configFile, key, String.valueOf(defaultValue)); + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + logger.warn("Invalid integer property: " + key + "=" + value + ", using default: " + defaultValue); + return defaultValue; + } + } + + /** + * Gets a boolean property with a default fallback. + */ + public boolean getBooleanProperty(String configFile, String key, boolean defaultValue) { + String value = getProperty(configFile, key, String.valueOf(defaultValue)); + return Boolean.parseBoolean(value); + } +} \ No newline at end of file diff --git a/src/main/java/brendanddev/tools/Logger.java b/src/main/java/brendanddev/tools/Logger.java new file mode 100644 index 0000000..34ff460 --- /dev/null +++ b/src/main/java/brendanddev/tools/Logger.java @@ -0,0 +1,112 @@ +package brendanddev.tools; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; + +/** + * + * @author albanna +*/ + +/** + * Singleton Logger service for centralized logging across the HTTP server + * project. Provides different log levels and can write to both console and + * file. + */ +public class Logger { + + public enum LogLevel { + DEBUG, INFO, WARN, ERROR + } + + private static Logger instance; + private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private LogLevel minLogLevel = LogLevel.INFO; + private boolean enableFileLogging = false; + private String logFilePath = "server.log"; + + private Logger() { + } + + /** + * Gets the singleton instance of the Logger. + */ + public static synchronized Logger getInstance() { + if (instance == null) { + instance = new Logger(); + } + return instance; + } + + /** + * Sets the minimum log level to display. + */ + public void setLogLevel(LogLevel level) { + this.minLogLevel = level; + } + + /** + * Enables or disables file logging. + */ + public void setFileLogging(boolean enable, String filePath) { + this.enableFileLogging = enable; + if (filePath != null) { + this.logFilePath = filePath; + } + } + + public void debug(String message) { + log(LogLevel.DEBUG, message); + } + + public void info(String message) { + log(LogLevel.INFO, message); + } + + public void warn(String message) { + log(LogLevel.WARN, message); + } + + public void error(String message) { + log(LogLevel.ERROR, message); + } + + public void error(String message, Throwable throwable) { + log(LogLevel.ERROR, message + " - " + throwable.getMessage()); + if (shouldLog(LogLevel.ERROR)) { + throwable.printStackTrace(); + } + } + + private void log(LogLevel level, String message) { + if (!shouldLog(level)) { + return; + } + + String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT); + String logMessage = String.format("[%s] %s: %s", timestamp, level.name(), message); + + // Console logging + System.out.println(logMessage); + + // File logging + if (enableFileLogging) { + writeToFile(logMessage); + } + } + + private boolean shouldLog(LogLevel level) { + return level.ordinal() >= minLogLevel.ordinal(); + } + + private void writeToFile(String message) { + try (PrintWriter writer = new PrintWriter(new FileWriter(logFilePath, true))) { + writer.println(message); + } catch (IOException e) { + System.err.println("Failed to write to log file: " + e.getMessage()); + } + } +} diff --git a/src/main/java/brendanddev/tools/RequestMetrics.java b/src/main/java/brendanddev/tools/RequestMetrics.java new file mode 100644 index 0000000..f7c10a9 --- /dev/null +++ b/src/main/java/brendanddev/tools/RequestMetrics.java @@ -0,0 +1,90 @@ +package brendanddev.tools; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +/** + * + * @author albanna +*/ + +/** + * Singleton service for tracking HTTP request metrics and statistics. + */ +public class RequestMetrics { + + private static RequestMetrics instance; + private final ConcurrentHashMap requestCounts = new ConcurrentHashMap<>(); + private final ConcurrentHashMap responseTimes = new ConcurrentHashMap<>(); + private final AtomicLong totalRequests = new AtomicLong(0); + private final Logger logger = Logger.getInstance(); + + private RequestMetrics() { + } + + /** + * Gets the singleton instance of RequestMetrics. + */ + public static synchronized RequestMetrics getInstance() { + if (instance == null) { + instance = new RequestMetrics(); + } + return instance; + } + + /** + * Records a request for the given endpoint. + */ + public void recordRequest(String method, String path, long responseTimeMs) { + String endpoint = method + " " + path; + requestCounts.computeIfAbsent(endpoint, k -> new AtomicLong(0)).incrementAndGet(); + responseTimes.computeIfAbsent(endpoint, k -> new AtomicLong(0)).addAndGet(responseTimeMs); + totalRequests.incrementAndGet(); + + logger.debug("Request recorded: " + endpoint + " (" + responseTimeMs + "ms)"); + } + + /** + * Gets the total number of requests processed. + */ + public long getTotalRequests() { + return totalRequests.get(); + } + + /** + * Gets the request count for a specific endpoint. + */ + public long getRequestCount(String method, String path) { + String endpoint = method + " " + path; + AtomicLong count = requestCounts.get(endpoint); + return count != null ? count.get() : 0; + } + + /** + * Gets the average response time for a specific endpoint. + */ + public double getAverageResponseTime(String method, String path) { + String endpoint = method + " " + path; + AtomicLong count = requestCounts.get(endpoint); + AtomicLong totalTime = responseTimes.get(endpoint); + + if (count == null || totalTime == null || count.get() == 0) { + return 0.0; + } + + return (double) totalTime.get() / count.get(); + } + + /** + * Prints current metrics to the logger. + */ + public void printMetrics() { + logger.info("=== Request Metrics ==="); + logger.info("Total Requests: " + totalRequests.get()); + + requestCounts.forEach((endpoint, count) -> { + double avgTime = getAverageResponseTime(endpoint.split(" ")[0], endpoint.split(" ", 2)[1]); + logger.info(String.format("%s: %d requests, %.2fms avg", endpoint, count.get(), avgTime)); + }); + } +} \ No newline at end of file diff --git a/src/main/resources/server.properties b/src/main/resources/server.properties new file mode 100644 index 0000000..40f9ead --- /dev/null +++ b/src/main/resources/server.properties @@ -0,0 +1,16 @@ +# Enhanced HTTP Server Configuration +server.port=9090 +server.host=localhost + +# Logging Configuration +logging.level=INFO +logging.file.enabled=true +logging.file.path=server.log + +# Metrics Configuration +metrics.enabled=true +metrics.print.interval=300000 + +# Performance Settings +server.thread.pool.size=10 +server.connection.timeout=30000 \ No newline at end of file