ApplyCollectionsTransform.java

/*******************************************************************************
 * Copyright (c) 2025 Eclipse RDF4J contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 *******************************************************************************/
package org.eclipse.rdf4j.queryrender.sparql.ir.util.transform;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.vocabulary.RDF;
import org.eclipse.rdf4j.query.algebra.Var;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrBGP;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrCollection;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrGraph;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrMinus;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrNode;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrOptional;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrPathTriple;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrService;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrStatementPattern;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrSubSelect;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrUnion;

/**
 * Recognize RDF collection encodings (rdf:first/rdf:rest/... rdf:nil) headed by an anonymous collection variable and
 * rewrite them to SPARQL collection syntax in text, e.g., {@code ?s ex:list (1 2 3)}.
 *
 * Details: - Scans the WHERE lines for contiguous rdf:first/rdf:rest chains and records the textual value sequence. -
 * Exposes overrides via the renderer so that the head variable prints as the compact "(item1 item2 ...)" form. -
 * Removes the consumed rdf:first/rest triples from the IR; recursion preserves container structure.
 */
public final class ApplyCollectionsTransform extends BaseTransform {
	private ApplyCollectionsTransform() {
	}

	public static IrBGP apply(IrBGP bgp) {
		if (bgp == null) {
			return null;
		}
		// Collect FIRST/REST triples by subject
		final Map<String, IrStatementPattern> firstByS = new LinkedHashMap<>();
		final Map<String, IrStatementPattern> restByS = new LinkedHashMap<>();
		for (IrNode n : bgp.getLines()) {
			if (!(n instanceof IrStatementPattern)) {
				continue;
			}
			IrStatementPattern sp = (IrStatementPattern) n;
			Var s = sp.getSubject();
			Var p = sp.getPredicate();
			if (s == null || p == null || s.getName() == null || !p.hasValue() || !(p.getValue() instanceof IRI)) {
				continue;
			}
			IRI pred = (IRI) p.getValue();
			if (RDF.FIRST.equals(pred)) {
				firstByS.put(s.getName(), sp);
			} else if (RDF.REST.equals(pred)) {
				restByS.put(s.getName(), sp);
			}
		}
		// Build structural collections and record consumed list triples
		final Map<String, IrCollection> collections = new LinkedHashMap<>();
		final Set<IrNode> consumed = new LinkedHashSet<>();

		for (String head : firstByS.keySet()) {
			if (head == null || (!head.startsWith("_anon_collection_") && !restByS.containsKey(head))) {
				continue;
			}
			List<Var> items = new ArrayList<>();
			Set<String> spine = new LinkedHashSet<>();
			String cur = head;
			int guard = 0;
			boolean ok = true;
			while (true) {
				if (++guard > 10000) {
					ok = false;
					break;
				}
				IrStatementPattern f = firstByS.get(cur);
				IrStatementPattern rSp = restByS.get(cur);
				if (f == null || rSp == null) {
					ok = false;
					break;
				}
				spine.add(cur);
				Var o = f.getObject();
				if (o != null) {
					items.add(o);
				}
				consumed.add(f);
				consumed.add(rSp);
				Var ro = rSp.getObject();
				if (ro == null) {
					ok = false;
					break;
				}
				if (ro.hasValue()) {
					if (!(ro.getValue() instanceof IRI) || !RDF.NIL.equals(ro.getValue())) {
						ok = false;
					}
					break; // end of list
				}
				cur = ro.getName();
				if (cur == null || cur.isEmpty() || spine.contains(cur)) {
					ok = false;
					break;
				}
			}
			if (ok && !items.isEmpty()) {
				IrCollection col = new IrCollection(false);
				for (Var v : items) {
					col.addItem(v);
				}
				collections.put(head, col);
			}
		}
		// Rewrite lines: replace occurrences of the collection head variable with an IrCollection node when used as
		// subject or object in triple/path triples; remove consumed list triples
		List<IrNode> out = new ArrayList<>();
		for (IrNode n : bgp.getLines()) {
			if (consumed.contains(n)) {
				continue;
			}
			if (n instanceof IrStatementPattern) {
				IrStatementPattern sp = (IrStatementPattern) n;
				// Subject replacement if the subject is a collection head
				Var subj = sp.getSubject();
				if (subj != null && !subj.hasValue() && subj.getName() != null
						&& collections.containsKey(subj.getName())) {
					IrCollection col = collections.get(subj.getName());
					sp.setSubjectOverride(col);
				}

				// Object replacement if the object is a collection head
				Var obj = sp.getObject();
				if (obj != null && !obj.hasValue() && obj.getName() != null && collections.containsKey(obj.getName())) {
					IrCollection col = collections.get(obj.getName());
					sp.setObjectOverride(col);
					out.add(sp);
					continue;
				}
			} else if (n instanceof IrPathTriple) {
				IrPathTriple pt = (IrPathTriple) n;
				// Subject replacement for path triple
				Var subj = pt.getSubject();
				if (subj != null && !subj.hasValue() && subj.getName() != null
						&& collections.containsKey(subj.getName())) {
					IrCollection col = collections.get(subj.getName());
					pt.setSubjectOverride(col);
				}
				// Object replacement for path triple
				Var obj = pt.getObject();
				if (obj != null && !obj.hasValue() && obj.getName() != null && collections.containsKey(obj.getName())) {
					IrCollection col = collections.get(obj.getName());
					pt.setObjectOverride(col);
				}
			} else if (n instanceof IrBGP || n instanceof IrGraph || n instanceof IrOptional || n instanceof IrUnion
					|| n instanceof IrMinus || n instanceof IrService || n instanceof IrSubSelect) {
				n = n.transformChildren(child -> {
					if (child instanceof IrBGP) {
						return apply((IrBGP) child);
					}
					return child;
				});
			}
			out.add(n);
		}
		return BaseTransform.bgpWithLines(bgp, out);
	}
}