Skip to content

Commit dae4083

Browse files
namedgraphclaude
andauthored
Fix XSLTMasterUpdater (#275)
* Preserve existing master stylesheet content when installing/uninstalling packages Replace XSLTMasterUpdater.regenerateMasterStylesheet() (which recreated the file from scratch, wiping custom templates and namespace declarations) with focused addPackageImport() and removePackageImport() methods that parse the existing document, mutate only the xsl:import chain, and serialize it back. Update InstallPackage and UninstallPackage call sites accordingly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Avoid duplicate `xsl:import` statements Fixed whitespace Added HTTP test --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3815c40 commit dae4083

4 files changed

Lines changed: 202 additions & 97 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL"
5+
initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL"
6+
purge_cache "$END_USER_VARNISH_SERVICE"
7+
purge_cache "$ADMIN_VARNISH_SERVICE"
8+
purge_cache "$FRONTEND_VARNISH_SERVICE"
9+
10+
# Clean up any leftover package stylesheet files from previous test runs
11+
docker compose exec -T linkeddatahub rm -rf /usr/local/tomcat/webapps/ROOT/static/com/linkeddatahub/packages/skos 2>/dev/null || true
12+
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
13+
14+
# Tomcat caches static files with default cacheTtl=5000ms (5 seconds)
15+
# See: https://tomcat.apache.org/tomcat-10.1-doc/config/resources.html#Attributes
16+
default_ttl=5
17+
18+
# test package URI (SKOS package)
19+
package_uri="https://packages.linkeddatahub.com/skos/#this"
20+
21+
# first install
22+
install-package.sh \
23+
-b "$END_USER_BASE_URL" \
24+
-f "$OWNER_CERT_FILE" \
25+
-p "$OWNER_CERT_PWD" \
26+
--package "$package_uri"
27+
28+
# Wait for Tomcat's static resource cache to expire
29+
sleep $default_ttl
30+
31+
# verify exactly one import after first install
32+
import_count=$(curl -k -s "${END_USER_BASE_URL}static/xsl/layout.xsl" \
33+
| grep -c "com/linkeddatahub/packages/skos/layout.xsl" || true)
34+
if [ "$import_count" -ne 1 ]; then
35+
exit 1
36+
fi
37+
38+
# second install (same package)
39+
install-package.sh \
40+
-b "$END_USER_BASE_URL" \
41+
-f "$OWNER_CERT_FILE" \
42+
-p "$OWNER_CERT_PWD" \
43+
--package "$package_uri"
44+
45+
# Wait for Tomcat's static resource cache to expire
46+
sleep $default_ttl
47+
48+
# verify still exactly one import after second install (deduplication guard)
49+
import_count=$(curl -k -s "${END_USER_BASE_URL}static/xsl/layout.xsl" \
50+
| grep -c "com/linkeddatahub/packages/skos/layout.xsl" || true)
51+
if [ "$import_count" -ne 1 ]; then
52+
exit 1
53+
fi
54+
55+
# cleanup
56+
uninstall-package.sh \
57+
-b "$END_USER_BASE_URL" \
58+
-f "$OWNER_CERT_FILE" \
59+
-p "$OWNER_CERT_PWD" \
60+
--package "$package_uri"

src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/InstallPackage.java

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,7 @@
5454
import java.nio.file.Paths;
5555
import java.security.MessageDigest;
5656
import java.security.NoSuchAlgorithmException;
57-
import java.util.ArrayList;
58-
import java.util.List;
5957
import java.util.Optional;
60-
import java.util.Set;
6158
import org.apache.jena.ontology.ConversionException;
6259
import org.apache.jena.update.UpdateFactory;
6360
import org.apache.jena.update.UpdateRequest;
@@ -419,24 +416,8 @@ private void installStylesheet(Path stylesheetFile, String stylesheetContent) th
419416
*/
420417
private void regenerateMasterStylesheet(EndUserApplication app, com.atomgraph.linkeddatahub.apps.model.Package newPackage) throws IOException
421418
{
422-
// Get all currently installed packages and convert to stylesheet paths
423-
Set<Resource> packageResources = app.getImportedPackages();
424-
List<String> packagePaths = new ArrayList<>();
425-
426-
for (Resource pkgRes : packageResources)
427-
{
428-
com.atomgraph.linkeddatahub.apps.model.Package pkg = pkgRes.as(com.atomgraph.linkeddatahub.apps.model.Package.class);
429-
packagePaths.add(pkg.getStylesheetPath());
430-
}
431-
432-
// Add the new package path
433-
String newPath = newPackage.getStylesheetPath();
434-
if (!packagePaths.contains(newPath))
435-
packagePaths.add(newPath);
436-
437-
// Regenerate master stylesheet (XSLTMasterUpdater works with paths)
438419
XSLTMasterUpdater updater = new XSLTMasterUpdater(getServletContext());
439-
updater.regenerateMasterStylesheet(packagePaths);
420+
updater.addPackageImport(newPackage.getStylesheetPath());
440421
}
441422

442423
/**

src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/UninstallPackage.java

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,7 @@
5353
import java.nio.file.Paths;
5454
import java.security.MessageDigest;
5555
import java.security.NoSuchAlgorithmException;
56-
import java.util.ArrayList;
57-
import java.util.List;
5856
import java.util.Optional;
59-
import java.util.Set;
6057
import org.apache.jena.ontology.ConversionException;
6158

6259
/**
@@ -275,23 +272,8 @@ private void uninstallStylesheet(Path stylesheetFile, String packagePath, EndUse
275272
*/
276273
private void regenerateMasterStylesheet(EndUserApplication app, com.atomgraph.linkeddatahub.apps.model.Package removedPackage) throws IOException
277274
{
278-
// Get all currently installed packages and convert to stylesheet paths
279-
Set<Resource> packageResources = app.getImportedPackages();
280-
List<String> packagePaths = new ArrayList<>();
281-
282-
String removedPath = removedPackage.getStylesheetPath();
283-
for (Resource pkgRes : packageResources)
284-
{
285-
com.atomgraph.linkeddatahub.apps.model.Package pkg = pkgRes.as(com.atomgraph.linkeddatahub.apps.model.Package.class);
286-
String pkgPath = pkg.getStylesheetPath();
287-
// Exclude the package being removed
288-
if (!pkgPath.equals(removedPath))
289-
packagePaths.add(pkgPath);
290-
}
291-
292-
// Regenerate master stylesheet (XSLTMasterUpdater works with paths)
293275
XSLTMasterUpdater updater = new XSLTMasterUpdater(getServletContext());
294-
updater.regenerateMasterStylesheet(packagePaths);
276+
updater.removePackageImport(removedPackage.getStylesheetPath());
295277

296278
// Purge master stylesheet from cache
297279
if (getSystem().getFrontendProxy() != null)

src/main/java/com/atomgraph/linkeddatahub/server/util/XSLTMasterUpdater.java

Lines changed: 140 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import org.slf4j.LoggerFactory;
2121
import org.w3c.dom.Document;
2222
import org.w3c.dom.Element;
23+
import org.w3c.dom.Node;
24+
import org.w3c.dom.NodeList;
2325
import jakarta.servlet.ServletContext;
2426
import javax.xml.parsers.DocumentBuilder;
2527
import javax.xml.parsers.DocumentBuilderFactory;
@@ -29,13 +31,12 @@
2931
import javax.xml.transform.dom.DOMSource;
3032
import javax.xml.transform.stream.StreamResult;
3133
import java.io.IOException;
32-
import java.nio.file.Files;
3334
import java.nio.file.Path;
3435
import java.nio.file.Paths;
35-
import java.util.List;
3636
import javax.xml.parsers.ParserConfigurationException;
3737
import javax.xml.transform.TransformerException;
3838
import org.w3c.dom.DOMException;
39+
import org.xml.sax.SAXException;
3940

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

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

5353
private final ServletContext servletContext;
5454

@@ -63,83 +63,165 @@ public XSLTMasterUpdater(ServletContext servletContext)
6363
}
6464

6565
/**
66-
* Regenerates the master stylesheet for the application.
67-
* Creates a fresh stylesheet with system import followed by package imports.
66+
* Adds a package import to the master stylesheet, preserving all existing content.
67+
* Inserts a new <code>xsl:import</code> after the last existing import element.
6868
*
69-
* @param packagePaths list of package paths to import (e.g., ["com/linkeddatahub/packages/skos"])
69+
* @param packagePath the package path (e.g., "com/linkeddatahub/packages/skos")
7070
* @throws IOException if file operations fail
7171
*/
72-
public void regenerateMasterStylesheet(List<String> packagePaths) throws IOException
72+
public void addPackageImport(String packagePath) throws IOException
7373
{
74-
regenerateMasterStylesheet(getStaticPath().resolve("xsl").resolve("layout.xsl"), packagePaths); // TO-DO: move to configuration
74+
addPackageImport(getStaticPath().resolve("xsl").resolve("layout.xsl"), packagePath);
7575
}
76-
77-
public void regenerateMasterStylesheet(Path masterFile, List<String> packagePaths) throws IOException
76+
77+
/**
78+
* Adds a package import to the specified master stylesheet, preserving all existing content.
79+
*
80+
* @param masterFile path to the master stylesheet
81+
* @param packagePath the package path (e.g., "com/linkeddatahub/packages/skos")
82+
* @throws IOException if file operations fail
83+
*/
84+
public void addPackageImport(Path masterFile, String packagePath) throws IOException
7885
{
7986
try
8087
{
81-
// Create fresh XML document
82-
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
83-
factory.setNamespaceAware(true);
84-
DocumentBuilder builder = factory.newDocumentBuilder();
85-
Document doc = builder.newDocument();
86-
87-
// Create stylesheet root element
88-
Element stylesheet = doc.createElementNS(XSL_NS, "xsl:stylesheet");
89-
stylesheet.setAttribute("version", "3.0");
90-
stylesheet.setAttribute("xmlns:xsl", XSL_NS);
91-
stylesheet.setAttribute("xmlns:xs", "http://www.w3.org/2001/XMLSchema");
92-
stylesheet.setAttribute("exclude-result-prefixes", "xs");
93-
doc.appendChild(stylesheet);
94-
95-
// Add system stylesheet import (lowest priority)
96-
stylesheet.appendChild(doc.createTextNode("\n\n "));
97-
stylesheet.appendChild(doc.createComment("System stylesheet (lowest priority) "));
98-
stylesheet.appendChild(doc.createTextNode("\n "));
99-
Element systemImport = doc.createElementNS(XSL_NS, "xsl:import");
100-
systemImport.setAttribute("href", SYSTEM_STYLESHEET_HREF);
101-
stylesheet.appendChild(systemImport);
102-
103-
// Add package stylesheet imports
104-
if (packagePaths != null && !packagePaths.isEmpty())
88+
Document doc = parseDocument(masterFile);
89+
Element stylesheet = doc.getDocumentElement();
90+
String href = "../" + packagePath + "/layout.xsl";
91+
92+
// Find the last xsl:import child element as insertion anchor, checking for duplicates
93+
Node lastImport = null;
94+
NodeList children = stylesheet.getChildNodes();
95+
for (int i = 0; i < children.getLength(); i++)
10596
{
106-
stylesheet.appendChild(doc.createTextNode("\n\n "));
107-
stylesheet.appendChild(doc.createComment(" Package stylesheets "));
108-
109-
for (String packagePath : packagePaths)
97+
Node child = children.item(i);
98+
if (child.getNodeType() == Node.ELEMENT_NODE
99+
&& XSL_NS.equals(child.getNamespaceURI())
100+
&& "import".equals(child.getLocalName()))
110101
{
111-
stylesheet.appendChild(doc.createTextNode("\n "));
112-
Element importElement = doc.createElementNS(XSL_NS, "xsl:import");
113-
importElement.setAttribute("href", "../" + packagePath + "/layout.xsl");
114-
stylesheet.appendChild(importElement);
102+
if (href.equals(((Element) child).getAttribute("href")))
103+
{
104+
if (log.isWarnEnabled()) log.warn("xsl:import href=\"{}\" already present in master stylesheet, skipping", href);
105+
return;
106+
}
107+
lastImport = child;
108+
}
109+
}
110+
111+
Element newImport = doc.createElementNS(XSL_NS, "xsl:import");
112+
newImport.setAttribute("href", href);
115113

116-
if (log.isDebugEnabled()) log.debug("Added xsl:import for package: {}", packagePath);
114+
if (lastImport != null)
115+
{
116+
// Capture anchor before any insertion — getNextSibling() shifts after insertBefore
117+
Node anchor = lastImport.getNextSibling();
118+
stylesheet.insertBefore(newImport, anchor);
119+
stylesheet.insertBefore(doc.createTextNode("\n "), newImport);
120+
}
121+
else
122+
{
123+
// No existing imports — prepend at start of stylesheet
124+
Node firstChild = stylesheet.getFirstChild();
125+
stylesheet.insertBefore(newImport, firstChild);
126+
stylesheet.insertBefore(doc.createTextNode("\n "), newImport);
127+
}
128+
129+
serializeDocument(doc, masterFile);
130+
131+
if (log.isDebugEnabled()) log.debug("Added xsl:import href=\"{}\" to master stylesheet: {}", href, masterFile);
132+
}
133+
catch (ParserConfigurationException | SAXException | TransformerException | DOMException e)
134+
{
135+
throw new IOException("Failed to add package import to master stylesheet", e);
136+
}
137+
}
138+
139+
/**
140+
* Removes a package import from the master stylesheet, preserving all other content.
141+
*
142+
* @param packagePath the package path (e.g., "com/linkeddatahub/packages/skos")
143+
* @throws IOException if file operations fail
144+
*/
145+
public void removePackageImport(String packagePath) throws IOException
146+
{
147+
removePackageImport(getStaticPath().resolve("xsl").resolve("layout.xsl"), packagePath);
148+
}
149+
150+
/**
151+
* Removes a package import from the specified master stylesheet, preserving all other content.
152+
*
153+
* @param masterFile path to the master stylesheet
154+
* @param packagePath the package path (e.g., "com/linkeddatahub/packages/skos")
155+
* @throws IOException if file operations fail
156+
*/
157+
public void removePackageImport(Path masterFile, String packagePath) throws IOException
158+
{
159+
try
160+
{
161+
Document doc = parseDocument(masterFile);
162+
Element stylesheet = doc.getDocumentElement();
163+
String href = "../" + packagePath + "/layout.xsl";
164+
165+
// Find and remove the matching xsl:import element
166+
Node targetImport = null;
167+
NodeList children = stylesheet.getChildNodes();
168+
for (int i = 0; i < children.getLength(); i++)
169+
{
170+
Node child = children.item(i);
171+
if (child.getNodeType() == Node.ELEMENT_NODE
172+
&& XSL_NS.equals(child.getNamespaceURI())
173+
&& "import".equals(child.getLocalName())
174+
&& href.equals(((Element) child).getAttribute("href")))
175+
{
176+
targetImport = child;
177+
break;
117178
}
118179
}
119180

120-
stylesheet.appendChild(doc.createTextNode("\n\n"));
181+
if (targetImport == null)
182+
{
183+
if (log.isWarnEnabled()) log.warn("xsl:import href=\"{}\" not found in master stylesheet: {}", href, masterFile);
184+
return;
185+
}
186+
187+
// Also remove the preceding text node (whitespace/newline) if present
188+
Node prev = targetImport.getPreviousSibling();
189+
if (prev != null && prev.getNodeType() == Node.TEXT_NODE)
190+
stylesheet.removeChild(prev);
121191

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

131-
DOMSource source = new DOMSource(doc);
132-
StreamResult result = new StreamResult(masterFile.toFile());
133-
transformer.transform(source, result);
194+
serializeDocument(doc, masterFile);
134195

135-
if (log.isDebugEnabled()) log.debug("Regenerated master stylesheet at: {}", masterFile);
196+
if (log.isDebugEnabled()) log.debug("Removed xsl:import href=\"{}\" from master stylesheet: {}", href, masterFile);
136197
}
137-
catch (ParserConfigurationException | TransformerException | DOMException e)
198+
catch (ParserConfigurationException | SAXException | TransformerException | DOMException e)
138199
{
139-
throw new IOException("Failed to regenerate master stylesheet", e);
200+
throw new IOException("Failed to remove package import from master stylesheet", e);
140201
}
141202
}
142203

204+
private Document parseDocument(Path file) throws ParserConfigurationException, SAXException, IOException
205+
{
206+
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
207+
factory.setNamespaceAware(true);
208+
DocumentBuilder builder = factory.newDocumentBuilder();
209+
return builder.parse(file.toFile());
210+
}
211+
212+
private void serializeDocument(Document doc, Path file) throws TransformerException
213+
{
214+
TransformerFactory transformerFactory = TransformerFactory.newInstance();
215+
Transformer transformer = transformerFactory.newTransformer();
216+
transformer.setOutputProperty(OutputKeys.INDENT, "no");
217+
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
218+
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
219+
220+
DOMSource source = new DOMSource(doc);
221+
StreamResult result = new StreamResult(file.toFile());
222+
transformer.transform(source, result);
223+
}
224+
143225
/**
144226
* Gets the path to the webapp's /static/ directory.
145227
*

0 commit comments

Comments
 (0)