| set -e | |
| BASE_DIR="src/main/java/ir/ac/ut/ece/ie/server" | |
| # Create directories | |
| mkdir -p "$BASE_DIR/staticcontent" | |
| mkdir -p "$BASE_DIR/dynamic/routehandlers" | |
| mkdir -p "$BASE_DIR/dynamic/pages" # اگر قبلاً نبوده باشد | |
| ################################# | |
| # Static server files | |
| ################################# | |
| cat > "$BASE_DIR/staticcontent/StaticContentServer.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.staticcontent; | |
| import java.io.File; | |
| import java.io.IOException; | |
| import java.net.ServerSocket; | |
| import java.net.Socket; | |
| import java.nio.file.Files; | |
| import java.nio.file.Path; | |
| import java.nio.file.Paths; | |
| import java.util.concurrent.ExecutorService; | |
| import java.util.concurrent.Executors; | |
| /** | |
| * Static Content Server with reverse proxy to dynamic server | |
| */ | |
| public class StaticContentServer { | |
| private ServerSocket serverSocket; | |
| static String BASE_PATH; | |
| private final ExecutorService threadPool; | |
| private boolean running = false; | |
| public static final String DYNAMIC_SERVER_HOST = "localhost"; | |
| public static final int DYNAMIC_SERVER_PORT = 9092; | |
| public StaticContentServer() { | |
| BASE_PATH = findBasePath(); | |
| this.threadPool = Executors.newFixedThreadPool(20); | |
| System.out.println("Static Server Base Path: " + BASE_PATH); | |
| } | |
| private String findBasePath() { | |
| String[] possiblePaths = { | |
| "./src/main/resources/Templates/", | |
| "src/main/resources/Templates/", | |
| "./Templates/", | |
| "Templates/", | |
| System.getProperty("user.dir") + "/src/main/resources/Templates/" | |
| }; | |
| for (String path : possiblePaths) { | |
| Path p = Paths.get(path).toAbsolutePath().normalize(); | |
| if (Files.exists(p) && Files.isDirectory(p)) { | |
| return p.toString() + File.separator; | |
| } | |
| } | |
| return Paths.get(".").toAbsolutePath().normalize().toString() + | |
| File.separator + "src" + File.separator + "main" + | |
| File.separator + "resources" + File.separator + "Templates" + File.separator; | |
| } | |
| public void start() throws IOException { | |
| serverSocket = new ServerSocket(9091); | |
| running = true; | |
| System.out.println("========================================"); | |
| System.out.println("Static Content Server v2.0 Started"); | |
| System.out.println("Port: 9091"); | |
| System.out.println("========================================"); | |
| StaticRequestHandler requestHandler = new StaticRequestHandler(); | |
| while (running) { | |
| Socket socket = serverSocket.accept(); | |
| socket.setSoTimeout(30000); | |
| threadPool.submit(() -> requestHandler.handleClient(socket)); | |
| } | |
| } | |
| public void stop() throws IOException { | |
| running = false; | |
| if (serverSocket != null && !serverSocket.isClosed()) { | |
| serverSocket.close(); | |
| } | |
| threadPool.shutdown(); | |
| } | |
| public static void main(String[] args) { | |
| try { | |
| StaticContentServer server = new StaticContentServer(); | |
| server.start(); | |
| } catch (IOException e) { | |
| e.printStackTrace(); | |
| } | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/staticcontent/StaticRequestHandler.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.staticcontent; | |
| import java.io.BufferedReader; | |
| import java.io.IOException; | |
| import java.io.InputStreamReader; | |
| import java.net.Socket; | |
| import java.nio.charset.StandardCharsets; | |
| import java.util.Map; | |
| import java.util.StringTokenizer; | |
| public class StaticRequestHandler { | |
| private final RequestParser requestParser; | |
| private final StaticFileResolver fileResolver; | |
| private final StaticResponseWriter responseWriter; | |
| private final ProxyForwarder proxyForwarder; | |
| public StaticRequestHandler() { | |
| this.requestParser = new RequestParser(); | |
| this.fileResolver = new StaticFileResolver(); | |
| this.responseWriter = new StaticResponseWriter(); | |
| this.proxyForwarder = new ProxyForwarder(); | |
| } | |
| public void handleClient(Socket socket) { | |
| try { | |
| handleRequest(socket); | |
| } catch (Exception e) { | |
| System.err.println("Error: " + e.getMessage()); | |
| } finally { | |
| try { socket.close(); } catch (IOException ignored) {} | |
| } | |
| } | |
| private void handleRequest(Socket socket) throws IOException { | |
| BufferedReader reader = new BufferedReader( | |
| new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8) | |
| ); | |
| String requestLine = reader.readLine(); | |
| if (requestLine == null) return; | |
| System.out.println("Request: " + requestLine); | |
| StringTokenizer tokenizer = new StringTokenizer(requestLine, " "); | |
| if (!tokenizer.hasMoreTokens()) return; | |
| String method = tokenizer.nextToken(); | |
| String path = tokenizer.nextToken(); | |
| Map<String, String> headers = requestParser.readHeaders(reader); | |
| String requestBody = requestParser.readRequestBody(reader, method, headers); | |
| if (fileResolver.isDynamicRoute(path)) { | |
| proxyForwarder.forwardToDynamicServer(socket, method, path, requestBody, headers); | |
| } else { | |
| responseWriter.serveStaticFile(socket, path); | |
| } | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/staticcontent/RequestParser.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.staticcontent; | |
| import java.io.BufferedReader; | |
| import java.io.IOException; | |
| import java.util.HashMap; | |
| import java.util.Map; | |
| public class RequestParser { | |
| public Map<String, String> readHeaders(BufferedReader reader) throws IOException { | |
| Map<String, String> headers = new HashMap<>(); | |
| String line; | |
| while ((line = reader.readLine()) != null && !line.isEmpty()) { | |
| String[] parts = line.split(": ", 2); | |
| if (parts.length == 2) { | |
| headers.put(parts[0], parts[1]); | |
| } | |
| } | |
| return headers; | |
| } | |
| public String readRequestBody(BufferedReader reader, String method, Map<String, String> headers) throws IOException { | |
| if (!("POST".equals(method) || "PUT".equals(method))) { | |
| return ""; | |
| } | |
| int contentLength = 0; | |
| try { | |
| contentLength = Integer.parseInt(headers.getOrDefault("Content-Length", "0")); | |
| } catch (NumberFormatException ignored) {} | |
| if (contentLength <= 0) return ""; | |
| char[] bodyChars = new char[contentLength]; | |
| int bytesRead = reader.read(bodyChars, 0, contentLength); | |
| return bytesRead > 0 ? new String(bodyChars, 0, bytesRead) : ""; | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/staticcontent/StaticFileResolver.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.staticcontent; | |
| import java.io.File; | |
| public class StaticFileResolver { | |
| public boolean isDynamicRoute(String path) { | |
| String cleanPath = path; | |
| if (path.contains("?")) { | |
| cleanPath = path.substring(0, path.indexOf("?")); | |
| } | |
| if (cleanPath.startsWith("/api/")) { | |
| return true; | |
| } | |
| return false; | |
| } | |
| public String getFileName(String path) { | |
| if (path.contains("?")) { | |
| path = path.substring(0, path.indexOf("?")); | |
| } | |
| if (path.startsWith("/")) { | |
| path = path.substring(1); | |
| } | |
| return path; | |
| } | |
| public File resolveFile(String path) { | |
| String fileName = getFileName(path); | |
| if (fileName.isEmpty() || "/".equals(fileName)) { | |
| fileName = "index.html"; | |
| } | |
| fileName = fileName.replace("..", "").replace("//", "/"); | |
| File file = new File(StaticContentServer.BASE_PATH + fileName); | |
| if (!file.exists() && !fileName.toLowerCase().endsWith(".html")) { | |
| File htmlFile = new File(StaticContentServer.BASE_PATH + fileName + ".html"); | |
| if (htmlFile.exists()) { | |
| file = htmlFile; | |
| } | |
| } | |
| return file; | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/staticcontent/StaticResponseWriter.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.staticcontent; | |
| import java.io.File; | |
| import java.io.IOException; | |
| import java.io.OutputStream; | |
| import java.net.Socket; | |
| import java.nio.charset.StandardCharsets; | |
| import java.nio.file.Files; | |
| public class StaticResponseWriter { | |
| private final StaticFileResolver fileResolver = new StaticFileResolver(); | |
| public void serveStaticFile(Socket socket, String path) throws IOException { | |
| OutputStream out = socket.getOutputStream(); | |
| File file = fileResolver.resolveFile(path); | |
| if (file.exists() && !file.isDirectory()) { | |
| byte[] data = Files.readAllBytes(file.toPath()); | |
| String extension = getFileExtension(file.getName()); | |
| String contentType = MimeTypes.getContentType(extension); | |
| String header = "HTTP/1.1 200 OK\r\n" + | |
| "Content-Type: " + contentType + "\r\n" + | |
| "Content-Length: " + data.length + "\r\n" + | |
| "Connection: close\r\n\r\n"; | |
| out.write(header.getBytes(StandardCharsets.UTF_8)); | |
| out.write(data); | |
| } else { | |
| sendErrorResponse(out, 404, "File not found"); | |
| } | |
| out.flush(); | |
| } | |
| public void sendErrorResponse(OutputStream out, int statusCode, String message) throws IOException { | |
| String statusMsg = getStatusMessage(statusCode); | |
| String json = "{\"error\": \"" + message + "\"}"; | |
| byte[] data = json.getBytes(StandardCharsets.UTF_8); | |
| String header = "HTTP/1.1 " + statusCode + " " + statusMsg + "\r\n" + | |
| "Content-Type: application/json; charset=UTF-8\r\n" + | |
| "Content-Length: " + data.length + "\r\n" + | |
| "Connection: close\r\n\r\n"; | |
| out.write(header.getBytes(StandardCharsets.UTF_8)); | |
| out.write(data); | |
| } | |
| private String getFileExtension(String fileName) { | |
| if (fileName == null || fileName.isEmpty()) return ""; | |
| int lastDot = fileName.lastIndexOf('.'); | |
| if (lastDot == -1 || lastDot == fileName.length() - 1) return ""; | |
| return fileName.substring(lastDot + 1).toLowerCase(); | |
| } | |
| private String getStatusMessage(int statusCode) { | |
| switch (statusCode) { | |
| case 200: return "OK"; | |
| case 201: return "Created"; | |
| case 204: return "No Content"; | |
| case 400: return "Bad Request"; | |
| case 401: return "Unauthorized"; | |
| case 403: return "Forbidden"; | |
| case 404: return "Not Found"; | |
| case 500: return "Internal Server Error"; | |
| case 503: return "Service Unavailable"; | |
| default: return "Unknown"; | |
| } | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/staticcontent/MimeTypes.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.staticcontent; | |
| import java.util.HashMap; | |
| import java.util.Map; | |
| public class MimeTypes { | |
| private static final Map<String, String> CONTENT_TYPES = new HashMap<>(); | |
| static { | |
| CONTENT_TYPES.put("html", "text/html; charset=UTF-8"); | |
| CONTENT_TYPES.put("htm", "text/html; charset=UTF-8"); | |
| CONTENT_TYPES.put("css", "text/css; charset=UTF-8"); | |
| CONTENT_TYPES.put("js", "application/javascript; charset=UTF-8"); | |
| CONTENT_TYPES.put("json", "application/json; charset=UTF-8"); | |
| CONTENT_TYPES.put("png", "image/png"); | |
| CONTENT_TYPES.put("jpg", "image/jpeg"); | |
| CONTENT_TYPES.put("jpeg", "image/jpeg"); | |
| CONTENT_TYPES.put("gif", "image/gif"); | |
| CONTENT_TYPES.put("svg", "image/svg+xml"); | |
| CONTENT_TYPES.put("ico", "image/x-icon"); | |
| CONTENT_TYPES.put("pdf", "application/pdf"); | |
| CONTENT_TYPES.put("txt", "text/plain; charset=UTF-8"); | |
| } | |
| public static String getContentType(String extension) { | |
| return CONTENT_TYPES.getOrDefault(extension, "application/octet-stream"); | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/staticcontent/DynamicProxyResponse.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.staticcontent; | |
| import java.util.Map; | |
| public class DynamicProxyResponse { | |
| int statusCode; | |
| String body; | |
| Map<String, String> headers; | |
| public DynamicProxyResponse(int statusCode, String body, Map<String, String> headers) { | |
| this.statusCode = statusCode; | |
| this.body = body; | |
| this.headers = headers; | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/staticcontent/ProxyForwarder.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.staticcontent; | |
| import java.io.BufferedReader; | |
| import java.io.IOException; | |
| import java.io.InputStreamReader; | |
| import java.io.OutputStream; | |
| import java.net.Socket; | |
| import java.nio.charset.StandardCharsets; | |
| import java.util.HashMap; | |
| import java.util.Map; | |
| public class ProxyForwarder { | |
| private final StaticResponseWriter responseWriter = new StaticResponseWriter(); | |
| public void forwardToDynamicServer(Socket clientSocket, | |
| String method, | |
| String path, | |
| String requestBody, | |
| Map<String, String> headers) { | |
| try (Socket dynamicSocket = new Socket( | |
| StaticContentServer.DYNAMIC_SERVER_HOST, | |
| StaticContentServer.DYNAMIC_SERVER_PORT)) { | |
| dynamicSocket.setSoTimeout(10000); | |
| OutputStream out = dynamicSocket.getOutputStream(); | |
| StringBuilder request = new StringBuilder(); | |
| request.append(method).append(" ").append(path).append(" HTTP/1.1\r\n"); | |
| request.append("Host: ") | |
| .append(StaticContentServer.DYNAMIC_SERVER_HOST) | |
| .append(":") | |
| .append(StaticContentServer.DYNAMIC_SERVER_PORT) | |
| .append("\r\n"); | |
| if (headers.containsKey("Content-Type")) { | |
| request.append("Content-Type: ").append(headers.get("Content-Type")).append("\r\n"); | |
| } | |
| if (headers.containsKey("Content-Length")) { | |
| request.append("Content-Length: ").append(headers.get("Content-Length")).append("\r\n"); | |
| } | |
| request.append("X-Forwarded-From: static-server\r\n"); | |
| request.append("Connection: close\r\n"); | |
| if (("POST".equals(method) || "PUT".equals(method)) && | |
| requestBody != null && !requestBody.isEmpty()) { | |
| request.append("\r\n").append(requestBody); | |
| } else { | |
| request.append("\r\n"); | |
| } | |
| out.write(request.toString().getBytes(StandardCharsets.UTF_8)); | |
| out.flush(); | |
| DynamicProxyResponse response = readDynamicResponse(dynamicSocket); | |
| sendResponseToClient(clientSocket, response); | |
| } catch (IOException e) { | |
| System.err.println("Forward error: " + e.getMessage()); | |
| try { | |
| responseWriter.sendErrorResponse( | |
| clientSocket.getOutputStream(), | |
| 503, | |
| "Dynamic server unavailable" | |
| ); | |
| } catch (IOException ignored) {} | |
| } | |
| } | |
| private DynamicProxyResponse readDynamicResponse(Socket socket) throws IOException { | |
| BufferedReader reader = new BufferedReader( | |
| new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8) | |
| ); | |
| String statusLine = reader.readLine(); | |
| int statusCode = 200; | |
| if (statusLine != null && statusLine.length() >= 9) { | |
| try { | |
| statusCode = Integer.parseInt(statusLine.split(" ")[1]); | |
| } catch (Exception ignored) {} | |
| } | |
| Map<String, String> responseHeaders = new HashMap<>(); | |
| String line; | |
| while ((line = reader.readLine()) != null && !line.isEmpty()) { | |
| String[] parts = line.split(": ", 2); | |
| if (parts.length == 2) { | |
| responseHeaders.put(parts[0], parts[1]); | |
| } | |
| } | |
| StringBuilder body = new StringBuilder(); | |
| if ("chunked".equalsIgnoreCase(responseHeaders.get("Transfer-Encoding"))) { | |
| String chunk; | |
| while ((chunk = reader.readLine()) != null) { | |
| if (chunk.isEmpty()) break; | |
| int chunkSize = Integer.parseInt(chunk.trim(), 16); | |
| if (chunkSize == 0) break; | |
| char[] chunkData = new char[chunkSize]; | |
| reader.read(chunkData, 0, chunkSize); | |
| body.append(chunkData); | |
| reader.readLine(); // CRLF | |
| } | |
| } else { | |
| int contentLength = 0; | |
| try { | |
| contentLength = Integer.parseInt(responseHeaders.getOrDefault("Content-Length", "0")); | |
| } catch (NumberFormatException ignored) {} | |
| if (contentLength > 0) { | |
| char[] bodyChars = new char[contentLength]; | |
| int bytesRead = reader.read(bodyChars, 0, contentLength); | |
| if (bytesRead > 0) body.append(bodyChars, 0, bytesRead); | |
| } else { | |
| int ch; | |
| while ((ch = reader.read()) != -1) body.append((char) ch); | |
| } | |
| } | |
| return new DynamicProxyResponse(statusCode, body.toString(), responseHeaders); | |
| } | |
| private void sendResponseToClient(Socket socket, DynamicProxyResponse response) throws IOException { | |
| OutputStream out = socket.getOutputStream(); | |
| String statusMsg = getStatusMessage(response.statusCode); | |
| String contentType = response.headers.getOrDefault("Content-Type", "text/html; charset=UTF-8"); | |
| byte[] data = response.body.getBytes(StandardCharsets.UTF_8); | |
| String header = "HTTP/1.1 " + response.statusCode + " " + statusMsg + "\r\n" + | |
| "Content-Type: " + contentType + "\r\n" + | |
| "Content-Length: " + data.length + "\r\n" + | |
| "Access-Control-Allow-Origin: *\r\n" + | |
| "Connection: close\r\n\r\n"; | |
| out.write(header.getBytes(StandardCharsets.UTF_8)); | |
| out.write(data); | |
| out.flush(); | |
| } | |
| private String getStatusMessage(int statusCode) { | |
| switch (statusCode) { | |
| case 200: return "OK"; | |
| case 201: return "Created"; | |
| case 204: return "No Content"; | |
| case 400: return "Bad Request"; | |
| case 401: return "Unauthorized"; | |
| case 403: return "Forbidden"; | |
| case 404: return "Not Found"; | |
| case 500: return "Internal Server Error"; | |
| case 503: return "Service Unavailable"; | |
| default: return "Unknown"; | |
| } | |
| } | |
| } | |
| EOF | |
| ################################# | |
| # Dynamic server files | |
| ################################# | |
| cat > "$BASE_DIR/dynamic/DynamicContentServer.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.dynamic; | |
| import ir.ac.ut.ece.ie.repository.ArticleRepository; | |
| import ir.ac.ut.ece.ie.routing.Router; | |
| import ir.ac.ut.ece.ie.service.ArticleService; | |
| import java.io.IOException; | |
| import java.net.ServerSocket; | |
| import java.net.Socket; | |
| import java.util.concurrent.ExecutorService; | |
| import java.util.concurrent.Executors; | |
| public class DynamicContentServer { | |
| private ServerSocket serverSocket; | |
| private final int port; | |
| private final ExecutorService threadPool; | |
| private final Router router; | |
| private final ArticleService articleService; | |
| private volatile boolean running = false; | |
| public DynamicContentServer() { | |
| this(9092); | |
| } | |
| public DynamicContentServer(int port) { | |
| this.port = port; | |
| this.threadPool = Executors.newFixedThreadPool(20); | |
| this.router = new Router(); | |
| ArticleRepository repo = new ArticleRepository(); | |
| this.articleService = new ArticleService(repo); | |
| new DynamicRouterInitializer(router, articleService).setupRoutes(); | |
| } | |
| public void start() throws IOException { | |
| serverSocket = new ServerSocket(port); | |
| running = true; | |
| System.out.println("DynamicContentServer started on port " + port); | |
| DynamicRequestHandler handler = new DynamicRequestHandler(router); | |
| while (running) { | |
| Socket socket = serverSocket.accept(); | |
| socket.setSoTimeout(30000); | |
| threadPool.submit(() -> handler.handleClient(socket)); | |
| } | |
| } | |
| public static void main(String[] args) throws IOException { | |
| new DynamicContentServer().start(); | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/dynamic/DynamicRequestHandler.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.dynamic; | |
| import ir.ac.ut.ece.ie.routing.Router; | |
| import java.io.IOException; | |
| import java.net.Socket; | |
| import java.nio.charset.StandardCharsets; | |
| import java.util.HashMap; | |
| import java.util.Map; | |
| import java.util.StringTokenizer; | |
| public class DynamicRequestHandler { | |
| private final Router router; | |
| private final HttpRequestReader requestReader; | |
| private final HttpResponseWriter responseWriter; | |
| private final QueryStringParser queryStringParser; | |
| private final CorsHandler corsHandler; | |
| private final ApiErrorFactory errorFactory; | |
| public DynamicRequestHandler(Router router) { | |
| this.router = router; | |
| this.requestReader = new HttpRequestReader(); | |
| this.responseWriter = new HttpResponseWriter(); | |
| this.queryStringParser = new QueryStringParser(); | |
| this.corsHandler = new CorsHandler(); | |
| this.errorFactory = new ApiErrorFactory(); | |
| } | |
| public void handleClient(Socket socket) { | |
| try { | |
| handleRequest(socket); | |
| } catch (Exception e) { | |
| e.printStackTrace(); | |
| } finally { | |
| try { socket.close(); } catch (IOException ignored) {} | |
| } | |
| } | |
| private void handleRequest(Socket socket) throws IOException { | |
| HttpRequestReader.RequestLineAndHeaders info = requestReader.readRequestLineAndHeaders(socket); | |
| if (info == null || info.getRequestLine() == null) return; | |
| String line = info.getRequestLine(); | |
| System.out.println("Request: " + line); | |
| StringTokenizer tok = new StringTokenizer(line); | |
| if (tok.countTokens() < 2) return; | |
| String method = tok.nextToken().toUpperCase(); | |
| String fullPath = tok.nextToken(); | |
| if ("OPTIONS".equals(method)) { | |
| corsHandler.sendCors(socket); | |
| return; | |
| } | |
| String body = requestReader.readBody(socket, method, info.getHeaders()); | |
| String path = fullPath; | |
| String query = ""; | |
| if (fullPath.contains("?")) { | |
| String[] p = fullPath.split("\\?", 2); | |
| path = p[0]; | |
| query = p[1]; | |
| } | |
| Map<String, String> qParams = queryStringParser.parseQuery(query); | |
| Router.RouteRequest req = new Router.RouteRequest( | |
| method, | |
| path, | |
| new HashMap<>(), | |
| qParams, | |
| info.getHeaders(), | |
| body | |
| ); | |
| Router.RouteResult res = router.findRoute(method, path); | |
| Router.RouteResponse resp; | |
| if (res != null) { | |
| req.getParams().putAll(res.params); | |
| try { | |
| resp = res.handler.handle(req); | |
| } catch (Exception e) { | |
| e.printStackTrace(); | |
| resp = errorFactory.error(500, "Internal error"); | |
| } | |
| } else { | |
| resp = errorFactory.error(404, "Not found"); | |
| } | |
| resp.header("Access-Control-Allow-Origin", "*"); | |
| responseWriter.sendResponse(socket, resp); | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/dynamic/HttpRequestReader.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.dynamic; | |
| import java.io.BufferedReader; | |
| import java.io.IOException; | |
| import java.io.InputStreamReader; | |
| import java.net.Socket; | |
| import java.nio.charset.StandardCharsets; | |
| import java.util.HashMap; | |
| import java.util.Map; | |
| public class HttpRequestReader { | |
| public static class RequestLineAndHeaders { | |
| private final String requestLine; | |
| private final Map<String, String> headers; | |
| private final BufferedReader reader; | |
| public RequestLineAndHeaders(String requestLine, | |
| Map<String, String> headers, | |
| BufferedReader reader) { | |
| this.requestLine = requestLine; | |
| this.headers = headers; | |
| this.reader = reader; | |
| } | |
| public String getRequestLine() { return requestLine; } | |
| public Map<String, String> getHeaders() { return headers; } | |
| public BufferedReader getReader() { return reader; } | |
| } | |
| public RequestLineAndHeaders readRequestLineAndHeaders(Socket socket) throws IOException { | |
| BufferedReader reader = new BufferedReader( | |
| new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8) | |
| ); | |
| String requestLine = reader.readLine(); | |
| if (requestLine == null) return null; | |
| Map<String, String> headers = readHeaders(reader); | |
| return new RequestLineAndHeaders(requestLine, headers, reader); | |
| } | |
| private Map<String, String> readHeaders(BufferedReader r) throws IOException { | |
| Map<String, String> h = new HashMap<>(); | |
| String line; | |
| while ((line = r.readLine()) != null && !line.isEmpty()) { | |
| String[] kv = line.split(":", 2); | |
| if (kv.length == 2) h.put(kv[0].trim(), kv[1].trim()); | |
| } | |
| return h; | |
| } | |
| public String readBody(Socket socket, String method, Map<String, String> h) throws IOException { | |
| if (!("POST".equals(method) || "PUT".equals(method))) return ""; | |
| int len = Integer.parseInt(h.getOrDefault("Content-Length", "0")); | |
| if (len <= 0) return ""; | |
| BufferedReader r = new BufferedReader( | |
| new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8) | |
| ); | |
| char[] buf = new char[len]; | |
| int read = 0; | |
| while (read < len) { | |
| int rd = r.read(buf, read, len - read); | |
| if (rd == -1) break; | |
| read += rd; | |
| } | |
| return new String(buf, 0, read); | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/dynamic/HttpResponseWriter.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.dynamic; | |
| import ir.ac.ut.ece.ie.routing.Router; | |
| import java.io.IOException; | |
| import java.io.OutputStream; | |
| import java.net.Socket; | |
| import java.nio.charset.StandardCharsets; | |
| import java.util.Map; | |
| public class HttpResponseWriter { | |
| public void sendResponse(Socket s, Router.RouteResponse r) throws IOException { | |
| OutputStream out = s.getOutputStream(); | |
| byte[] body = r.getBody() == null ? new byte[0] : r.getBody(); | |
| Map<String, String> h = r.getHeaders(); | |
| if (!h.containsKey("Content-Type")) { | |
| h.put("Content-Type", "application/json; charset=UTF-8"); | |
| } | |
| h.put("Content-Length", String.valueOf(body.length)); | |
| StringBuilder sb = new StringBuilder(); | |
| sb.append("HTTP/1.1 ") | |
| .append(r.getStatusCode()) | |
| .append(" ") | |
| .append(r.getStatusMessage()) | |
| .append("\r\n"); | |
| h.forEach((k, v) -> sb.append(k).append(": ").append(v).append("\r\n")); | |
| sb.append("\r\n"); | |
| out.write(sb.toString().getBytes(StandardCharsets.UTF_8)); | |
| out.write(body); | |
| out.flush(); | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/dynamic/QueryStringParser.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.dynamic; | |
| import java.net.URLDecoder; | |
| import java.nio.charset.StandardCharsets; | |
| import java.util.HashMap; | |
| import java.util.Map; | |
| public class QueryStringParser { | |
| public Map<String, String> parseQuery(String q) { | |
| Map<String, String> m = new HashMap<>(); | |
| if (q == null || q.isEmpty()) return m; | |
| for (String pair : q.split("&")) { | |
| String[] kv = pair.split("=", 2); | |
| try { | |
| m.put( | |
| kv[0], | |
| kv.length > 1 | |
| ? URLDecoder.decode(kv[1], StandardCharsets.UTF_8) | |
| : "" | |
| ); | |
| } catch (Exception e) { | |
| m.put(kv[0], kv.length > 1 ? kv[1] : ""); | |
| } | |
| } | |
| return m; | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/dynamic/CorsHandler.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.dynamic; | |
| import ir.ac.ut.ece.ie.routing.Router; | |
| import java.io.IOException; | |
| import java.net.Socket; | |
| public class CorsHandler { | |
| private final HttpResponseWriter responseWriter = new HttpResponseWriter(); | |
| public void sendCors(Socket s) throws IOException { | |
| Router.RouteResponse r = new Router.RouteResponse().status(204); | |
| r.header("Access-Control-Allow-Origin", "*"); | |
| r.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); | |
| responseWriter.sendResponse(s, r); | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/dynamic/ApiErrorFactory.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.dynamic; | |
| import com.fasterxml.jackson.databind.node.ObjectNode; | |
| import ir.ac.ut.ece.ie.routing.Router; | |
| import ir.ac.ut.ece.ie.util.JsonUtils; | |
| public class ApiErrorFactory { | |
| public Router.RouteResponse error(int code, String msg) { | |
| ObjectNode err = JsonUtils.mapper().createObjectNode(); | |
| err.put("error", msg); | |
| return new Router.RouteResponse() | |
| .status(code) | |
| .json(err.toString()); | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/dynamic/JsonResponseHelper.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.dynamic; | |
| import ir.ac.ut.ece.ie.routing.Router; | |
| import java.nio.charset.StandardCharsets; | |
| public class JsonResponseHelper { | |
| public Router.RouteResponse json(String json) { | |
| return new Router.RouteResponse().json(json); | |
| } | |
| public Router.RouteResponse html(byte[] content) { | |
| return new Router.RouteResponse().html(new String(content, StandardCharsets.UTF_8)); | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/dynamic/PageRenderer.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.dynamic; | |
| import ir.ac.ut.ece.ie.server.dynamic.pages.*; | |
| import java.util.HashMap; | |
| import java.util.Map; | |
| public class PageRenderer { | |
| public byte[] renderHome(Map<String, String> params) { | |
| return new HomePage().pageBody(params); | |
| } | |
| public byte[] renderAddArticle(Map<String, String> params) { | |
| return new AddArticlePage().pageBody(params); | |
| } | |
| public byte[] renderSearch(Map<String, String> params) { | |
| return new SearchPage().pageBody(params); | |
| } | |
| public byte[] renderArticle(Map<String, String> params) { | |
| return new ArticlePage().pageBody(params); | |
| } | |
| public byte[] renderEditArticle(Map<String, String> params) { | |
| return new EditArticlePage().pageBody(params); | |
| } | |
| public Map<String, String> singleParam(String key, String value) { | |
| Map<String, String> map = new HashMap<>(); | |
| if (value != null) map.put(key, value); | |
| return map; | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/dynamic/DynamicRouterInitializer.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.dynamic; | |
| import com.fasterxml.jackson.databind.JsonNode; | |
| import com.fasterxml.jackson.databind.node.ArrayNode; | |
| import com.fasterxml.jackson.databind.node.ObjectNode; | |
| import ir.ac.ut.ece.ie.dto.ArticleDTO; | |
| import ir.ac.ut.ece.ie.dto.CreateArticleRequest; | |
| import ir.ac.ut.ece.ie.model.Article; | |
| import ir.ac.ut.ece.ie.routing.Router; | |
| import ir.ac.ut.ece.ie.service.ArticleService; | |
| import ir.ac.ut.ece.ie.util.JsonUtils; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import java.util.Optional; | |
| public class DynamicRouterInitializer { | |
| private final Router router; | |
| private final ArticleService articleService; | |
| private final JsonResponseHelper jsonHelper; | |
| private final ApiErrorFactory errorFactory; | |
| private final PageRenderer pageRenderer; | |
| public DynamicRouterInitializer(Router router, ArticleService articleService) { | |
| this.router = router; | |
| this.articleService = articleService; | |
| this.jsonHelper = new JsonResponseHelper(); | |
| this.errorFactory = new ApiErrorFactory(); | |
| this.pageRenderer = new PageRenderer(); | |
| } | |
| public void setupRoutes() { | |
| // Page routes | |
| router.get("/", req -> jsonHelper.html(pageRenderer.renderHome(req.getQueryParams()))); | |
| router.get("/addarticle", req -> jsonHelper.html(pageRenderer.renderAddArticle(req.getQueryParams()))); | |
| router.get("/search", req -> jsonHelper.html(pageRenderer.renderSearch(req.getQueryParams()))); | |
| router.get("/article", req -> { | |
| String id = req.getQueryParam("id"); | |
| return jsonHelper.html(pageRenderer.renderArticle(pageRenderer.singleParam("id", id))); | |
| }); | |
| router.get("/editarticle", req -> { | |
| String id = req.getQueryParam("id"); | |
| return jsonHelper.html(pageRenderer.renderEditArticle(pageRenderer.singleParam("id", id))); | |
| }); | |
| // API routes | |
| router.get("/api/articles", new routehandlers.ArticlesApiHandler(articleService)::handleList); | |
| router.get("/api/articles/search", new routehandlers.SearchApiHandler(articleService)::handleSearch); | |
| router.get("/api/stats", new routehandlers.StatsApiHandler(articleService)::handleStats); | |
| router.post("/api/admin/clear", req -> { | |
| articleService.clearAll(); | |
| return jsonHelper.json("{\"status\":\"cleared\"}"); | |
| }); | |
| router.post("/api/articles", new routehandlers.CreateArticleHandler(articleService)::handleCreate); | |
| router.get("/api/articles/{id}", new routehandlers.ArticlesApiHandler(articleService)::handleGetById); | |
| router.put("/api/articles/{id}", new routehandlers.UpdateArticleHandler(articleService)::handleUpdate); | |
| router.delete("/api/articles/{id}", new routehandlers.DeleteArticleHandler(articleService)::handleDelete); | |
| } | |
| // Utility JSON builders (can be shared by handlers if needed) | |
| public static ObjectNode articleToSummaryJson(Article a) { | |
| ObjectNode node = JsonUtils.mapper().createObjectNode(); | |
| node.put("id", a.getId()); | |
| node.put("title", a.getTitle()); | |
| node.put("abstract", a.getAbstract()); | |
| node.put("publishedDate", a.getPublishedDate() != null ? a.getPublishedDate().toString() : null); | |
| node.put("relatedCount", a.getRelatedArticles() != null ? a.getRelatedArticles().size() : 0); | |
| return node; | |
| } | |
| public static ObjectNode articleToDetailJson(Article a) { | |
| ObjectNode node = articleToSummaryJson(a); | |
| node.put("body", a.getBody()); | |
| ArrayNode rel = JsonUtils.mapper().createArrayNode(); | |
| if (a.getRelatedArticles() != null) { | |
| for (Article ra : a.getRelatedArticles()) { | |
| ObjectNode rn = JsonUtils.mapper().createObjectNode(); | |
| rn.put("id", ra.getId()); | |
| rn.put("title", ra.getTitle()); | |
| rel.add(rn); | |
| } | |
| } | |
| node.set("relatedArticles", rel); | |
| return node; | |
| } | |
| public static Long parsePositiveLong(String s) { | |
| try { | |
| Long id = Long.parseLong(s); | |
| return id > 0 ? id : null; | |
| } catch (Exception e) { | |
| return null; | |
| } | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/dynamic/routehandlers/ArticlesApiHandler.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.dynamic.routehandlers; | |
| import com.fasterxml.jackson.databind.node.ArrayNode; | |
| import ir.ac.ut.ece.ie.model.Article; | |
| import ir.ac.ut.ece.ie.routing.Router; | |
| import ir.ac.ut.ece.ie.server.dynamic.ApiErrorFactory; | |
| import ir.ac.ut.ece.ie.server.dynamic.DynamicRouterInitializer; | |
| import ir.ac.ut.ece.ie.service.ArticleService; | |
| import ir.ac.ut.ece.ie.util.JsonUtils; | |
| import java.util.Optional; | |
| public class ArticlesApiHandler { | |
| private final ArticleService articleService; | |
| private final ApiErrorFactory errorFactory = new ApiErrorFactory(); | |
| public ArticlesApiHandler(ArticleService articleService) { | |
| this.articleService = articleService; | |
| } | |
| public Router.RouteResponse handleList(Router.RouteRequest req) { | |
| ArrayNode arr = JsonUtils.mapper().createArrayNode(); | |
| for (Article a : articleService.getAllArticles()) { | |
| arr.add(DynamicRouterInitializer.articleToSummaryJson(a)); | |
| } | |
| return new Router.RouteResponse().json(arr.toString()); | |
| } | |
| public Router.RouteResponse handleGetById(Router.RouteRequest req) { | |
| Long id = DynamicRouterInitializer.parsePositiveLong(req.getParam("id")); | |
| if (id == null) return errorFactory.error(400, "Invalid id"); | |
| Optional<Article> opt = articleService.getArticleById(id); | |
| if (opt.isEmpty()) return errorFactory.error(404, "Article not found"); | |
| return new Router.RouteResponse().json( | |
| DynamicRouterInitializer.articleToDetailJson(opt.get()).toString() | |
| ); | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/dynamic/routehandlers/SearchApiHandler.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.dynamic.routehandlers; | |
| import com.fasterxml.jackson.databind.node.ArrayNode; | |
| import ir.ac.ut.ece.ie.model.Article; | |
| import ir.ac.ut.ece.ie.routing.Router; | |
| import ir.ac.ut.ece.ie.server.dynamic.DynamicRouterInitializer; | |
| import ir.ac.ut.ece.ie.service.ArticleService; | |
| import ir.ac.ut.ece.ie.util.JsonUtils; | |
| import java.util.List; | |
| public class SearchApiHandler { | |
| private final ArticleService articleService; | |
| public SearchApiHandler(ArticleService articleService) { | |
| this.articleService = articleService; | |
| } | |
| public Router.RouteResponse handleSearch(Router.RouteRequest req) { | |
| String q = req.getQueryParam("q"); | |
| if (q == null || q.trim().isEmpty()) q = req.getQueryParam("query"); | |
| if (q == null || q.trim().isEmpty()) return new Router.RouteResponse().json("[]"); | |
| List<Article> results = articleService.searchArticles(q); | |
| ArrayNode arr = JsonUtils.mapper().createArrayNode(); | |
| for (Article a : results) { | |
| arr.add(DynamicRouterInitializer.articleToSummaryJson(a)); | |
| } | |
| return new Router.RouteResponse().json(arr.toString()); | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/dynamic/routehandlers/StatsApiHandler.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.dynamic.routehandlers; | |
| import com.fasterxml.jackson.databind.node.ObjectNode; | |
| import ir.ac.ut.ece.ie.model.Article; | |
| import ir.ac.ut.ece.ie.routing.Router; | |
| import ir.ac.ut.ece.ie.service.ArticleService; | |
| import ir.ac.ut.ece.ie.util.JsonUtils; | |
| import java.util.List; | |
| public class StatsApiHandler { | |
| private final ArticleService articleService; | |
| public StatsApiHandler(ArticleService articleService) { | |
| this.articleService = articleService; | |
| } | |
| public Router.RouteResponse handleStats(Router.RouteRequest req) { | |
| List<Article> all = articleService.getAllArticles(); | |
| int total = all.size(); | |
| int refs = all.stream().mapToInt(a -> a.getRelatedArticles().size()).sum(); | |
| ObjectNode node = JsonUtils.mapper().createObjectNode(); | |
| node.put("totalArticles", total); | |
| node.put("totalReferences", refs); | |
| return new Router.RouteResponse().json(node.toString()); | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/dynamic/routehandlers/CreateArticleHandler.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.dynamic.routehandlers; | |
| import com.fasterxml.jackson.databind.JsonNode; | |
| import ir.ac.ut.ece.ie.dto.ArticleDTO; | |
| import ir.ac.ut.ece.ie.dto.CreateArticleRequest; | |
| import ir.ac.ut.ece.ie.routing.Router; | |
| import ir.ac.ut.ece.ie.server.dynamic.ApiErrorFactory; | |
| import ir.ac.ut.ece.ie.service.ArticleService; | |
| import ir.ac.ut.ece.ie.util.JsonUtils; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| public class CreateArticleHandler { | |
| private final ArticleService articleService; | |
| private final ApiErrorFactory errorFactory = new ApiErrorFactory(); | |
| public CreateArticleHandler(ArticleService articleService) { | |
| this.articleService = articleService; | |
| } | |
| public Router.RouteResponse handleCreate(Router.RouteRequest req) { | |
| try { | |
| JsonNode body = JsonUtils.mapper().readTree(req.getBody()); | |
| if (!body.hasNonNull("title") || !body.hasNonNull("abstract") || !body.hasNonNull("body")) | |
| return errorFactory.error(400, "Missing required fields"); | |
| String title = body.get("title").asText().trim(); | |
| if (title.isEmpty()) return errorFactory.error(400, "Title cannot be empty"); | |
| if (!articleService.isTitleUnique(title)) return errorFactory.error(409, "Duplicate title"); | |
| CreateArticleRequest r = new CreateArticleRequest(); | |
| r.setTitle(title); | |
| r.setAbstractText(body.get("abstract").asText().trim()); | |
| r.setBody(body.get("body").asText().trim()); | |
| if (body.has("relatedArticleIds") && body.get("relatedArticleIds").isArray()) { | |
| List<Long> ids = new ArrayList<>(); | |
| for (JsonNode n : body.get("relatedArticleIds")) { | |
| if (!n.canConvertToLong()) return errorFactory.error(400, "Invalid related id"); | |
| ids.add(n.asLong()); | |
| } | |
| r.setRelatedArticleIds(ids); | |
| } | |
| ArticleDTO created = articleService.create(r); | |
| return new Router.RouteResponse() | |
| .status(201) | |
| .json(JsonUtils.toJson(created)); | |
| } catch (Exception e) { | |
| e.printStackTrace(); | |
| return errorFactory.error(400, "Invalid request"); | |
| } | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/dynamic/routehandlers/UpdateArticleHandler.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.dynamic.routehandlers; | |
| import com.fasterxml.jackson.databind.JsonNode; | |
| import ir.ac.ut.ece.ie.model.Article; | |
| import ir.ac.ut.ece.ie.routing.Router; | |
| import ir.ac.ut.ece.ie.server.dynamic.ApiErrorFactory; | |
| import ir.ac.ut.ece.ie.server.dynamic.DynamicRouterInitializer; | |
| import ir.ac.ut.ece.ie.service.ArticleService; | |
| import ir.ac.ut.ece.ie.util.JsonUtils; | |
| import java.util.Optional; | |
| public class UpdateArticleHandler { | |
| private final ArticleService articleService; | |
| private final ApiErrorFactory errorFactory = new ApiErrorFactory(); | |
| public UpdateArticleHandler(ArticleService articleService) { | |
| this.articleService = articleService; | |
| } | |
| public Router.RouteResponse handleUpdate(Router.RouteRequest req) { | |
| Long id = DynamicRouterInitializer.parsePositiveLong(req.getParam("id")); | |
| if (id == null) return errorFactory.error(400, "Invalid id"); | |
| Optional<Article> opt = articleService.getArticleById(id); | |
| if (opt.isEmpty()) return errorFactory.error(404, "Article not found"); | |
| Article article = opt.get(); | |
| JsonNode body; | |
| try { | |
| body = JsonUtils.mapper().readTree(req.getBody()); | |
| } catch (Exception e) { | |
| return errorFactory.error(400, "Invalid JSON"); | |
| } | |
| if (body.has("title")) { | |
| String t = body.get("title").asText().trim(); | |
| if (t.isEmpty()) return errorFactory.error(400, "Title cannot be empty"); | |
| if (!t.equals(article.getTitle()) && !articleService.isTitleUnique(t)) | |
| return errorFactory.error(409, "Duplicate title"); | |
| article.setTitle(t); | |
| } | |
| if (body.has("abstract")) { | |
| String abs = body.get("abstract").asText().trim(); | |
| if (abs.isEmpty()) return errorFactory.error(400, "Abstract cannot be empty"); | |
| article.setAbstract(abs); | |
| } | |
| if (body.has("body")) { | |
| String b = body.get("body").asText().trim(); | |
| if (b.isEmpty()) return errorFactory.error(400, "Body cannot be empty"); | |
| article.setBody(b); | |
| } | |
| if (body.has("relatedArticleIds") && body.get("relatedArticleIds").isArray()) { | |
| article.getRelatedArticles().clear(); | |
| for (JsonNode n : body.get("relatedArticleIds")) { | |
| if (!n.canConvertToLong()) return errorFactory.error(400, "Invalid related id"); | |
| Long rid = n.asLong(); | |
| if (!rid.equals(article.getId())) | |
| articleService.getArticleById(rid).ifPresent(article::addRelatedArticle); | |
| } | |
| } | |
| articleService.saveArticle(article); | |
| return new Router.RouteResponse().json( | |
| DynamicRouterInitializer.articleToDetailJson(article).toString() | |
| ); | |
| } | |
| } | |
| EOF | |
| cat > "$BASE_DIR/dynamic/routehandlers/DeleteArticleHandler.java" << 'EOF' | |
| package ir.ac.ut.ece.ie.server.dynamic.routehandlers; | |
| import ir.ac.ut.ece.ie.routing.Router; | |
| import ir.ac.ut.ece.ie.server.dynamic.ApiErrorFactory; | |
| import ir.ac.ut.ece.ie.server.dynamic.DynamicRouterInitializer; | |
| import ir.ac.ut.ece.ie.service.ArticleService; | |
| public class DeleteArticleHandler { | |
| private final ArticleService articleService; | |
| private final ApiErrorFactory errorFactory = new ApiErrorFactory(); | |
| public DeleteArticleHandler(ArticleService articleService) { | |
| this.articleService = articleService; | |
| } | |
| public Router.RouteResponse handleDelete(Router.RouteRequest req) { | |
| Long id = DynamicRouterInitializer.parsePositiveLong(req.getParam("id")); | |
| if (id == null) return errorFactory.error(400, "Invalid id"); | |
| if (articleService.getArticleById(id).isEmpty()) return errorFactory.error(404, "Not found"); | |
| articleService.deleteArticle(id); | |
| return new Router.RouteResponse().status(204); | |
| } | |
| } | |
| EOF | |
Xet Storage Details
- Size:
- 45.4 kB
- Xet hash:
- a819d1ab9416395575691d5a441352079b9f38451af5957ee50222ee620f3796
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.