Add support for RSS feeds
This commit is contained in:
parent
867c972988
commit
8a67093102
@ -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();
|
||||||
|
31
src/main/java/me/mrletsplay/mdblog/rss/FeedConfig.java
Normal file
31
src/main/java/me/mrletsplay/mdblog/rss/FeedConfig.java
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
78
src/main/java/me/mrletsplay/mdblog/rss/RSSFeed.java
Normal file
78
src/main/java/me/mrletsplay/mdblog/rss/RSSFeed.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
5
src/main/java/me/mrletsplay/mdblog/rss/RSSItem.java
Normal file
5
src/main/java/me/mrletsplay/mdblog/rss/RSSItem.java
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package me.mrletsplay.mdblog.rss;
|
||||||
|
|
||||||
|
public record RSSItem(String title, String author, String link, String description) {
|
||||||
|
|
||||||
|
}
|
49
src/main/java/me/mrletsplay/mdblog/rss/RSSResponse.java
Normal file
49
src/main/java/me/mrletsplay/mdblog/rss/RSSResponse.java
Normal 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user