Skip to content

Commit b875af4

Browse files
committed
Purge stylesheet from Varnish during installation
1 parent 34a132f commit b875af4

6 files changed

Lines changed: 147 additions & 151 deletions

File tree

src/main/java/com/atomgraph/linkeddatahub/Application.java

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@
194194
import jakarta.ws.rs.client.Client;
195195
import jakarta.ws.rs.client.ClientBuilder;
196196
import jakarta.ws.rs.client.ClientRequestFilter;
197+
import jakarta.ws.rs.core.Response;
197198
import java.util.concurrent.ConcurrentHashMap;
198199
import javax.xml.parsers.ParserConfigurationException;
199200
import javax.xml.transform.TransformerException;
@@ -241,6 +242,7 @@
241242
import org.glassfish.jersey.process.internal.RequestScoped;
242243
import org.glassfish.jersey.server.ResourceConfig;
243244
import org.glassfish.jersey.server.filter.HttpMethodOverrideFilter;
245+
import org.glassfish.jersey.uri.UriComponent;
244246
import org.xml.sax.SAXException;
245247

246248
/**
@@ -252,9 +254,11 @@
252254
*/
253255
public class Application extends ResourceConfig
254256
{
255-
257+
256258
private static final Logger log = LoggerFactory.getLogger(Application.class);
257259

260+
public static final String MASTER_STYLESHEET_PATH = "/static/xsl/layout.xsl";
261+
258262
private final ExecutorService importThreadPool;
259263
private final ServletConfig servletConfig;
260264
private final EventBus eventBus = new EventBus();
@@ -1747,14 +1751,42 @@ public Client getClient()
17471751
/**
17481752
* Returns the external HTTP client.
17491753
* It is used by the Linked Data browser to avoid sharing the connection pool with the system client.
1750-
*
1754+
*
17511755
* @return client object
17521756
*/
17531757
public Client getExternalClient()
17541758
{
17551759
return externalClient;
17561760
}
17571761

1762+
/**
1763+
* Bans URL from the proxy cache.
1764+
*
1765+
* @param proxy proxy server resource
1766+
* @param url banned URL
1767+
* @param urlEncode if true, the banned URL value will be URL-encoded
1768+
* @throws IllegalArgumentException if url is null
1769+
*/
1770+
public void ban(Resource proxy, String url, boolean urlEncode)
1771+
{
1772+
if (url == null) throw new IllegalArgumentException("URL cannot be null");
1773+
1774+
// Extract path from URL - Varnish req.url only contains the path, not the full URL
1775+
URI uri = URI.create(url);
1776+
String path = uri.getPath();
1777+
if (uri.getQuery() != null) path += "?" + uri.getQuery();
1778+
1779+
final String urlValue = urlEncode ? UriComponent.encode(path, UriComponent.Type.UNRESERVED) : path;
1780+
1781+
try (Response cr = getClient().target(proxy.getURI()).
1782+
request().
1783+
header(CacheInvalidationFilter.HEADER_NAME, urlValue).
1784+
method("BAN", Response.class))
1785+
{
1786+
// Response automatically closed by try-with-resources
1787+
}
1788+
}
1789+
17581790
/**
17591791
* Returns the system base URI.
17601792
*

src/main/java/com/atomgraph/linkeddatahub/apps/model/Package.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,14 @@ public interface Package extends Resource
5151
*/
5252
java.util.Set<Resource> getImportedPackages();
5353

54+
/**
55+
* Returns the filesystem resource path for this package.
56+
* Converts the package URI to a path by reversing hostname components.
57+
* Example: <samp>https://packages.linkeddatahub.com/skos/#this -> com/linkeddatahub/packages/skos</samp>
58+
*
59+
* @return filesystem path relative to static directory
60+
* @throws IllegalArgumentException if package URI is invalid
61+
*/
62+
String getStylesheetPath();
63+
5464
}

src/main/java/com/atomgraph/linkeddatahub/apps/model/impl/PackageImpl.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.apache.jena.rdf.model.StmtIterator;
2828
import org.apache.jena.rdf.model.impl.ResourceImpl;
2929

30+
import java.net.URI;
3031
import java.util.HashSet;
3132
import java.util.Set;
3233

@@ -85,4 +86,44 @@ public Set<Resource> getImportedPackages()
8586
return packages;
8687
}
8788

89+
@Override
90+
public String getStylesheetPath()
91+
{
92+
String uri = getURI();
93+
if (uri == null)
94+
throw new IllegalArgumentException("Package URI cannot be null");
95+
96+
try
97+
{
98+
URI uriObj = URI.create(uri);
99+
String host = uriObj.getHost();
100+
String path = uriObj.getPath();
101+
102+
if (host == null)
103+
throw new IllegalArgumentException("Package URI must have a host: " + uri);
104+
105+
// Reverse hostname components: packages.linkeddatahub.com -> com/linkeddatahub/packages
106+
String[] hostParts = host.split("\\.");
107+
StringBuilder reversedHost = new StringBuilder();
108+
for (int i = hostParts.length - 1; i >= 0; i--)
109+
{
110+
reversedHost.append(hostParts[i]);
111+
if (i > 0) reversedHost.append("/");
112+
}
113+
114+
// Append path without leading/trailing slashes and fragment
115+
if (path != null && !path.isEmpty() && !path.equals("/"))
116+
{
117+
String cleanPath = path.replaceAll("^/+|/+$", ""); // Remove leading/trailing slashes
118+
return reversedHost + "/" + cleanPath;
119+
}
120+
121+
return reversedHost.toString();
122+
}
123+
catch (IllegalArgumentException e)
124+
{
125+
throw new IllegalArgumentException("Invalid package URI: " + uri, e);
126+
}
127+
}
128+
88129
}

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

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import com.atomgraph.linkeddatahub.client.GraphStoreClient;
2424
import com.atomgraph.linkeddatahub.resource.admin.ClearOntology;
2525
import com.atomgraph.linkeddatahub.server.security.AgentContext;
26-
import com.atomgraph.linkeddatahub.server.util.UriPath;
2726
import com.atomgraph.linkeddatahub.server.util.XSLTMasterUpdater;
2827
import static com.atomgraph.server.status.UnprocessableEntityStatus.UNPROCESSABLE_ENTITY;
2928
import jakarta.inject.Inject;
@@ -146,8 +145,6 @@ public Response post(@FormParam("package-uri") String packageURI, @HeaderParam("
146145

147146
try
148147
{
149-
String packagePath = UriPath.convert(packageURI);
150-
151148
if (ontology != null)
152149
{
153150
if (log.isDebugEnabled()) log.debug("Downloading package ontology from: {}", ontology.getURI());
@@ -158,13 +155,15 @@ public Response post(@FormParam("package-uri") String packageURI, @HeaderParam("
158155
if (stylesheet != null)
159156
{
160157
URI stylesheetURI = URI.create(stylesheet.getURI());
158+
String packagePath = pkg.getStylesheetPath();
161159

162160
if (log.isDebugEnabled()) log.debug("Downloading package stylesheet from: {}", stylesheetURI);
163-
String stylesheetContent = downloadStylesheet(stylesheetURI);
164-
installStylesheet(Paths.get(getServletContext().getRealPath("/static")), packagePath, stylesheetContent);
165-
161+
String stylesheetContent = downloadStylesheet(stylesheetURI);
162+
163+
installStylesheet(Paths.get(getServletContext().getRealPath("/static")).resolve(packagePath).resolve("layout.xsl"), packagePath, stylesheetContent, endUserApp);
164+
166165
// 4. Regenerate master stylesheet
167-
regenerateMasterStylesheet(endUserApp, packagePath);
166+
regenerateMasterStylesheet(endUserApp, pkg);
168167
}
169168

170169
//addImportToApplication(endUserApp, packageURI);
@@ -343,38 +342,58 @@ private void installOntology(EndUserApplication app, Model ontologyModel, String
343342
}
344343

345344
/**
346-
* Installs stylesheet to /static/<package-path>/layout.xsl
345+
* Installs stylesheet to <samp>/static/<package-path>/layout.xsl</samp>
347346
*/
348-
private void installStylesheet(Path staticDir, String packagePath, String stylesheetContent) throws IOException
347+
private void installStylesheet(Path stylesheetFile, String packagePath, String stylesheetContent, EndUserApplication endUserApp) throws IOException
349348
{
350-
Path packageDir = staticDir.resolve(packagePath);
351-
Files.createDirectories(packageDir);
352-
353-
Path stylesheetFile = packageDir.resolve("layout.xsl");
349+
Files.createDirectories(stylesheetFile.getParent());
354350
Files.writeString(stylesheetFile, stylesheetContent);
355351

356352
if (log.isDebugEnabled()) log.debug("Installed package stylesheet at: {}", stylesheetFile);
353+
354+
// Purge stylesheet from frontend proxy cache to clear any cached 404 responses
355+
String stylesheetURL = "/static/" + packagePath + "/layout.xsl";
356+
if (endUserApp.getFrontendProxy() != null)
357+
{
358+
if (log.isDebugEnabled()) log.debug("Purging stylesheet from frontend proxy cache: {}", stylesheetURL);
359+
getSystem().ban(endUserApp.getFrontendProxy(), stylesheetURL, false);
360+
}
357361
}
358362

359363
/**
360364
* Regenerates master stylesheet for the application.
365+
*
366+
* @param app the application
367+
* @param newPackage the package being installed
368+
* @throws IOException if regeneration fails
361369
*/
362-
private void regenerateMasterStylesheet(EndUserApplication app, String newPackagePath) throws IOException
370+
private void regenerateMasterStylesheet(EndUserApplication app, com.atomgraph.linkeddatahub.apps.model.Package newPackage) throws IOException
363371
{
364-
// Get all currently installed packages
365-
Set<Resource> packages = app.getImportedPackages();
372+
// Get all currently installed packages and convert to stylesheet paths
373+
Set<Resource> packageResources = app.getImportedPackages();
366374
List<String> packagePaths = new ArrayList<>();
367375

368-
for (Resource pkg : packages)
369-
packagePaths.add(UriPath.convert(pkg.getURI()));
376+
for (Resource pkgRes : packageResources)
377+
{
378+
com.atomgraph.linkeddatahub.apps.model.Package pkg = pkgRes.as(com.atomgraph.linkeddatahub.apps.model.Package.class);
379+
packagePaths.add(pkg.getStylesheetPath());
380+
}
370381

371-
// Add the new package
372-
if (!packagePaths.contains(newPackagePath))
373-
packagePaths.add(newPackagePath);
382+
// Add the new package path
383+
String newPath = newPackage.getStylesheetPath();
384+
if (!packagePaths.contains(newPath))
385+
packagePaths.add(newPath);
374386

375-
// Regenerate master stylesheet
387+
// Regenerate master stylesheet (XSLTMasterUpdater works with paths)
376388
XSLTMasterUpdater updater = new XSLTMasterUpdater(getServletContext());
377389
updater.regenerateMasterStylesheet(packagePaths);
390+
391+
// Purge master stylesheet from cache
392+
if (app.getFrontendProxy() != null)
393+
{
394+
if (log.isDebugEnabled()) log.debug("Purging master stylesheet from frontend proxy cache: {}", com.atomgraph.linkeddatahub.Application.MASTER_STYLESHEET_PATH);
395+
getSystem().ban(app.getFrontendProxy(), com.atomgraph.linkeddatahub.Application.MASTER_STYLESHEET_PATH, false);
396+
}
378397
}
379398

380399
/**

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

Lines changed: 21 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@
2121
import com.atomgraph.linkeddatahub.apps.model.EndUserApplication;
2222
import com.atomgraph.linkeddatahub.client.GraphStoreClient;
2323
import com.atomgraph.linkeddatahub.resource.admin.ClearOntology;
24-
import com.atomgraph.linkeddatahub.server.filter.response.CacheInvalidationFilter;
2524
import com.atomgraph.linkeddatahub.server.security.AgentContext;
26-
import com.atomgraph.linkeddatahub.server.util.UriPath;
2725
import com.atomgraph.linkeddatahub.server.util.XSLTMasterUpdater;
2826
import static com.atomgraph.server.status.UnprocessableEntityStatus.UNPROCESSABLE_ENTITY;
2927
import jakarta.inject.Inject;
@@ -45,7 +43,6 @@
4543
import org.apache.jena.update.UpdateFactory;
4644
import org.apache.jena.update.UpdateRequest;
4745
import org.apache.jena.util.FileManager;
48-
import org.glassfish.jersey.uri.UriComponent;
4946
import org.slf4j.Logger;
5047
import org.slf4j.LoggerFactory;
5148
import java.io.IOException;
@@ -78,8 +75,6 @@ public class UninstallPackage
7875
{
7976
private static final Logger log = LoggerFactory.getLogger(UninstallPackage.class);
8077

81-
public final String MASTER_STYLESHEET_URL = "/static/xsl/layout.xsl";
82-
8378
private final com.atomgraph.linkeddatahub.apps.model.Application application;
8479
private final com.atomgraph.linkeddatahub.Application system;
8580
private final DataManager dataManager;
@@ -111,7 +106,7 @@ public UninstallPackage(com.atomgraph.linkeddatahub.apps.model.Application appli
111106
/**
112107
* Uninstalls a package from the current dataspace.
113108
*
114-
* @param packageURI the package URI (e.g., https://packages.linkeddatahub.com/skos/#this)
109+
* @param packageURI the package URI (e.g., <samp>https://packages.linkeddatahub.com/skos/#this</samp>)
115110
* @param referer the referring URL
116111
* @return JAX-RS response
117112
*/
@@ -147,14 +142,14 @@ public Response post(@FormParam("package-uri") String packageURI, @HeaderParam("
147142
if (log.isErrorEnabled()) log.error("Package ontology and stylesheet are both unspecified for package: {}", packageURI);
148143
throw new WebApplicationException("Package ontology and stylesheet are both unspecified", UNPROCESSABLE_ENTITY.getStatusCode()); // 422 Unprocessable Entity
149144
}
150-
145+
151146
if (ontology != null) uninstallOntology(endUserApp, ontology.getURI());
152147

153148
if (stylesheet != null)
154149
{
155-
String packagePath = UriPath.convert(packageURI);
150+
String packagePath = pkg.getStylesheetPath();
156151
uninstallStylesheet(Paths.get(getServletContext().getRealPath("/static")), packagePath, endUserApp);
157-
regenerateMasterStylesheet(endUserApp, packagePath);
152+
regenerateMasterStylesheet(endUserApp, pkg);
158153
}
159154

160155
//removeImportFromApplication(endUserApp, packageURI);
@@ -264,7 +259,7 @@ private void uninstallStylesheet(Path staticDir, String packagePath, EndUserAppl
264259
if (endUserApp.getFrontendProxy() != null)
265260
{
266261
if (log.isDebugEnabled()) log.debug("Purging stylesheet from frontend proxy cache: {}", stylesheetURL);
267-
ban(endUserApp.getFrontendProxy(), stylesheetURL, false);
262+
getSystem().ban(endUserApp.getFrontendProxy(), stylesheetURL, false);
268263
}
269264

270265
// Delete directory if empty
@@ -277,29 +272,36 @@ private void uninstallStylesheet(Path staticDir, String packagePath, EndUserAppl
277272

278273
/**
279274
* Regenerates master stylesheet for the application without the uninstalled package.
275+
*
276+
* @param app the application
277+
* @param removedPackage the package being uninstalled
278+
* @throws IOException if regeneration fails
280279
*/
281-
private void regenerateMasterStylesheet(EndUserApplication app, String removedPackagePath) throws IOException
280+
private void regenerateMasterStylesheet(EndUserApplication app, com.atomgraph.linkeddatahub.apps.model.Package removedPackage) throws IOException
282281
{
283-
// Get all currently installed packages
284-
Set<Resource> packages = app.getImportedPackages();
282+
// Get all currently installed packages and convert to stylesheet paths
283+
Set<Resource> packageResources = app.getImportedPackages();
285284
List<String> packagePaths = new ArrayList<>();
286285

287-
for (Resource pkg : packages)
286+
String removedPath = removedPackage.getStylesheetPath();
287+
for (Resource pkgRes : packageResources)
288288
{
289-
String pkgPath = UriPath.convert(pkg.getURI());
289+
com.atomgraph.linkeddatahub.apps.model.Package pkg = pkgRes.as(com.atomgraph.linkeddatahub.apps.model.Package.class);
290+
String pkgPath = pkg.getStylesheetPath();
290291
// Exclude the package being removed
291-
if (!pkgPath.equals(removedPackagePath)) packagePaths.add(pkgPath);
292+
if (!pkgPath.equals(removedPath))
293+
packagePaths.add(pkgPath);
292294
}
293295

294-
// Regenerate master stylesheet
296+
// Regenerate master stylesheet (XSLTMasterUpdater works with paths)
295297
XSLTMasterUpdater updater = new XSLTMasterUpdater(getServletContext());
296298
updater.regenerateMasterStylesheet(packagePaths);
297299

298300
// Purge master stylesheet from cache
299301
if (app.getFrontendProxy() != null)
300302
{
301-
if (log.isDebugEnabled()) log.debug("Purging master stylesheet from frontend proxy cache: {}", MASTER_STYLESHEET_URL);
302-
ban(app.getFrontendProxy(), MASTER_STYLESHEET_URL, false);
303+
if (log.isDebugEnabled()) log.debug("Purging master stylesheet from frontend proxy cache: {}", com.atomgraph.linkeddatahub.Application.MASTER_STYLESHEET_PATH);
304+
getSystem().ban(app.getFrontendProxy(), com.atomgraph.linkeddatahub.Application.MASTER_STYLESHEET_PATH, false);
303305
}
304306
}
305307

@@ -374,38 +376,6 @@ private com.atomgraph.linkeddatahub.apps.model.Package getPackage(String package
374376
}
375377
}
376378

377-
protected void ban(Resource proxy, String url)
378-
{
379-
ban(proxy, url, true);
380-
}
381-
382-
/**
383-
* Bans URL from the backend proxy cache.
384-
*
385-
* @param proxy proxy server URL
386-
* @param url banned URL
387-
* @param urlEncode if true, the banned URL value will be URL-encoded
388-
*/
389-
protected void ban(Resource proxy, String url, boolean urlEncode)
390-
{
391-
if (url == null) throw new IllegalArgumentException("Resource cannot be null");
392-
393-
// Extract path from URL - Varnish req.url only contains the path, not the full URL
394-
URI uri = URI.create(url);
395-
String path = uri.getPath();
396-
if (uri.getQuery() != null) path += "?" + uri.getQuery();
397-
398-
final String urlValue = urlEncode ? UriComponent.encode(path, UriComponent.Type.UNRESERVED) : path;
399-
400-
try (Response cr = getSystem().getClient().target(proxy.getURI()).
401-
request().
402-
header(CacheInvalidationFilter.HEADER_NAME, urlValue).
403-
method("BAN", Response.class))
404-
{
405-
// Response automatically closed by try-with-resources
406-
}
407-
}
408-
409379
/**
410380
* Returns the system application.
411381
*

0 commit comments

Comments
 (0)