SyndicationService.java

/*
 * Copyright (c) 2007-2026 MetaSolutions AB
 *
 * Licensed 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.
 */

package org.entrystore.rest.springboot.service;

import com.rometools.rome.feed.synd.SyndFeed;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.solr.client.solrj.request.SolrQuery;
import org.apache.solr.client.solrj.util.ClientUtils;
import org.entrystore.ContextManager;
import org.entrystore.Entry;
import org.entrystore.EntryType;
import org.entrystore.GraphType;
import org.entrystore.impl.RepositoryManagerImpl;
import org.entrystore.repository.util.EntryUtil;
import org.entrystore.repository.util.SolrSearchIndex;
import org.entrystore.rest.springboot.configuration.SyndicationProperties;
import org.entrystore.rest.springboot.model.dto.RenderedFeed;
import org.entrystore.rest.springboot.model.exception.MethodNotAllowedException;
import org.entrystore.rest.springboot.model.exception.NotImplementedException;
import org.entrystore.rest.springboot.util.Syndication;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;

import java.net.URI;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;

@Slf4j
@Service
@RequiredArgsConstructor
public class SyndicationService {

	private static final int DEFAULT_FEED_SIZE = 50;

	private final RepositoryManagerImpl repositoryManager;
	private final SyndicationProperties syndicationProperties;

	@Value("${entrystore.solr.max-limit:" + DEFAULT_FEED_SIZE + "}")
	private int maxFeedSize;

	/**
	 * Renders the feed of a context or list entry as XML; an unknown feed type fails as a bad request. Entries of
	 * any other graph type are rejected with {@link MethodNotAllowedException}, installations without a Solr index
	 * answer {@link NotImplementedException}, and {@code feedSize} is clamped to {@code entrystore.solr.max-limit}.
	 * Item links follow the URL template configured under {@code urlTemplate} (the {@code default} one when null),
	 * else the resource URI; {@code feedTitle} replaces the default title "Feed of <alias>" and is sanitized
	 * like a /search feed title.
	 */
	public RenderedFeed renderFeed(Entry entry, String feedType, String language, int feedSize, String urlTemplate,
								   String feedTitle) {
		SyndFeed feed = getSyndicationFeedSolr(entry, feedType, language, feedSize, urlTemplate, feedTitle);
		String xml = Syndication.convertSyndFeedToXml(feed);
		MediaType mediaType = Objects.requireNonNull(Syndication.convertFeedTypeToMediaType(feed.getFeedType()),
				() -> "No media type for feed type " + feed.getFeedType());
		return new RenderedFeed(xml, mediaType);
	}

	private SyndFeed getSyndicationFeedSolr(Entry entry, String type, String language, int feedSize,
											String urlTemplate, String feedTitle) {

		if (repositoryManager.getIndex() == null) {
			throw new NotImplementedException("Feeds are not supported by this installation");
		}

		if (feedSize > maxFeedSize) {
			feedSize = maxFeedSize;
		} else if (feedSize < 0) {
			// we allow 0 on purpose, this enables requests for the purpose of getting a result count only
			feedSize = DEFAULT_FEED_SIZE;
		}

		GraphType gt = entry.getGraphType();
		if (!GraphType.Context.equals(gt) && !GraphType.List.equals(gt)) {
			throw new MethodNotAllowedException("Entry is not a context or a list");
		}

		List<Entry> recursiveEntries = findSyndicationEntriesInSolr(entry);
		ContextManager cm = repositoryManager.getContextManager();

		String alias;
		if (GraphType.Context.equals(gt)) {
			alias = cm.getName(entry.getResourceURI());
		} else {
			alias = EntryUtil.getTitle(entry, language);
		}

		SyndFeed feed = Syndication.createFeedFromEntries(repositoryManager.getPrincipalManager(),
				syndicationProperties.template(urlTemplate), recursiveEntries, language, feedSize);
		feed.setTitle(Syndication.sanitizeFeedTitle(Objects.requireNonNullElse(feedTitle, "Feed of " + alias)));
		feed.setLink(entry.getResourceURI().toString());
		feed.setFeedType(type);

		return feed;
	}

	private List<Entry> findSyndicationEntriesInSolr(Entry entry) {

		String solrQueryValue = "lists:";
		if (GraphType.Context.equals(entry.getGraphType())) {
			solrQueryValue = "context:";
		}

		solrQueryValue += ClientUtils.escapeQueryChars(entry.getResourceURI().toString());
		SolrQuery solrQuery = new SolrQuery(solrQueryValue);
		solrQuery.setStart(0);
		solrQuery.setRows(1000);
		solrQuery.setSort("modified", SolrQuery.ORDER.desc);

		Set<Entry> searchEntries = ((SolrSearchIndex) repositoryManager.getIndex()).sendQuery(solrQuery).getEntries();
		List<Entry> recursiveEntries = new LinkedList<>();
		for (Entry e : searchEntries) {
			recursiveEntries.addAll(getListChildrenRecursively(e));
		}
		EntryUtil.sortAfterModificationDate(recursiveEntries, false, null);

		return recursiveEntries;
	}

	private Set<Entry> getListChildrenRecursively(Entry listEntry) {
		Set<Entry> result = new HashSet<>();
		if (GraphType.List.equals(listEntry.getGraphType()) && EntryType.Local.equals(listEntry.getEntryType())) {
			org.entrystore.List l = (org.entrystore.List) listEntry.getResource();
			List<URI> c = l.getChildren();
			for (URI uri : c) {
				Entry e = repositoryManager.getContextManager().getEntry(uri);
				if (e != null) {
					if (GraphType.List.equals(e.getGraphType())) {
						result.addAll(getListChildrenRecursively(e));
					} else {
						result.add(e);
					}
				}
			}
		} else {
			result.add(listEntry);
		}
		return result;
	}


}