2020import org .slf4j .LoggerFactory ;
2121import org .w3c .dom .Document ;
2222import org .w3c .dom .Element ;
23+ import org .w3c .dom .Node ;
24+ import org .w3c .dom .NodeList ;
2325import jakarta .servlet .ServletContext ;
2426import javax .xml .parsers .DocumentBuilder ;
2527import javax .xml .parsers .DocumentBuilderFactory ;
2931import javax .xml .transform .dom .DOMSource ;
3032import javax .xml .transform .stream .StreamResult ;
3133import java .io .IOException ;
32- import java .nio .file .Files ;
3334import java .nio .file .Path ;
3435import java .nio .file .Paths ;
35- import java .util .List ;
3636import javax .xml .parsers .ParserConfigurationException ;
3737import javax .xml .transform .TransformerException ;
3838import 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