From 8a67093102cd8b098a86ea446fc699820197b2cc Mon Sep 17 00:00:00 2001 From: MrLetsplay Date: Sun, 18 Feb 2024 14:44:31 +0100 Subject: [PATCH] Add support for RSS feeds --- .../java/me/mrletsplay/mdblog/MdBlog.java | 47 ++++++++++- .../me/mrletsplay/mdblog/rss/FeedConfig.java | 31 ++++++++ .../me/mrletsplay/mdblog/rss/RSSFeed.java | 78 +++++++++++++++++++ .../me/mrletsplay/mdblog/rss/RSSItem.java | 5 ++ .../me/mrletsplay/mdblog/rss/RSSResponse.java | 49 ++++++++++++ 5 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 src/main/java/me/mrletsplay/mdblog/rss/FeedConfig.java create mode 100644 src/main/java/me/mrletsplay/mdblog/rss/RSSFeed.java create mode 100644 src/main/java/me/mrletsplay/mdblog/rss/RSSItem.java create mode 100644 src/main/java/me/mrletsplay/mdblog/rss/RSSResponse.java diff --git a/src/main/java/me/mrletsplay/mdblog/MdBlog.java b/src/main/java/me/mrletsplay/mdblog/MdBlog.java index 1276b1f..71b7646 100644 --- a/src/main/java/me/mrletsplay/mdblog/MdBlog.java +++ b/src/main/java/me/mrletsplay/mdblog/MdBlog.java @@ -22,6 +22,10 @@ import me.mrletsplay.mdblog.blog.Post; import me.mrletsplay.mdblog.blog.PostMetadata; import me.mrletsplay.mdblog.markdown.MdParser; import me.mrletsplay.mdblog.markdown.MdRenderer; +import me.mrletsplay.mdblog.rss.FeedConfig; +import me.mrletsplay.mdblog.rss.RSSFeed; +import me.mrletsplay.mdblog.rss.RSSItem; +import me.mrletsplay.mdblog.rss.RSSResponse; import me.mrletsplay.mdblog.template.Template; import me.mrletsplay.mdblog.template.Templates; import me.mrletsplay.mdblog.util.PostPath; @@ -30,6 +34,7 @@ import me.mrletsplay.mrcore.http.HttpUtils; import me.mrletsplay.simplehttpserver.dom.html.HtmlDocument; import me.mrletsplay.simplehttpserver.dom.html.HtmlElement; import me.mrletsplay.simplehttpserver.http.HttpRequestMethod; +import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; import me.mrletsplay.simplehttpserver.http.document.FileDocument; import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; import me.mrletsplay.simplehttpserver.http.server.HttpServer; @@ -40,12 +45,17 @@ public class MdBlog { FILES_PATH = Path.of("files"), POSTS_PATH = FILES_PATH.resolve("posts"); + private static final String + FEED_NAME = "feed.xml", + FEED_CONFIG_NAME = "feed.txt"; + private static HttpServer server; private static WatchService watchService; private static List watchedDirectories; private static Map posts; private static Map indexTemplates; private static Map globalResources; + private static Map feedConfigs; private static Templates defaultTemplates; @@ -56,6 +66,7 @@ public class MdBlog { .create()); server.getDocumentProvider().register(HttpRequestMethod.GET, "/", () -> createPostsIndex(PostPath.root())); + server.getDocumentProvider().registerPattern(HttpRequestMethod.GET, "/{path...}", () -> handleRequest(HttpRequestContext.getCurrentContext())); defaultTemplates = new Templates(null); @@ -74,6 +85,7 @@ public class MdBlog { posts = new HashMap<>(); indexTemplates = new HashMap<>(); globalResources = new HashMap<>(); + feedConfigs = new HashMap<>(); extractAndRegister("style/base.css"); extractAndRegister("style/index.css"); @@ -105,7 +117,7 @@ public class MdBlog { // Generate posts index List allPaths = posts.keySet().stream() - .filter(p -> path == null || (p.startsWith(path) && !p.equals(path))) + .filter(p -> p.startsWith(path) && !p.equals(path)) .map(p -> path == null ? p : p.subPath(path.length())) .toList(); @@ -191,6 +203,25 @@ public class MdBlog { return; } + if(path.getName().equals(FEED_NAME)) { + PostPath blogPath = path.getParent(); + FeedConfig config = feedConfigs.get(blogPath); + if(config != null) { + boolean recursive = ctx.getRequestedPath().getQuery().getFirst("recursive", "false").equals("true"); + + RSSFeed feed = new RSSFeed(config.title(), config.link(), config.description()); + + posts.entrySet().stream() + .filter(e -> e.getKey().startsWith(blogPath) && (recursive || e.getKey().length() == path.length())) + .forEach(e -> { + Post p = e.getValue(); + feed.addItem(new RSSItem(p.getMetadata().title(), p.getMetadata().author(), config.link() + "/" + e.getKey().subPath(blogPath.length()), p.getMetadata().description())); + }); + ctx.respond(HttpStatusCodes.OK_200, new RSSResponse(feed)); + return; + } + } + Post post = posts.get(path); if(post != null) { @@ -247,8 +278,8 @@ public class MdBlog { } private static void updateBlogs() throws IOException { - System.out.println("Update"); indexTemplates.clear(); + feedConfigs.clear(); Files.walk(POSTS_PATH) .filter(Files::isRegularFile) @@ -273,6 +304,18 @@ public class MdBlog { } catch (IOException e) {} }); + Files.walk(POSTS_PATH) + .filter(Files::isRegularFile) + .filter(f -> f.getFileName().toString().equals(FEED_CONFIG_NAME)) + .forEach(f -> { + try { + PostPath path = PostPath.of(POSTS_PATH.relativize(f).getParent()); + String configString = Files.readString(f, StandardCharsets.UTF_8); + FeedConfig config = FeedConfig.load(configString); + feedConfigs.put(path, config); + }catch(IOException e) {} + }); + Iterator> it = posts.entrySet().iterator(); while(it.hasNext()) { Map.Entry en = it.next(); diff --git a/src/main/java/me/mrletsplay/mdblog/rss/FeedConfig.java b/src/main/java/me/mrletsplay/mdblog/rss/FeedConfig.java new file mode 100644 index 0000000..83a8b66 --- /dev/null +++ b/src/main/java/me/mrletsplay/mdblog/rss/FeedConfig.java @@ -0,0 +1,31 @@ +package me.mrletsplay.mdblog.rss; + +public record FeedConfig(String title, String description, String link) { + + public static FeedConfig load(String configString) { + String title = "Untitled Blog"; + String description = "No description"; + String link = "http://localhost"; + + for(String line : configString.split("\n")) { + if(line.isBlank()) continue; + String[] spl = line.split(":", 2); + if(spl.length != 2) { + System.err.println("Invalid config line: " + line); + continue; + } + + String key = spl[0].toLowerCase().trim(); + String value = spl[1].trim(); + + switch(key) { + case "title" -> title = value; + case "description" -> description = value; + case "link" -> link = value; + } + } + + return new FeedConfig(title, description, link); + } + +} diff --git a/src/main/java/me/mrletsplay/mdblog/rss/RSSFeed.java b/src/main/java/me/mrletsplay/mdblog/rss/RSSFeed.java new file mode 100644 index 0000000..891be6e --- /dev/null +++ b/src/main/java/me/mrletsplay/mdblog/rss/RSSFeed.java @@ -0,0 +1,78 @@ +package me.mrletsplay.mdblog.rss; + +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +public class RSSFeed { + + private String title; + private String link; + private String description; + private List items; + + public RSSFeed(String title, String link, String description) { + this.title = title; + this.link = link; + this.description = description; + this.items = new ArrayList<>(); + } + + public void addItem(RSSItem item) { + items.add(item); + } + + public Document toXML() throws ParserConfigurationException { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = dbf.newDocumentBuilder(); + Document doc = builder.newDocument(); + + Element rss = doc.createElement("rss"); + doc.appendChild(rss); + + Element channel = doc.createElement("channel"); + rss.appendChild(channel); + + Element title = doc.createElement("title"); + title.setTextContent(this.title); + channel.appendChild(title); + + Element link = doc.createElement("link"); + link.setTextContent(this.link); + channel.appendChild(link); + + Element description = doc.createElement("description"); + description.setTextContent(this.description); + channel.appendChild(description); + + for(RSSItem item : items) { + Element itemEl = doc.createElement("item"); + channel.appendChild(itemEl); + + Element itTitle = doc.createElement("title"); + itTitle.setTextContent(item.title()); + itemEl.appendChild(itTitle); + + Element itAuthor = doc.createElement("author"); + itAuthor.setTextContent(item.author()); + itemEl.appendChild(itAuthor); + + Element itLink = doc.createElement("link"); + itLink.setTextContent(item.link()); + itemEl.appendChild(itLink); + + Element itDescription = doc.createElement("description"); + itDescription.setTextContent(item.description()); + itemEl.appendChild(itDescription); + } + + return doc; + } + +} diff --git a/src/main/java/me/mrletsplay/mdblog/rss/RSSItem.java b/src/main/java/me/mrletsplay/mdblog/rss/RSSItem.java new file mode 100644 index 0000000..6c1ef22 --- /dev/null +++ b/src/main/java/me/mrletsplay/mdblog/rss/RSSItem.java @@ -0,0 +1,5 @@ +package me.mrletsplay.mdblog.rss; + +public record RSSItem(String title, String author, String link, String description) { + +} diff --git a/src/main/java/me/mrletsplay/mdblog/rss/RSSResponse.java b/src/main/java/me/mrletsplay/mdblog/rss/RSSResponse.java new file mode 100644 index 0000000..f634af4 --- /dev/null +++ b/src/main/java/me/mrletsplay/mdblog/rss/RSSResponse.java @@ -0,0 +1,49 @@ +package me.mrletsplay.mdblog.rss; + +import java.io.ByteArrayOutputStream; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.TransformerFactoryConfigurationError; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.w3c.dom.Document; + +import me.mrletsplay.simplehttpserver.http.response.HttpResponse; +import me.mrletsplay.simplehttpserver.http.util.MimeType; + +public class RSSResponse implements HttpResponse { + + private RSSFeed feed; + + public RSSResponse(RSSFeed feed) { + this.feed = feed; + } + + @Override + public byte[] getContent() { + try { + Document doc = feed.toXML(); + Transformer transform = TransformerFactory.newInstance().newTransformer(); + transform.setOutputProperty(OutputKeys.INDENT, "yes"); + transform.setOutputProperty(OutputKeys.METHOD, "xml"); + transform.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transform.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + transform.transform(new DOMSource(doc), new StreamResult(bOut)); + return bOut.toByteArray(); + } catch (TransformerException | TransformerFactoryConfigurationError | ParserConfigurationException e) { + throw new RuntimeException(e); + } + } + + @Override + public MimeType getContentType() { + return MimeType.of("application/rss+xml"); + } + +}