From c7440bb29056a3a4f127fe918e633773c99c1630 Mon Sep 17 00:00:00 2001
From: Matt Raible
Date: Sat, 14 Nov 2026 13:38:57 -0700
Subject: [PATCH 1/2] Upgrade ROME to 2.1.0
ROME 2.1.0 is the latest release on Maven Central. Its classes target Java 8 (class file version 52), so it runs on Roller's Java 17 baseline. Roller depends only on the core rome artifact, with rome-utils 2.1.0 coming in transitively; the modules ROME 2 removed (rome-fetcher, rome-propono, rome-certiorem) were already gone from Roller.
ROME 2 makes an Atom feed's rel="self" link the SyndFeed link, where 1.x used the first alternate link. Planet stores that link as the subscription's site URL, so Atom subscriptions would have linked to the feed instead of the blog. RomeFeedFetcher now takes the first alternate link from the feed's links and falls back to the feed link, which is still the channel link for RSS.
Nothing else Planet relies on changed: dates are still java.util.Date, rome.properties keeps the same keys and the custom parsers, converters and content module still load, and SyndFeedInput still refuses DOCTYPEs and external entities by default.
RomeFeedFetcherLocalTest serves Atom, RSS 2.0 and RSS 0.91 feeds from a loopback server and pins the site URL, entry dates and time zones, authors, content, categories, the conditional fetch, the Planet plugins registered in rome.properties, and the refusal of a feed with a DOCTYPE. All six tests also pass on ROME 1.19.0 with the previous fetcher, and the site URL check fails on 2.1.0 without the fix.
---
CHANGES.md | 1 +
app/pom.xml | 2 +-
.../business/fetcher/RomeFeedFetcher.java | 17 +-
.../fetcher/RomeFeedFetcherLocalTest.java | 270 ++++++++++++++++++
4 files changed, 288 insertions(+), 2 deletions(-)
create mode 100644 app/src/test/java/org/apache/roller/planet/business/fetcher/RomeFeedFetcherLocalTest.java
diff --git a/CHANGES.md b/CHANGES.md
index 631a2498db..947f582966 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -134,6 +134,7 @@
- The source distribution now extracts to `apache-roller--source/`,
matching its file name, instead of `roller-weblogger--source/`.
+- ROME, the feed library Planet uses, is upgraded from 1.19.0 to 2.1.0.
## 6.1.7
diff --git a/app/pom.xml b/app/pom.xml
index 43c8b51ef8..d8df727321 100644
--- a/app/pom.xml
+++ b/app/pom.xml
@@ -52,7 +52,7 @@ limitations under the License.
3.5.13.6.01.0b3
- 1.19.0
+ 2.1.02.0.207.0.97.1.1
diff --git a/app/src/main/java/org/apache/roller/planet/business/fetcher/RomeFeedFetcher.java b/app/src/main/java/org/apache/roller/planet/business/fetcher/RomeFeedFetcher.java
index 1f38660044..1f43579578 100644
--- a/app/src/main/java/org/apache/roller/planet/business/fetcher/RomeFeedFetcher.java
+++ b/app/src/main/java/org/apache/roller/planet/business/fetcher/RomeFeedFetcher.java
@@ -23,6 +23,7 @@
import com.rometools.rome.feed.synd.SyndContent;
import com.rometools.rome.feed.synd.SyndEntry;
import com.rometools.rome.feed.synd.SyndFeed;
+import com.rometools.rome.feed.synd.SyndLink;
import com.rometools.rome.io.FeedException;
import com.rometools.rome.io.SyndFeedInput;
import com.rometools.rome.io.XmlReader;
@@ -100,7 +101,7 @@ public Subscription fetchSubscription(String feedURL, Date lastModified) throws
// build planet subscription from fetched feed
Subscription newSub = new Subscription();
newSub.setFeedURL(feedURL);
- newSub.setSiteURL(feed.getLink());
+ newSub.setSiteURL(siteURL(feed));
newSub.setTitle(feed.getTitle());
newSub.setAuthor(feed.getAuthor());
newSub.setLastUpdated(feed.getPublishedDate());
@@ -159,6 +160,20 @@ public Subscription fetchSubscription(String feedURL, Date lastModified) throws
}
+ // ROME 2 makes an Atom feed's rel="self" link (the feed itself) its link,
+ // so look for the alternate link to the site first. RSS feeds have no
+ // links list and keep using the channel link.
+ private static String siteURL(SyndFeed feed) {
+ for (SyndLink link : feed.getLinks()) {
+ String rel = link.getRel();
+ if (StringUtils.isBlank(rel) || "alternate".equals(rel)) {
+ return link.getHref();
+ }
+ }
+ return feed.getLink();
+ }
+
+
// build a SubscriptionEntry from Rome SyndEntry and SyndFeed
private SubscriptionEntry buildEntry(SyndEntry romeEntry) {
diff --git a/app/src/test/java/org/apache/roller/planet/business/fetcher/RomeFeedFetcherLocalTest.java b/app/src/test/java/org/apache/roller/planet/business/fetcher/RomeFeedFetcherLocalTest.java
new file mode 100644
index 0000000000..282486c876
--- /dev/null
+++ b/app/src/test/java/org/apache/roller/planet/business/fetcher/RomeFeedFetcherLocalTest.java
@@ -0,0 +1,270 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License. For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+
+package org.apache.roller.planet.business.fetcher;
+
+import com.rometools.rome.feed.synd.SyndEntry;
+import com.rometools.rome.feed.synd.impl.Converters;
+import com.rometools.rome.io.ParsingFeedException;
+import com.rometools.rome.io.SyndFeedInput;
+import com.rometools.rome.io.impl.FeedParsers;
+import com.sun.net.httpserver.HttpServer;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.StringReader;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.roller.planet.pojos.Subscription;
+import org.apache.roller.planet.pojos.SubscriptionEntry;
+import org.apache.roller.planet.util.rome.ContentModule;
+import org.apache.roller.planet.util.rome.PlanetConverterForRSS091N;
+import org.apache.roller.planet.util.rome.PlanetConverterForRSS091U;
+import org.apache.roller.planet.util.rome.PlanetConverterForRSS20;
+import org.apache.roller.planet.util.rome.PlanetRSS091UParser;
+import org.apache.roller.weblogger.util.SafeSAXBuilder;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Fetches feeds from a loopback HTTP server, so the mapping from ROME's model
+ * to Planet's is checked without reaching the network.
+ */
+class RomeFeedFetcherLocalTest {
+
+ private static final String ATOM = """
+
+
+ Atom Blog
+ urn:example:atom
+
+
+ 2026-10-12T08:30:00Z
+
+ urn:example:atom:1
+ Atom entry
+ Entry Author
+
+ 2026-10-10T20:00:00-04:00
+ 2026-10-11T09:15:00+02:00
+
+
+ <p>Atom body</p>
+
+
+ """;
+
+ private static final String RSS_20 = """
+
+
+
+ RSS Blog
+ https://rss.example.org/
+
+ RSS 2.0 feed
+
+ With pubDate
+ https://rss.example.org/1
+ Sat, 10 Oct 2026 20:00:00 -0400
+ Dee Creator
+ planet
+ Summary one
+ Full text one
]]>
+
+
+ With dc:date
+ https://rss.example.org/2
+ 2026-10-09T12:00:00+02:00
+ Summary two
+
+
+
+ """;
+
+ private static final String RSS_091 = """
+
+
+
+ Old Blog
+ https://old.example.org/
+ RSS 0.91 feed
+ en-us
+
+ Old item
+ https://old.example.org/1
+ Old text
+ Fri, 09 Oct 2026 08:00:00 GMT
+
+
+
+ """;
+
+ private static final Map FEEDS = new ConcurrentHashMap<>();
+
+ private static HttpServer server;
+
+ private final RomeFeedFetcher fetcher = new RomeFeedFetcher();
+
+ @BeforeAll
+ static void startServer() throws IOException {
+ server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
+ server.createContext("/", exchange -> {
+ String body = FEEDS.get(exchange.getRequestURI().getPath());
+ byte[] bytes = body == null ? new byte[0] : body.getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().set("Content-Type", "application/xml; charset=utf-8");
+ exchange.sendResponseHeaders(body == null ? 404 : 200, bytes.length == 0 ? -1 : bytes.length);
+ try (OutputStream out = exchange.getResponseBody()) {
+ out.write(bytes);
+ }
+ });
+ server.start();
+ }
+
+ @AfterAll
+ static void stopServer() {
+ server.stop(0);
+ }
+
+ @Test
+ void atomFeedUsesAlternateLinkAsSiteUrl() throws Exception {
+ String url = serve("/atom", ATOM);
+
+ Subscription sub = fetcher.fetchSubscription(url);
+
+ assertEquals(url, sub.getFeedURL());
+ // ROME 2 reports the rel="self" link as the feed link; Planet wants the site
+ assertEquals("https://atom.example.org/", sub.getSiteURL());
+ assertEquals("Atom Blog", sub.getTitle());
+ assertEquals(Instant.parse("2026-10-12T08:30:00Z"), sub.getLastUpdated().toInstant());
+
+ SubscriptionEntry entry = entry(sub, "https://atom.example.org/entry/1");
+ assertEquals("Atom entry", entry.getTitle());
+ assertEquals("Entry Author", entry.getAuthor());
+ assertEquals(timestamp("2026-10-11T00:00:00Z"), entry.getPubTime());
+ assertEquals(timestamp("2026-10-11T07:15:00Z"), entry.getUpdateTime());
+ assertEquals("