-
Notifications
You must be signed in to change notification settings - Fork 150
Expand file tree
/
Copy pathUninstallPackage.java
More file actions
383 lines (340 loc) · 15.3 KB
/
UninstallPackage.java
File metadata and controls
383 lines (340 loc) · 15.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
/**
* Copyright 2025 Martynas Jusevičius <martynas@atomgraph.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.atomgraph.linkeddatahub.resource.admin.pkg;
import com.atomgraph.client.util.DataManager;
import com.atomgraph.linkeddatahub.apps.model.AdminApplication;
import com.atomgraph.linkeddatahub.apps.model.EndUserApplication;
import com.atomgraph.linkeddatahub.client.GraphStoreClient;
import com.atomgraph.linkeddatahub.resource.admin.ClearOntology;
import com.atomgraph.linkeddatahub.server.security.AgentContext;
import com.atomgraph.linkeddatahub.server.util.XSLTMasterUpdater;
import static com.atomgraph.server.status.UnprocessableEntityStatus.UNPROCESSABLE_ENTITY;
import jakarta.inject.Inject;
import jakarta.servlet.ServletContext;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.container.ResourceContext;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import org.apache.commons.codec.binary.Hex;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.Resource;
import org.apache.jena.update.UpdateFactory;
import org.apache.jena.update.UpdateRequest;
import org.apache.jena.util.FileManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Optional;
import org.apache.jena.ontology.ConversionException;
/**
* JAX-RS resource that uninstalls a LinkedDataHub package.
* Package uninstallation involves:
* 1. DELETEing package ontology document from ontologies/{hash}/
* 2. Removing owl:imports triple from namespace graph
* 3. Clearing and reloading namespace ontology from cache
* 4. Deleting package stylesheet from /static/{package-path}/
* 5. Regenerating application master stylesheet
*
* @author Martynas Jusevičius {@literal <martynas@atomgraph.com>}
*/
public class UninstallPackage
{
private static final Logger log = LoggerFactory.getLogger(UninstallPackage.class);
private final com.atomgraph.linkeddatahub.apps.model.Application application;
private final com.atomgraph.linkeddatahub.Application system;
private final DataManager dataManager;
private final Optional<AgentContext> agentContext;
@Context ServletContext servletContext;
@Context ResourceContext resourceContext;
/**
* Constructs endpoint.
*
* @param application matched application (admin app)
* @param system system application
* @param dataManager data manager
* @param agentContext authenticated agent context
*/
@Inject
public UninstallPackage(com.atomgraph.linkeddatahub.apps.model.Application application,
com.atomgraph.linkeddatahub.Application system,
DataManager dataManager,
Optional<AgentContext> agentContext)
{
this.application = application;
this.system = system;
this.dataManager = dataManager;
this.agentContext = agentContext;
}
/**
* Uninstalls a package from the current dataspace.
*
* @param packageURI the package URI (e.g., <samp>https://packages.linkeddatahub.com/skos/#this</samp>)
* @param referer the referring URL
* @return JAX-RS response
*/
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response post(@FormParam("package-uri") String packageURI, @HeaderParam("Referer") URI referer)
{
if (packageURI == null)
{
if (log.isErrorEnabled()) log.error("Package URI not specified");
throw new BadRequestException("Package URI not specified");
}
try
{
EndUserApplication endUserApp = getApplication().as(AdminApplication.class).getEndUserApplication();
if (log.isInfoEnabled()) log.info("Uninstalling package: {}", packageURI);
com.atomgraph.linkeddatahub.apps.model.Package pkg = getPackage(packageURI);
if (pkg == null)
{
if (log.isErrorEnabled()) log.error("Loading package failed: {}", packageURI);
throw new WebApplicationException("Loading package failed", UNPROCESSABLE_ENTITY.getStatusCode()); // 422 Unprocessable Entity
}
Resource ontology = pkg.getOntology();
Resource stylesheet = pkg.getStylesheet();
// either ontology or stylesheet need to be specified, or both
if (ontology == null && stylesheet == null)
{
if (log.isErrorEnabled()) log.error("Package ontology and stylesheet are both unspecified for package: {}", packageURI);
throw new WebApplicationException("Package ontology and stylesheet are both unspecified", UNPROCESSABLE_ENTITY.getStatusCode()); // 422 Unprocessable Entity
}
if (ontology != null) uninstallOntology(endUserApp, ontology.getURI());
if (stylesheet != null)
{
String packagePath = pkg.getStylesheetPath();
Path packageDir = Paths.get(getServletContext().getRealPath("/static")).resolve(packagePath);
Path stylesheetFile = packageDir.resolve("layout.xsl");
uninstallStylesheet(stylesheetFile, packagePath, endUserApp);
regenerateMasterStylesheet(endUserApp, pkg);
}
if (log.isInfoEnabled()) log.info("Successfully uninstalled package: {}", packageURI);
URI redirectURI = (referer != null) ? referer : endUserApp.getBaseURI();
return Response.seeOther(redirectURI).build();
}
catch (IOException e)
{
if (log.isErrorEnabled()) log.error("Failed to uninstall package: {}", packageURI, e);
throw new WebApplicationException("Package uninstallation failed", e);
}
}
/**
* Uninstalls ontology by deleting the package ontology document.
*
* @param app the end-user application
* @param packageOntologyURI the package ONTOLOGY URI
* @throws IOException if uninstallation fails
*/
private void uninstallOntology(EndUserApplication app, String packageOntologyURI) throws IOException
{
AdminApplication adminApp = app.getAdminApplication();
String hash;
try
{
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(packageOntologyURI.getBytes(StandardCharsets.UTF_8));
hash = Hex.encodeHexString(md.digest());
if (log.isDebugEnabled()) log.debug("Package ontology URI '{}' hashed to '{}'", packageOntologyURI, hash);
}
catch (NoSuchAlgorithmException e)
{
if (log.isErrorEnabled()) log.error("Failed to hash package ontology URI: {}", packageOntologyURI, e);
throw new IOException("Failed to hash package ontology URI", e);
}
// 3. DELETE package ontology document at ontologies/{hash}/
URI ontologyDocumentURI = UriBuilder.fromUri(adminApp.getBaseURI()).path("ontologies/{hash}/").build(hash);
if (log.isDebugEnabled()) log.debug("DELETEing package ontology document: {}", ontologyDocumentURI);
GraphStoreClient gsc = GraphStoreClient.create(getSystem().getClient(), getSystem().getMediaTypes());
// Delegate agent credentials if authenticated
if (getAgentContext().isPresent())
{
if (log.isDebugEnabled()) log.debug("Delegating agent credentials for DELETE request");
gsc = gsc.delegation(adminApp.getBaseURI(), getAgentContext().get());
}
try (Response deleteResponse = gsc.delete(ontologyDocumentURI))
{
if (!deleteResponse.getStatusInfo().getFamily().equals(Response.Status.Family.SUCCESSFUL))
{
if (log.isErrorEnabled()) log.error("Failed to DELETE package ontology document {}: {}", ontologyDocumentURI, deleteResponse.getStatus());
throw new IOException("Failed to DELETE package ontology document " + ontologyDocumentURI + ": " + deleteResponse.getStatus());
}
if (log.isDebugEnabled()) log.debug("Package ontology DELETE response status: {}", deleteResponse.getStatus());
}
// 4. Remove owl:imports triple from namespace ontology in namespace graph
String namespaceOntologyURI = app.getOntology().getURI();
URI namespaceGraphURI = UriBuilder.fromUri(adminApp.getBaseURI()).path("ontologies/namespace/").build();
if (log.isDebugEnabled()) log.debug("Removing owl:imports from namespace ontology '{}' to package ontology '{}'", namespaceOntologyURI, packageOntologyURI);
String updateString = String.format(
"PREFIX owl: <http://www.w3.org/2002/07/owl#> " +
"DELETE WHERE { <%s> owl:imports <%s> }",
namespaceOntologyURI, packageOntologyURI
);
UpdateRequest updateRequest = UpdateFactory.create(updateString);
try (Response patchResponse = gsc.patch(namespaceGraphURI, updateRequest))
{
if (!patchResponse.getStatusInfo().getFamily().equals(Response.Status.Family.SUCCESSFUL))
{
if (log.isErrorEnabled()) log.error("Failed to PATCH namespace graph {}: {}", namespaceGraphURI, patchResponse.getStatus());
throw new IOException("Failed to PATCH namespace graph " + namespaceGraphURI + ": " + patchResponse.getStatus());
}
if (log.isDebugEnabled()) log.debug("Namespace graph PATCH response status: {}", patchResponse.getStatus());
}
// 5. Clear and reload namespace ontology from cache
if (log.isDebugEnabled()) log.debug("Clearing and reloading namespace ontology '{}'", namespaceOntologyURI);
getResourceContext().getResource(ClearOntology.class).post(namespaceOntologyURI, null);
}
/**
* Deletes stylesheet from <samp>/static/<package-path>/</samp>
*/
private void uninstallStylesheet(Path stylesheetFile, String packagePath, EndUserApplication endUserApp) throws IOException
{
Files.delete(stylesheetFile);
if (log.isDebugEnabled()) log.debug("Deleted package stylesheet: {}", stylesheetFile);
// Purge stylesheet from frontend proxy cache
String stylesheetURL = "/static/" + packagePath + "/layout.xsl";
if (getSystem().getFrontendProxy() != null)
{
if (log.isDebugEnabled()) log.debug("Purging stylesheet from frontend proxy cache: {}", stylesheetURL);
getSystem().ban(getSystem().getFrontendProxy(), stylesheetURL, false);
}
// Delete directory if empty
if (Files.list(stylesheetFile.getParent()).count() == 0)
{
Files.delete(stylesheetFile.getParent());
if (log.isDebugEnabled()) log.debug("Deleted package directory: {}", stylesheetFile.getParent());
}
}
/**
* Regenerates master stylesheet for the application without the uninstalled package.
*
* @param app the application
* @param removedPackage the package being uninstalled
* @throws IOException if regeneration fails
*/
private void regenerateMasterStylesheet(EndUserApplication app, com.atomgraph.linkeddatahub.apps.model.Package removedPackage) throws IOException
{
XSLTMasterUpdater updater = new XSLTMasterUpdater(getServletContext());
updater.removePackageImport(removedPackage.getStylesheetPath());
// Purge master stylesheet from cache
if (getSystem().getFrontendProxy() != null)
{
if (log.isDebugEnabled()) log.debug("Purging master stylesheet from frontend proxy cache: {}", com.atomgraph.linkeddatahub.Application.MASTER_STYLESHEET_PATH);
getSystem().ban(getSystem().getFrontendProxy(), com.atomgraph.linkeddatahub.Application.MASTER_STYLESHEET_PATH, false);
}
}
/**
* Returns the current application.
*
* @return application resource
*/
public com.atomgraph.linkeddatahub.apps.model.Application getApplication()
{
return application;
}
/**
* Returns servlet context.
*
* @return servlet context
*/
public ServletContext getServletContext()
{
return servletContext;
}
/**
* Loads package metadata from its URI using GraphStoreClient.
* Package metadata is expected to be available as Linked Data.
*
* @param packageURI the package URI (e.g., https://packages.linkeddatahub.com/skos/#this)
* @return Package instance, or null if package cannot be loaded
*/
private com.atomgraph.linkeddatahub.apps.model.Package getPackage(String packageURI)
{
if (log.isDebugEnabled()) log.debug("Loading package from: {}", packageURI);
final Model model;
// check if we have the model in the cache first and if yes, return it from there instead making an HTTP request
if (((FileManager)getDataManager()).hasCachedModel(packageURI) ||
(getDataManager().isResolvingMapped() && getDataManager().isMapped(packageURI))) // read mapped URIs (such as system ontologies) from a file
{
if (log.isDebugEnabled()) log.debug("hasCachedModel({}): {}", packageURI, ((FileManager)getDataManager()).hasCachedModel(packageURI));
if (log.isDebugEnabled()) log.debug("isMapped({}): {}", packageURI, getDataManager().isMapped(packageURI));
model = getDataManager().loadModel(packageURI);
}
else
{
GraphStoreClient gsc = GraphStoreClient.create(getSystem().getClient(), getSystem().getMediaTypes());
model = gsc.getModel(packageURI);
}
try
{
return model.getResource(packageURI).as(com.atomgraph.linkeddatahub.apps.model.Package.class);
}
catch (ConversionException ex)
{
return null;
}
}
/**
* Returns the system application.
*
* @return system application
*/
public com.atomgraph.linkeddatahub.Application getSystem()
{
return system;
}
/**
* Returns RDF data manager.
*
* @return RDF data manager
*/
public DataManager getDataManager()
{
return dataManager;
}
/**
* Returns JAX-RS resource context.
*
* @return resource context
*/
public ResourceContext getResourceContext()
{
return resourceContext;
}
/**
* Returns the authenticated agent context.
*
* @return agent context
*/
public Optional<AgentContext> getAgentContext()
{
return agentContext;
}
}