diff --git a/src/main/java/com/github/voxxin/web/AbstractRoute.java b/src/main/java/com/github/voxxin/web/AbstractRoute.java index 3aa0c60..1cecc73 100644 --- a/src/main/java/com/github/voxxin/web/AbstractRoute.java +++ b/src/main/java/com/github/voxxin/web/AbstractRoute.java @@ -11,7 +11,8 @@ public class AbstractRoute { public final String route; public AbstractRoute(String route) { - this.route = route; + route = route.replaceAll("^/+", ""); + this.route = "/"+route; } /** diff --git a/src/main/java/com/github/voxxin/web/FilePathRoute.java b/src/main/java/com/github/voxxin/web/FilePathRoute.java new file mode 100644 index 0000000..785bfe4 --- /dev/null +++ b/src/main/java/com/github/voxxin/web/FilePathRoute.java @@ -0,0 +1,39 @@ +package com.github.voxxin.web; + +import com.github.voxxin.web.request.FormattedRequest; +import com.github.voxxin.web.request.FormattedResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; + +import java.io.*; + +class FilePathRoute extends AbstractRoute { + private final Logger LOGGER = LoggerFactory.getLogger(WebServer.class); + private final File file; + + public FilePathRoute(File file, String route) { + super(route + file.getName()); + this.file = file; + } + + @Override + public OutputStream handleRequests(FormattedRequest request, OutputStream outputStream) throws IOException { + try (FileInputStream fileInputStream = new FileInputStream(file)) { + outputStream.write(new FormattedResponse() + .contentType(Files.probeContentType(file.toPath())) + .content(fileInputStream.readAllBytes()) + .statusCode(200) + .statusMessage("OK").build()); + } catch (IOException e) { + LOGGER.error("Error occurred while handling file request: {}", e.getMessage()); + } + + return outputStream; + } +} + diff --git a/src/main/java/com/github/voxxin/web/WebServer.java b/src/main/java/com/github/voxxin/web/WebServer.java index 3d71e98..91bc308 100644 --- a/src/main/java/com/github/voxxin/web/WebServer.java +++ b/src/main/java/com/github/voxxin/web/WebServer.java @@ -1,11 +1,18 @@ package com.github.voxxin.web; import java.io.BufferedReader; +import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; +import java.net.URISyntaxException; +import java.net.URL; import java.nio.charset.StandardCharsets; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -123,4 +130,107 @@ public void changePort(int newPort) { public void errorPage(AbstractRoute errorRoute) { this.errorRoute = errorRoute; } + + /** + * Adds a directory path to be served publicly. + * @param dirPath The path of the directory to be served. + * @param publicPath The public path where the directory should be accessible. + * @param pathType The type of path, either INTERNAL or EXTERNAL. + * @param directoryPosition The position of the directory relative to the publicPath. + */ + public void addPublicDirPath(String dirPath, String publicPath, PathType pathType, DirectoryPosition directoryPosition) { + switch (pathType) { + case INTERNAL: + case EXTERNAL: + processDirPath(dirPath, publicPath, pathType.toString(), directoryPosition); + break; + default: + LOGGER.error("Invalid path type: " + pathType); + } + } + + /** + * Processes a directory path recursively. + * @param dirPath The path of the directory to process. + * @param publicPath The public path corresponding to the directory. + * @param type The type of path, either "INTERNAL" or "EXTERNAL". + * @param directoryPosition The position of the directory relative to the publicPath. + */ + private void processDirPath(String dirPath, String publicPath, String type, DirectoryPosition directoryPosition) { + try { + Path directory = type.equals("INTERNAL") ? Paths.get(WebServer.class.getClassLoader().getResource(dirPath).toURI()) : Paths.get(dirPath); + if (!Files.exists(directory)) { + LOGGER.error(type + " directory not found: " + dirPath); + return; + } + + try (DirectoryStream stream = Files.newDirectoryStream(directory)) { + for (Path path : stream) { + if (Files.isDirectory(path)) { + String newPath = dirPath + "/" + path.getFileName() + "/"; + String newPublicPath = ""; + + if (directoryPosition == DirectoryPosition.CURRENT) newPublicPath = publicPath; + else if (directoryPosition == DirectoryPosition.SUBDIRECTORY) + newPublicPath = publicPath + path.getFileName() + "/"; + else continue; + + processDirPath(newPath, newPublicPath, type, directoryPosition); + } else if (Files.isRegularFile(path)) { + processPublicFile(path.toFile(), publicPath); + } + } + } catch (IOException e) { + LOGGER.error("Error reading files in " + type.toLowerCase() + " directory: " + dirPath, e); + } + } catch (URISyntaxException e) { + LOGGER.error("Invalid directory path: " + dirPath, e); + } + } + + + /** + * Processes a public file and adds it to the routes. + * @param file The public file to be processed. + * @param publicPath The public path corresponding to the file. + */ + private void processPublicFile(File file, String publicPath) { + FilePathRoute filePathRoute = new FilePathRoute(file, publicPath); + if (!routes.contains(filePathRoute)) routes.add(filePathRoute); + } + + /** + * Enum representing the type of path. + */ + public enum PathType { + /** + * Represents an internal path, typically referring to resources within the application. + */ + INTERNAL, + + /** + * Represents an external path, typically referring to resources outside the application. + */ + EXTERNAL; + } + + /** + * Enum representing the position of the directory relative to the publicPath. + */ + public enum DirectoryPosition { + /** + * Indicates that the directory should not be included as a subsidiary div. + */ + NONE, + + /** + * Indicates that the directory should be included as a subsidiary div at the same level as the publicPath. + */ + CURRENT, + + /** + * Indicates that the directory should be included as a subsidiary div within the publicPath. + */ + SUBDIRECTORY; + } } \ No newline at end of file diff --git a/src/main/java/com/github/voxxin/web/element/HtmlElement.java b/src/main/java/com/github/voxxin/web/element/HtmlElement.java index 962d0ed..ad5434c 100644 --- a/src/main/java/com/github/voxxin/web/element/HtmlElement.java +++ b/src/main/java/com/github/voxxin/web/element/HtmlElement.java @@ -7,8 +7,8 @@ import java.util.List; public class HtmlElement { - private final String tagName; - private final List attributes; + private String tagName; + private List attributes; private List subElements; private String subElement; @@ -133,6 +133,11 @@ public String getTagName() { return this.tagName; } + public void setTagName(String tagName) { + this.tagName = tagName; + } + + /** * Get the list of attributes of this HtmlElement. * diff --git a/src/main/java/com/github/voxxin/web/element/HtmlParser.java b/src/main/java/com/github/voxxin/web/element/HtmlParser.java index f4830d9..900c8cf 100644 --- a/src/main/java/com/github/voxxin/web/element/HtmlParser.java +++ b/src/main/java/com/github/voxxin/web/element/HtmlParser.java @@ -1,59 +1,104 @@ package com.github.voxxin.web.element; import java.util.*; -import java.util.regex.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class HtmlParser { - /** - * Parse HTML string into a list of HtmlElements. + * Parses the given HTML string and returns a list of HtmlElement objects representing the parsed content. * - * @param htmlString The HTML string to parse. - * @return The list of HtmlElements parsed from the HTML string. + * @param htmlString the HTML string to be parsed + * @return a list of HtmlElement objects representing the parsed content */ public static List parseHtmlString(String htmlString) { return parseContent(htmlString); } /** - * Parses the content of an HTML string recursively. + * Parses the given HTML string and returns a list of HtmlElement objects representing the parsed content. * - * @param content The HTML content to parse. - * @return The list of HtmlElements parsed from the HTML content. + * @param html the HTML string to be parsed + * @return a list of HtmlElement objects representing the parsed content */ - private static List parseContent(String content) { + private static List parseContent(String html) { + List tags = new ArrayList<>(); List elements = new ArrayList<>(); - Pattern pattern = Pattern.compile("<(\\w+)(.*?)>(.*?)|<(\\w+)(.*?)>", Pattern.DOTALL); - Matcher matcher = pattern.matcher(content); + int end = html.length() - 1; + int layer = 0; + + while (end >= 0) { + int closingIndex = html.lastIndexOf('>', end); + int openingIndex = html.lastIndexOf('<', closingIndex); + + if (openingIndex != -1 && closingIndex != -1) { + String tagString = html.substring(openingIndex + 1, closingIndex); + boolean isOpeningTag = !tagString.startsWith("/"); + String tagName = isOpeningTag ? tagString.split("\\s+")[0] : tagString.substring(1); + String attributes = isOpeningTag ? tagString.substring(tagName.length()).trim() : ""; + int rareLayer = html.contains("") ? layer : layer+1; + + tags.add(new Tag(tagName, attributes, isOpeningTag, openingIndex, closingIndex + 1, rareLayer)); + + if (isOpeningTag && html.contains("")) { + layer--; + } else if (!isOpeningTag) { + layer++; + } + end = openingIndex - 1; + } else { + break; + } + } - while (matcher.find()) { - String tagName = matcher.group(1) != null ? matcher.group(1) : matcher.group(4); - String attributes = matcher.group(2) != null ? matcher.group(2) : matcher.group(5); - String innerContent = matcher.group(3); + Collections.reverse(tags); - if (tagName != null) { - HtmlElement element = new HtmlElement(tagName, parseAttributes(attributes), innerContent != null ? innerContent : ""); + for (int i = 0; i < tags.size(); i++) { + Tag tag = tags.get(i); - if (innerContent != null) { - List innerElements = parseContent(innerContent); + if (tag.layer != 1 || !tag.isOpeningTag) continue; - if (!innerElements.isEmpty()) element.addSubElements(innerElements); - else element.setSubElement(innerContent); + boolean hasClosingTag = false; + int sameTagRepeats = 0; + for (int j = i + 1; j < tags.size(); j++) { + Tag closingTag = tags.get(j); + if (closingTag.isOpeningTag && closingTag.tagName.equals(tag.tagName)) { + sameTagRepeats++; + continue; } - elements.add(element); + if (!closingTag.isOpeningTag && closingTag.tagName.equals(tag.tagName)) { + if (sameTagRepeats > 0) { + sameTagRepeats--; + continue; + } + + String content = html.substring(tag.closingIndex, closingTag.openingIndex); + + List subElements = parseContent(content); + if (subElements.isEmpty()) elements.add(new HtmlElement(tag.tagName, parseAttributes(tag.attributes), content)); + else elements.add(new HtmlElement(tag.tagName, parseAttributes(tag.attributes), subElements)); + + hasClosingTag = true; + break; + } + } + + if (!hasClosingTag) { + elements.add(new HtmlElement(tag.tagName, parseAttributes(tag.attributes), (String) null)); } } + return elements; } /** - * Parse HTML attributes into a list of strings. + * Parses the given attributes string and returns a list of attributes. * - * @param attributesString The string containing HTML attributes. - * @return The list of parsed attributes. + * @param attributesString the string containing the attributes + * @return a list of attributes */ private static List parseAttributes(String attributesString) { List attributes = new ArrayList<>(); @@ -68,4 +113,22 @@ private static List parseAttributes(String attributesString) { } return attributes; } -} \ No newline at end of file + + static class Tag { + String tagName; + String attributes; + boolean isOpeningTag; + int openingIndex; + int closingIndex; + int layer; + + public Tag(String tagName, String attributes, boolean isOpeningTag, int openingIndex, int closingIndex, int layer) { + this.tagName = tagName; + this.attributes = attributes; + this.isOpeningTag = isOpeningTag; + this.openingIndex = openingIndex; + this.closingIndex = closingIndex; + this.layer = layer; + } + } +} diff --git a/src/main/java/com/github/voxxin/web/request/FormattedRequest.java b/src/main/java/com/github/voxxin/web/request/FormattedRequest.java index aeb8587..52685e7 100644 --- a/src/main/java/com/github/voxxin/web/request/FormattedRequest.java +++ b/src/main/java/com/github/voxxin/web/request/FormattedRequest.java @@ -5,13 +5,13 @@ public class FormattedRequest { - private final HashMap headers; - private final String body; - private final String method; + private HashMap headers; + private String body; + private String method; private String path; - private final String pathParameters; - private final HashMap query; - private final String httpVersion; + private String pathParameters; + private HashMap query; + private String httpVersion; /** * Constructor for FormattedRequest. @@ -19,6 +19,8 @@ public class FormattedRequest { * @param inputHeaders The list of input headers. */ public FormattedRequest(List inputHeaders) { + if (!inputHeaders.get(0).contains("HTTP/")) return; + this.body = (inputHeaders.get(0).split(" ").length > 1 && inputHeaders.get(0).split(" ")[2].contains("HTTP/")) ? null : inputHeaders.remove(0); String[] mainMethods = inputHeaders.remove(0).split(" "); diff --git a/src/main/java/com/github/voxxin/web/request/FormattedResponse.java b/src/main/java/com/github/voxxin/web/request/FormattedResponse.java index f2b5750..e9e43bc 100644 --- a/src/main/java/com/github/voxxin/web/request/FormattedResponse.java +++ b/src/main/java/com/github/voxxin/web/request/FormattedResponse.java @@ -2,6 +2,10 @@ import java.nio.charset.StandardCharsets; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + public class FormattedResponse { private static final String DEFAULT_HTTP_VERSION = "HTTP/1.1"; @@ -12,6 +16,7 @@ public class FormattedResponse { private String statusMessage; private String contentType; private byte[] contentBytes; + private Map customHeaders = new HashMap<>(); /** * Set the HTTP version for the response. @@ -79,23 +84,45 @@ public FormattedResponse content(byte[] contentBytes) { return this; } + /** + * Add a custom header to the response. + * + * @param name The name of the header. + * @param value The value of the header. + * @return The FormattedResponse instance. + */ + public FormattedResponse addHeader(String name, String value) { + customHeaders.put(name, value); + return this; + } + /** * Build the formatted response. * - * @return The formatted response string. + * @return The formatted response as bytes. */ - public String build() { + public byte[] build() { StringBuilder responseBuilder = new StringBuilder(); responseBuilder.append(httpVersion).append(" ").append(statusCode).append(" ").append(statusMessage).append("\r\n") .append("Content-Type: ").append(contentType).append("\r\n"); + // Append custom headers + for (Map.Entry entry : customHeaders.entrySet()) { + responseBuilder.append(entry.getKey()).append(": ").append(entry.getValue()).append("\r\n"); + } + if (contentBytes != null) { - responseBuilder.append("Content-Length: ").append(contentBytes.length).append("\r\n\r\n") - .append(new String(contentBytes, StandardCharsets.UTF_8)); + responseBuilder.append("Content-Length: ").append(contentBytes.length).append("\r\n\r\n"); + byte[] headerBytes = responseBuilder.toString().getBytes(StandardCharsets.UTF_8); + + byte[] responseBytes = new byte[headerBytes.length + contentBytes.length]; + System.arraycopy(headerBytes, 0, responseBytes, 0, headerBytes.length); + System.arraycopy(contentBytes, 0, responseBytes, headerBytes.length, contentBytes.length); + + return responseBytes; } else { - responseBuilder.append("\r\n"); + return responseBuilder.append("\r\n").toString().getBytes(StandardCharsets.UTF_8); } - - return responseBuilder.toString(); } } + diff --git a/src/test/java/WebsiteTest.java b/src/test/java/WebsiteTest.java index 72079f0..ad1f101 100644 --- a/src/test/java/WebsiteTest.java +++ b/src/test/java/WebsiteTest.java @@ -6,8 +6,8 @@ public class WebsiteTest { private final WebServer web; public WebsiteTest() { - this.web = new WebServer(2020, new IndexRoute()); + this.web.addPublicDirPath("assets/web/public", "public/", WebServer.PathType.INTERNAL, WebServer.DirectoryPosition.SUBDIRECTORY); this.web.errorPage(new ErrorRoute()); } diff --git a/src/test/java/routes/ErrorRoute.java b/src/test/java/routes/ErrorRoute.java index 69c99d0..8230862 100644 --- a/src/test/java/routes/ErrorRoute.java +++ b/src/test/java/routes/ErrorRoute.java @@ -21,7 +21,6 @@ public OutputStream handleRequests(FormattedRequest request, OutputStream output .statusCode(404) .statusMessage("Not Found") .build() - .getBytes() ); return outputStream; } diff --git a/src/test/java/routes/IndexRoute.java b/src/test/java/routes/IndexRoute.java index 84919b0..36d84b9 100644 --- a/src/test/java/routes/IndexRoute.java +++ b/src/test/java/routes/IndexRoute.java @@ -54,11 +54,10 @@ public OutputStream handleRequests(FormattedRequest request, OutputStream output outputStream.write( new FormattedResponse() .contentType("text/html") - .content(welcomePage) + .content(welcomePage.getBytes()) .statusCode(200) .statusMessage("NICE ONE") .build() - .getBytes() ); return super.handleRequests(request, outputStream); diff --git a/src/test/resources/assets/web/public/hello.html b/src/test/resources/assets/web/public/hello.html new file mode 100644 index 0000000..36c6561 --- /dev/null +++ b/src/test/resources/assets/web/public/hello.html @@ -0,0 +1,13 @@ + + + + + Hello + + + +welcome ! + + + + \ No newline at end of file diff --git a/src/test/resources/assets/web/public/img/ladies.webp b/src/test/resources/assets/web/public/img/ladies.webp new file mode 100644 index 0000000..e9aaa69 Binary files /dev/null and b/src/test/resources/assets/web/public/img/ladies.webp differ