Add support for RSS feeds

This commit is contained in:
MrLetsplay 2024-02-18 14:44:31 +01:00
parent 867c972988
commit 8a67093102
Signed by: mr
SSH Key Fingerprint: SHA256:92jBH80vpXyaZHjaIl47pjRq+Yt7XGTArqQg1V7hSqg
5 changed files with 208 additions and 2 deletions

View File

@ -22,6 +22,10 @@ import me.mrletsplay.mdblog.blog.Post;
import me.mrletsplay.mdblog.blog.PostMetadata; import me.mrletsplay.mdblog.blog.PostMetadata;
import me.mrletsplay.mdblog.markdown.MdParser; import me.mrletsplay.mdblog.markdown.MdParser;
import me.mrletsplay.mdblog.markdown.MdRenderer; 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.Template;
import me.mrletsplay.mdblog.template.Templates; import me.mrletsplay.mdblog.template.Templates;
import me.mrletsplay.mdblog.util.PostPath; 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.HtmlDocument;
import me.mrletsplay.simplehttpserver.dom.html.HtmlElement; import me.mrletsplay.simplehttpserver.dom.html.HtmlElement;
import me.mrletsplay.simplehttpserver.http.HttpRequestMethod; import me.mrletsplay.simplehttpserver.http.HttpRequestMethod;
import me.mrletsplay.simplehttpserver.http.HttpStatusCodes;
import me.mrletsplay.simplehttpserver.http.document.FileDocument; import me.mrletsplay.simplehttpserver.http.document.FileDocument;
import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext;
import me.mrletsplay.simplehttpserver.http.server.HttpServer; import me.mrletsplay.simplehttpserver.http.server.HttpServer;
@ -40,12 +45,17 @@ public class MdBlog {
FILES_PATH = Path.of("files"), FILES_PATH = Path.of("files"),
POSTS_PATH = FILES_PATH.resolve("posts"); 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 HttpServer server;
private static WatchService watchService; private static WatchService watchService;
private static List<WatchKey> watchedDirectories; private static List<WatchKey> watchedDirectories;
private static Map<PostPath, Post> posts; private static Map<PostPath, Post> posts;
private static Map<PostPath, Templates> indexTemplates; private static Map<PostPath, Templates> indexTemplates;
private static Map<PostPath, FileDocument> globalResources; private static Map<PostPath, FileDocument> globalResources;
private static Map<PostPath, FeedConfig> feedConfigs;
private static Templates defaultTemplates; private static Templates defaultTemplates;
@ -56,6 +66,7 @@ public class MdBlog {
.create()); .create());
server.getDocumentProvider().register(HttpRequestMethod.GET, "/", () -> createPostsIndex(PostPath.root())); server.getDocumentProvider().register(HttpRequestMethod.GET, "/", () -> createPostsIndex(PostPath.root()));
server.getDocumentProvider().registerPattern(HttpRequestMethod.GET, "/{path...}", () -> handleRequest(HttpRequestContext.getCurrentContext())); server.getDocumentProvider().registerPattern(HttpRequestMethod.GET, "/{path...}", () -> handleRequest(HttpRequestContext.getCurrentContext()));
defaultTemplates = new Templates(null); defaultTemplates = new Templates(null);
@ -74,6 +85,7 @@ public class MdBlog {
posts = new HashMap<>(); posts = new HashMap<>();
indexTemplates = new HashMap<>(); indexTemplates = new HashMap<>();
globalResources = new HashMap<>(); globalResources = new HashMap<>();
feedConfigs = new HashMap<>();
extractAndRegister("style/base.css"); extractAndRegister("style/base.css");
extractAndRegister("style/index.css"); extractAndRegister("style/index.css");
@ -105,7 +117,7 @@ public class MdBlog {
// Generate posts index // Generate posts index
List<PostPath> allPaths = posts.keySet().stream() List<PostPath> 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())) .map(p -> path == null ? p : p.subPath(path.length()))
.toList(); .toList();
@ -191,6 +203,25 @@ public class MdBlog {
return; 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); Post post = posts.get(path);
if(post != null) { if(post != null) {
@ -247,8 +278,8 @@ public class MdBlog {
} }
private static void updateBlogs() throws IOException { private static void updateBlogs() throws IOException {
System.out.println("Update");
indexTemplates.clear(); indexTemplates.clear();
feedConfigs.clear();
Files.walk(POSTS_PATH) Files.walk(POSTS_PATH)
.filter(Files::isRegularFile) .filter(Files::isRegularFile)
@ -273,6 +304,18 @@ public class MdBlog {
} catch (IOException e) {} } 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<Map.Entry<PostPath, Post>> it = posts.entrySet().iterator(); Iterator<Map.Entry<PostPath, Post>> it = posts.entrySet().iterator();
while(it.hasNext()) { while(it.hasNext()) {
Map.Entry<PostPath, Post> en = it.next(); Map.Entry<PostPath, Post> en = it.next();

View File

@ -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);
}
}

View File

@ -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<RSSItem> 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;
}
}

View File

@ -0,0 +1,5 @@
package me.mrletsplay.mdblog.rss;
public record RSSItem(String title, String author, String link, String description) {
}

View File

@ -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");
}
}