Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
set -euo pipefail

initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL"
initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL"
purge_cache "$END_USER_VARNISH_SERVICE"
purge_cache "$ADMIN_VARNISH_SERVICE"
purge_cache "$FRONTEND_VARNISH_SERVICE"

# Clean up any leftover package stylesheet files from previous test runs
docker compose exec -T linkeddatahub rm -rf /usr/local/tomcat/webapps/ROOT/static/com/linkeddatahub/packages/skos 2>/dev/null || true
docker compose exec -T linkeddatahub sed -i '/linkeddatahub\/packages\/skos\/layout.xsl/d' /usr/local/tomcat/webapps/ROOT/static/xsl/layout.xsl 2>/dev/null || true

# Tomcat caches static files with default cacheTtl=5000ms (5 seconds)
# See: https://tomcat.apache.org/tomcat-10.1-doc/config/resources.html#Attributes
default_ttl=5

# test package URI (SKOS package)
package_uri="https://packages.linkeddatahub.com/skos/#this"

# first install
install-package.sh \
-b "$END_USER_BASE_URL" \
-f "$OWNER_CERT_FILE" \
-p "$OWNER_CERT_PWD" \
--package "$package_uri"

# Wait for Tomcat's static resource cache to expire
sleep $default_ttl

# verify exactly one import after first install
import_count=$(curl -k -s "${END_USER_BASE_URL}static/xsl/layout.xsl" \
| grep -c "com/linkeddatahub/packages/skos/layout.xsl" || true)
if [ "$import_count" -ne 1 ]; then
exit 1
fi

# second install (same package)
install-package.sh \
-b "$END_USER_BASE_URL" \
-f "$OWNER_CERT_FILE" \
-p "$OWNER_CERT_PWD" \
--package "$package_uri"

# Wait for Tomcat's static resource cache to expire
sleep $default_ttl

# verify still exactly one import after second install (deduplication guard)
import_count=$(curl -k -s "${END_USER_BASE_URL}static/xsl/layout.xsl" \
| grep -c "com/linkeddatahub/packages/skos/layout.xsl" || true)
if [ "$import_count" -ne 1 ]; then
exit 1
fi

# cleanup
uninstall-package.sh \
-b "$END_USER_BASE_URL" \
-f "$OWNER_CERT_FILE" \
-p "$OWNER_CERT_PWD" \
--package "$package_uri"
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,7 @@
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.apache.jena.ontology.ConversionException;
import org.apache.jena.update.UpdateFactory;
import org.apache.jena.update.UpdateRequest;
Expand Down Expand Up @@ -419,24 +416,8 @@ private void installStylesheet(Path stylesheetFile, String stylesheetContent) th
*/
private void regenerateMasterStylesheet(EndUserApplication app, com.atomgraph.linkeddatahub.apps.model.Package newPackage) throws IOException
{
// Get all currently installed packages and convert to stylesheet paths
Set<Resource> packageResources = app.getImportedPackages();
List<String> packagePaths = new ArrayList<>();

for (Resource pkgRes : packageResources)
{
com.atomgraph.linkeddatahub.apps.model.Package pkg = pkgRes.as(com.atomgraph.linkeddatahub.apps.model.Package.class);
packagePaths.add(pkg.getStylesheetPath());
}

// Add the new package path
String newPath = newPackage.getStylesheetPath();
if (!packagePaths.contains(newPath))
packagePaths.add(newPath);

// Regenerate master stylesheet (XSLTMasterUpdater works with paths)
XSLTMasterUpdater updater = new XSLTMasterUpdater(getServletContext());
updater.regenerateMasterStylesheet(packagePaths);
updater.addPackageImport(newPackage.getStylesheetPath());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,7 @@
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.apache.jena.ontology.ConversionException;

/**
Expand Down Expand Up @@ -275,23 +272,8 @@ private void uninstallStylesheet(Path stylesheetFile, String packagePath, EndUse
*/
private void regenerateMasterStylesheet(EndUserApplication app, com.atomgraph.linkeddatahub.apps.model.Package removedPackage) throws IOException
{
// Get all currently installed packages and convert to stylesheet paths
Set<Resource> packageResources = app.getImportedPackages();
List<String> packagePaths = new ArrayList<>();

String removedPath = removedPackage.getStylesheetPath();
for (Resource pkgRes : packageResources)
{
com.atomgraph.linkeddatahub.apps.model.Package pkg = pkgRes.as(com.atomgraph.linkeddatahub.apps.model.Package.class);
String pkgPath = pkg.getStylesheetPath();
// Exclude the package being removed
if (!pkgPath.equals(removedPath))
packagePaths.add(pkgPath);
}

// Regenerate master stylesheet (XSLTMasterUpdater works with paths)
XSLTMasterUpdater updater = new XSLTMasterUpdater(getServletContext());
updater.regenerateMasterStylesheet(packagePaths);
updater.removePackageImport(removedPackage.getStylesheetPath());

// Purge master stylesheet from cache
if (getSystem().getFrontendProxy() != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import jakarta.servlet.ServletContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
Expand All @@ -29,13 +31,12 @@
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import org.w3c.dom.DOMException;
import org.xml.sax.SAXException;

/**
* Updates master XSLT stylesheets with package import chains.
Expand All @@ -48,7 +49,6 @@ public class XSLTMasterUpdater
private static final Logger log = LoggerFactory.getLogger(XSLTMasterUpdater.class);

private static final String XSL_NS = "http://www.w3.org/1999/XSL/Transform";
private static final String SYSTEM_STYLESHEET_HREF = "../com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/layout.xsl";

private final ServletContext servletContext;

Expand All @@ -63,83 +63,165 @@ public XSLTMasterUpdater(ServletContext servletContext)
}

/**
* Regenerates the master stylesheet for the application.
* Creates a fresh stylesheet with system import followed by package imports.
* Adds a package import to the master stylesheet, preserving all existing content.
* Inserts a new <code>xsl:import</code> after the last existing import element.
*
* @param packagePaths list of package paths to import (e.g., ["com/linkeddatahub/packages/skos"])
* @param packagePath the package path (e.g., "com/linkeddatahub/packages/skos")
* @throws IOException if file operations fail
*/
public void regenerateMasterStylesheet(List<String> packagePaths) throws IOException
public void addPackageImport(String packagePath) throws IOException
{
regenerateMasterStylesheet(getStaticPath().resolve("xsl").resolve("layout.xsl"), packagePaths); // TO-DO: move to configuration
addPackageImport(getStaticPath().resolve("xsl").resolve("layout.xsl"), packagePath);
}

public void regenerateMasterStylesheet(Path masterFile, List<String> packagePaths) throws IOException

/**
* Adds a package import to the specified master stylesheet, preserving all existing content.
*
* @param masterFile path to the master stylesheet
* @param packagePath the package path (e.g., "com/linkeddatahub/packages/skos")
* @throws IOException if file operations fail
*/
public void addPackageImport(Path masterFile, String packagePath) throws IOException
{
try
{
// Create fresh XML document
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.newDocument();

// Create stylesheet root element
Element stylesheet = doc.createElementNS(XSL_NS, "xsl:stylesheet");
stylesheet.setAttribute("version", "3.0");
stylesheet.setAttribute("xmlns:xsl", XSL_NS);
stylesheet.setAttribute("xmlns:xs", "http://www.w3.org/2001/XMLSchema");
stylesheet.setAttribute("exclude-result-prefixes", "xs");
doc.appendChild(stylesheet);

// Add system stylesheet import (lowest priority)
stylesheet.appendChild(doc.createTextNode("\n\n "));
stylesheet.appendChild(doc.createComment("System stylesheet (lowest priority) "));
stylesheet.appendChild(doc.createTextNode("\n "));
Element systemImport = doc.createElementNS(XSL_NS, "xsl:import");
systemImport.setAttribute("href", SYSTEM_STYLESHEET_HREF);
stylesheet.appendChild(systemImport);

// Add package stylesheet imports
if (packagePaths != null && !packagePaths.isEmpty())
Document doc = parseDocument(masterFile);
Element stylesheet = doc.getDocumentElement();
String href = "../" + packagePath + "/layout.xsl";

// Find the last xsl:import child element as insertion anchor, checking for duplicates
Node lastImport = null;
NodeList children = stylesheet.getChildNodes();
for (int i = 0; i < children.getLength(); i++)
{
stylesheet.appendChild(doc.createTextNode("\n\n "));
stylesheet.appendChild(doc.createComment(" Package stylesheets "));

for (String packagePath : packagePaths)
Node child = children.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE
&& XSL_NS.equals(child.getNamespaceURI())
&& "import".equals(child.getLocalName()))
{
stylesheet.appendChild(doc.createTextNode("\n "));
Element importElement = doc.createElementNS(XSL_NS, "xsl:import");
importElement.setAttribute("href", "../" + packagePath + "/layout.xsl");
stylesheet.appendChild(importElement);
if (href.equals(((Element) child).getAttribute("href")))
{
if (log.isWarnEnabled()) log.warn("xsl:import href=\"{}\" already present in master stylesheet, skipping", href);
return;
}
lastImport = child;
}
}

Element newImport = doc.createElementNS(XSL_NS, "xsl:import");
newImport.setAttribute("href", href);

if (log.isDebugEnabled()) log.debug("Added xsl:import for package: {}", packagePath);
if (lastImport != null)
{
// Capture anchor before any insertion — getNextSibling() shifts after insertBefore
Node anchor = lastImport.getNextSibling();
stylesheet.insertBefore(newImport, anchor);
stylesheet.insertBefore(doc.createTextNode("\n "), newImport);
}
else
{
// No existing imports — prepend at start of stylesheet
Node firstChild = stylesheet.getFirstChild();
stylesheet.insertBefore(newImport, firstChild);
stylesheet.insertBefore(doc.createTextNode("\n "), newImport);
}

serializeDocument(doc, masterFile);

if (log.isDebugEnabled()) log.debug("Added xsl:import href=\"{}\" to master stylesheet: {}", href, masterFile);
}
catch (ParserConfigurationException | SAXException | TransformerException | DOMException e)
{
throw new IOException("Failed to add package import to master stylesheet", e);
}
}

/**
* Removes a package import from the master stylesheet, preserving all other content.
*
* @param packagePath the package path (e.g., "com/linkeddatahub/packages/skos")
* @throws IOException if file operations fail
*/
public void removePackageImport(String packagePath) throws IOException
{
removePackageImport(getStaticPath().resolve("xsl").resolve("layout.xsl"), packagePath);
}

/**
* Removes a package import from the specified master stylesheet, preserving all other content.
*
* @param masterFile path to the master stylesheet
* @param packagePath the package path (e.g., "com/linkeddatahub/packages/skos")
* @throws IOException if file operations fail
*/
public void removePackageImport(Path masterFile, String packagePath) throws IOException
{
try
{
Document doc = parseDocument(masterFile);
Element stylesheet = doc.getDocumentElement();
String href = "../" + packagePath + "/layout.xsl";

// Find and remove the matching xsl:import element
Node targetImport = null;
NodeList children = stylesheet.getChildNodes();
for (int i = 0; i < children.getLength(); i++)
{
Node child = children.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE
&& XSL_NS.equals(child.getNamespaceURI())
&& "import".equals(child.getLocalName())
&& href.equals(((Element) child).getAttribute("href")))
{
targetImport = child;
break;
}
}

stylesheet.appendChild(doc.createTextNode("\n\n"));
if (targetImport == null)
{
if (log.isWarnEnabled()) log.warn("xsl:import href=\"{}\" not found in master stylesheet: {}", href, masterFile);
return;
}

// Also remove the preceding text node (whitespace/newline) if present
Node prev = targetImport.getPreviousSibling();
if (prev != null && prev.getNodeType() == Node.TEXT_NODE)
stylesheet.removeChild(prev);

// Write to file
Files.createDirectories(masterFile.getParent());
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
stylesheet.removeChild(targetImport);

DOMSource source = new DOMSource(doc);
StreamResult result = new StreamResult(masterFile.toFile());
transformer.transform(source, result);
serializeDocument(doc, masterFile);

if (log.isDebugEnabled()) log.debug("Regenerated master stylesheet at: {}", masterFile);
if (log.isDebugEnabled()) log.debug("Removed xsl:import href=\"{}\" from master stylesheet: {}", href, masterFile);
}
catch (ParserConfigurationException | TransformerException | DOMException e)
catch (ParserConfigurationException | SAXException | TransformerException | DOMException e)
{
throw new IOException("Failed to regenerate master stylesheet", e);
throw new IOException("Failed to remove package import from master stylesheet", e);
}
}

private Document parseDocument(Path file) throws ParserConfigurationException, SAXException, IOException
{
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(file.toFile());
}

private void serializeDocument(Document doc, Path file) throws TransformerException
{
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "no");
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");

DOMSource source = new DOMSource(doc);
StreamResult result = new StreamResult(file.toFile());
transformer.transform(source, result);
}

/**
* Gets the path to the webapp's /static/ directory.
*
Expand Down
Loading