diff --git a/pom.xml b/pom.xml index d745a488e..4000207a9 100644 --- a/pom.xml +++ b/pom.xml @@ -139,13 +139,13 @@ ${project.groupId} client - 4.2.10 + 4.2.11-SNAPSHOT classes ${project.groupId} client - 4.2.10 + 4.2.11-SNAPSHOT war diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/Add.java b/src/main/java/com/atomgraph/linkeddatahub/resource/Add.java index 0a12c036d..9bc6b93ad 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/Add.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/Add.java @@ -19,7 +19,6 @@ import com.atomgraph.core.MediaTypes; import com.atomgraph.core.vocabulary.SD; import com.atomgraph.linkeddatahub.client.GraphStoreClient; -import com.atomgraph.linkeddatahub.model.Service; import com.atomgraph.linkeddatahub.server.security.AgentContext; import java.io.InputStream; import java.io.OutputStream; @@ -27,18 +26,14 @@ import java.util.Optional; import jakarta.inject.Inject; import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.POST; -import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Request; import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.SecurityContext; import jakarta.ws.rs.core.StreamingOutput; import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.ext.Providers; -import org.apache.jena.ontology.Ontology; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ResIterator; import org.apache.jena.rdf.model.Resource; @@ -67,18 +62,13 @@ public class Add * @param request current request * @param uriInfo current URI info * @param mediaTypes supported media types - * @param application matched application - * @param ontology matched application's ontology - * @param service matched application's service * @param providers JAX-RS providers * @param system system application - * @param securityContext JAX-RS security context * @param agentContext authenticated agent's context */ @Inject public Add(@Context Request request, @Context UriInfo uriInfo, MediaTypes mediaTypes, - com.atomgraph.linkeddatahub.apps.model.Application application, Optional ontology, Optional service, - @Context SecurityContext securityContext, Optional agentContext, + Optional agentContext, @Context Providers providers, com.atomgraph.linkeddatahub.Application system) { this.uriInfo = uriInfo; @@ -92,12 +82,10 @@ public Add(@Context Request request, @Context UriInfo uriInfo, MediaTypes mediaT * Expects a model containing a resource with dct:source (source URI) and sd:name (target graph URI) properties. * * @param model the RDF model containing the import parameters - * @param defaultGraph whether to import into the default graph - * @param graphUri the target graph URI * @return JAX-RS response with the imported data */ @POST - public Response post(Model model, @QueryParam("default") @DefaultValue("false") Boolean defaultGraph, @QueryParam("graph") URI graphUri) + public Response post(Model model) { ResIterator it = model.listSubjectsWithProperty(DCTerms.source); try diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/Generate.java b/src/main/java/com/atomgraph/linkeddatahub/resource/Generate.java index f4a1ccea9..716289439 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/Generate.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/Generate.java @@ -16,13 +16,11 @@ */ package com.atomgraph.linkeddatahub.resource; -import com.atomgraph.client.util.DataManager; import com.atomgraph.core.MediaTypes; +import com.atomgraph.linkeddatahub.apps.model.Application; import com.atomgraph.linkeddatahub.client.GraphStoreClient; import com.atomgraph.linkeddatahub.imports.QueryLoader; -import com.atomgraph.linkeddatahub.model.Service; -import com.atomgraph.linkeddatahub.server.filter.response.CacheInvalidationFilter; -import com.atomgraph.linkeddatahub.server.model.impl.GraphStoreImpl; +import com.atomgraph.linkeddatahub.server.model.impl.DirectGraphStoreImpl; import com.atomgraph.linkeddatahub.server.security.AgentContext; import com.atomgraph.linkeddatahub.server.util.Skolemizer; import com.atomgraph.linkeddatahub.vocabulary.LDH; @@ -37,18 +35,15 @@ import java.util.UUID; import jakarta.inject.Inject; import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.InternalServerErrorException; import jakarta.ws.rs.POST; -import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.container.ResourceContext; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Request; import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.SecurityContext; +import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.core.UriBuilder; import jakarta.ws.rs.core.UriInfo; -import jakarta.ws.rs.ext.Providers; -import org.apache.jena.ontology.Ontology; import org.apache.jena.query.ParameterizedSparqlString; import org.apache.jena.query.Query; import org.apache.jena.query.Syntax; @@ -56,10 +51,8 @@ import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.ResIterator; import org.apache.jena.rdf.model.Resource; -import org.apache.jena.rdf.model.ResourceFactory; import org.apache.jena.vocabulary.DCTerms; import org.apache.jena.vocabulary.RDF; -import org.glassfish.jersey.uri.UriComponent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,39 +61,53 @@ * * @author {@literal Martynas Jusevičius } */ -public class Generate extends GraphStoreImpl +public class Generate { private static final Logger log = LoggerFactory.getLogger(Generate.class); + + private final UriInfo uriInfo; + private final MediaTypes mediaTypes; + private final Application application; + private final Optional agentContext; + private final com.atomgraph.linkeddatahub.Application system; + private final ResourceContext resourceContext; /** * Constructs endpoint for container generation. - * + * * @param request current request * @param uriInfo current URI info * @param mediaTypes supported media types * @param application matched application - * @param ontology matched application's ontology - * @param service matched application's service - * @param providers JAX-RS providers * @param system system application - * @param securityContext JAX-RS security context * @param agentContext authenticated agent's context - * @param dataManager RDF data manager + * @param resourceContext resource context for creating resources */ @Inject public Generate(@Context Request request, @Context UriInfo uriInfo, MediaTypes mediaTypes, - com.atomgraph.linkeddatahub.apps.model.Application application, Optional ontology, Optional service, - @Context SecurityContext securityContext, Optional agentContext, - @Context Providers providers, com.atomgraph.linkeddatahub.Application system, - DataManager dataManager) + com.atomgraph.linkeddatahub.apps.model.Application application, Optional agentContext, + com.atomgraph.linkeddatahub.Application system, @Context ResourceContext resourceContext) { - super(request, uriInfo, mediaTypes, application, ontology, service, securityContext, agentContext, providers, system); + this.uriInfo = uriInfo; + this.mediaTypes = mediaTypes; + this.application = application; + this.agentContext = agentContext; + this.system = system; + this.resourceContext = resourceContext; } - + + /** + * Generates containers for given classes. + * Expects a model containing a parent container (sioc:has_parent) and one or more class specifications + * with void:class and spin:query properties. Creates a new container for each class with a view based + * on the provided SPARQL SELECT query. + * + * @param model the RDF model containing the generation parameters + * @return JAX-RS response indicating success or failure + */ @POST - @Override - public Response post(Model model, @QueryParam("default") @DefaultValue("false") Boolean defaultGraph, @QueryParam("graph") URI graphUri) + public Response post(Model model) { ResIterator it = model.listSubjectsWithProperty(SIOC.HAS_PARENT); try @@ -143,9 +150,10 @@ public Response post(Model model, @QueryParam("default") @DefaultValue("false") service))); new Skolemizer(containerGraphURI.toString()).apply(containerModel); - try (Response containerResponse = super.post(containerModel, false, containerGraphURI)) + // append triples directly to the graph store without doing an HTTP request (and thus no ACL check) + try (Response containerResponse = getResourceContext().getResource(DirectGraphStoreImpl.class).post(containerModel, false, containerGraphURI)) { - if (containerResponse.getStatus() != Response.Status.CREATED.getStatusCode()) + if (!containerResponse.getStatusInfo().getFamily().equals(Status.Family.SUCCESSFUL)) { if (log.isErrorEnabled()) log.error("Cannot create container"); throw new InternalServerErrorException("Cannot create container"); @@ -159,10 +167,7 @@ public Response post(Model model, @QueryParam("default") @DefaultValue("false") } // ban the parent container URI from proxy cache to make sure the next query using it will be fresh (e.g. SELECT that loads children) - try (Response response = ban(getApplication().getService().getBackendProxy(), parent.getURI())) - { - // Response automatically closed by try-with-resources - } + getSystem().ban(getApplication().getService().getBackendProxy(), parent.getURI(), true); return Response.ok().build(); } @@ -211,7 +216,7 @@ public Resource createContainer(Model model, URI graphURI, Resource parent, Stri addLiteral(DCTerms.title, title). addLiteral(DH.slug, UUID.randomUUID().toString()). addLiteral(DCTerms.created, Calendar.getInstance()). - addProperty(ResourceFactory.createProperty(RDF.getURI(), "_1"), content); + addProperty(model.createProperty(RDF.getURI(), "_1"), content); // TO-DO: make sure we're creating sequence value larger than the existing ones? } /** @@ -228,20 +233,64 @@ public Resource createView(Model model, Resource query) addProperty(SPIN.query, query); } - /** - * Bans URL from the backend proxy cache. - * - * @param proxy proxy server URL - * @param url banned URL - * @return proxy server response + /** + * Returns the supported media types. + * + * @return media types */ - public Response ban(Resource proxy, String url) + public MediaTypes getMediaTypes() { - if (url == null) throw new IllegalArgumentException("Resource cannot be null"); - - return getSystem().getClient().target(proxy.getURI()).request(). - header(CacheInvalidationFilter.HEADER_NAME, UriComponent.encode(url, UriComponent.Type.UNRESERVED)). // the value has to be URL-encoded in order to match request URLs in Varnish - method("BAN", Response.class); + return mediaTypes; } - + + /** + * Returns the current application. + * + * @return the application + */ + public Application getApplication() + { + return application; + } + + /** + * Returns the current URI info. + * + * @return URI info + */ + public UriInfo getUriInfo() + { + return uriInfo; + } + + /** + * Returns the authenticated agent's context. + * + * @return optional agent context + */ + public Optional getAgentContext() + { + return agentContext; + } + + /** + * Returns the system application. + * + * @return system application + */ + public com.atomgraph.linkeddatahub.Application getSystem() + { + return system; + } + + /** + * Returns the resource context. + * + * @return resource context + */ + public ResourceContext getResourceContext() + { + return resourceContext; + } + } \ No newline at end of file diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/Transform.java b/src/main/java/com/atomgraph/linkeddatahub/resource/Transform.java index cc449de4f..19366b18c 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/Transform.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/Transform.java @@ -16,14 +16,12 @@ */ package com.atomgraph.linkeddatahub.resource; -import com.atomgraph.client.util.DataManager; import com.atomgraph.core.MediaTypes; import com.atomgraph.core.vocabulary.SD; import com.atomgraph.linkeddatahub.client.GraphStoreClient; import com.atomgraph.linkeddatahub.imports.QueryLoader; -import com.atomgraph.linkeddatahub.model.Service; import com.atomgraph.linkeddatahub.server.io.ValidatingModelProvider; -import com.atomgraph.linkeddatahub.server.model.impl.GraphStoreImpl; +import com.atomgraph.linkeddatahub.server.model.impl.DirectGraphStoreImpl; import com.atomgraph.linkeddatahub.server.security.AgentContext; import com.atomgraph.linkeddatahub.vocabulary.NFO; import com.atomgraph.spinrdf.vocabulary.SPIN; @@ -36,20 +34,19 @@ import jakarta.inject.Inject; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotAllowedException; import jakarta.ws.rs.POST; -import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.container.ResourceContext; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Request; import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.SecurityContext; import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.ext.MessageBodyReader; import jakarta.ws.rs.ext.Providers; import org.apache.jena.atlas.RuntimeIOException; -import org.apache.jena.ontology.Ontology; import org.apache.jena.query.Query; import org.apache.jena.query.QueryExecution; import org.apache.jena.query.Syntax; @@ -67,39 +64,69 @@ * * @author {@literal Martynas Jusevičius } */ -public class Transform extends GraphStoreImpl +public class Transform { private static final Logger log = LoggerFactory.getLogger(Transform.class); + private final UriInfo uriInfo; + private final MediaTypes mediaTypes; + private final com.atomgraph.linkeddatahub.apps.model.Application application; + private final Optional agentContext; + private final Providers providers; + private final com.atomgraph.linkeddatahub.Application system; + private final ResourceContext resourceContext; + /** * Constructs endpoint for synchronous RDF data imports. * * @param request current request * @param uriInfo current URI info * @param mediaTypes supported media types - * @param application matched application - * @param ontology matched application's ontology - * @param service matched application's service + * @param application current application * @param providers JAX-RS providers * @param system system application - * @param securityContext JAX-RS security context * @param agentContext authenticated agent's context - * @param dataManager RDF data manager + * @param resourceContext resource context */ @Inject public Transform(@Context Request request, @Context UriInfo uriInfo, MediaTypes mediaTypes, - com.atomgraph.linkeddatahub.apps.model.Application application, Optional ontology, Optional service, - @Context SecurityContext securityContext, Optional agentContext, + com.atomgraph.linkeddatahub.apps.model.Application application, + Optional agentContext, @Context Providers providers, com.atomgraph.linkeddatahub.Application system, - DataManager dataManager) + @Context ResourceContext resourceContext) + { + this.uriInfo = uriInfo; + this.mediaTypes = mediaTypes; + this.application = application; + this.agentContext = agentContext; + this.providers = providers; + this.system = system; + this.resourceContext = resourceContext; + } + + /** + * Rejects GET requests on this endpoint. + * + * @return never returns normally + * @throws NotAllowedException always thrown to indicate GET is not supported + */ + @GET + public Response get() { - super(request, uriInfo, mediaTypes, application, ontology, service, securityContext, agentContext, providers, system); + throw new NotAllowedException("GET is not allowed on this endpoint"); } + /** + * Transforms RDF data from a remote source using a SPARQL CONSTRUCT query and adds it to a target graph. + * Validates URIs to prevent SSRF attacks before processing. + * + * @param model RDF model containing transformation parameters (dct:source, sd:name, spin:query) + * @return HTTP response from forwarding the transformed data to the target graph + * @throws BadRequestException if required parameters are missing or invalid + */ @POST - @Override - public Response post(Model model, @QueryParam("default") @DefaultValue("false") Boolean defaultGraph, @QueryParam("graph") URI graphUri) + public Response post(Model model) { ResIterator it = model.listSubjectsWithProperty(DCTerms.source); try @@ -145,24 +172,24 @@ public Response post(Model model, @QueryParam("default") @DefaultValue("false") * Handles multipart requests with RDF files. * * @param multiPart multipart request object - * @param defaultGraph true if default graph was specified - * @param graphUri graph name * @return response */ @POST @Consumes(MediaType.MULTIPART_FORM_DATA) - public Response postMultipart(FormDataMultiPart multiPart, @QueryParam("default") @DefaultValue("false") Boolean defaultGraph, @QueryParam("graph") URI graphUri) + public Response postMultipart(FormDataMultiPart multiPart) { if (log.isDebugEnabled()) log.debug("MultiPart fields: {} body parts: {}", multiPart.getFields(), multiPart.getBodyParts()); try { - Model model = parseModel(multiPart); // do not skolemize because we don't know the graphUri yet + DirectGraphStoreImpl graphStore = getResourceContext().getResource(DirectGraphStoreImpl.class); + + Model model = graphStore.parseModel(multiPart); // do not skolemize because we don't know the graphUri yet MessageBodyReader reader = getProviders().getMessageBodyReader(Model.class, null, null, com.atomgraph.core.MediaType.APPLICATION_NTRIPLES_TYPE); if (reader instanceof ValidatingModelProvider validatingModelProvider) model = validatingModelProvider.processRead(model); if (log.isDebugEnabled()) log.debug("POSTed Model size: {}", model.size()); - return postFileBodyPart(model, getFileNameBodyPartMap(multiPart)); // do not write the uploaded file -- instead append its triples/quads + return postFileBodyPart(model, graphStore.getFileNameBodyPartMap(multiPart)); // do not write the uploaded file -- instead append its triples/quads } catch (URISyntaxException ex) { @@ -287,4 +314,74 @@ protected static void validateNotInternalURL(URI uri) } } + /** + * Returns the supported media types. + * + * @return media types + */ + public MediaTypes getMediaTypes() + { + return mediaTypes; + } + + /** + * Returns the current application. + * + * @return application resource + */ + public com.atomgraph.linkeddatahub.apps.model.Application getApplication() + { + return application; + } + + /** + * Returns the current URI info. + * + * @return URI info + */ + public UriInfo getUriInfo() + { + return uriInfo; + } + + /** + * Returns the authenticated agent's context. + * + * @return optional agent context + */ + public Optional getAgentContext() + { + return agentContext; + } + + /** + * Returns the registry of JAX-RS providers. + * + * @return JAX-RS providers registry + */ + public Providers getProviders() + { + return providers; + } + + /** + * Returns the system application. + * + * @return system application + */ + public com.atomgraph.linkeddatahub.Application getSystem() + { + return system; + } + + /** + * Returns the JAX-RS resource context. + * + * @return resource context + */ + public ResourceContext getResourceContext() + { + return resourceContext; + } + } diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/acl/Access.java b/src/main/java/com/atomgraph/linkeddatahub/resource/acl/Access.java index 021523308..f22b7378e 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/acl/Access.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/acl/Access.java @@ -18,9 +18,6 @@ import com.atomgraph.client.util.HTMLMediaTypePredicate; import com.atomgraph.core.MediaTypes; -import static com.atomgraph.core.model.SPARQLEndpoint.DEFAULT_GRAPH_URI; -import static com.atomgraph.core.model.SPARQLEndpoint.NAMED_GRAPH_URI; -import static com.atomgraph.core.model.SPARQLEndpoint.QUERY; import com.atomgraph.core.util.ModelUtils; import com.atomgraph.linkeddatahub.apps.model.AdminApplication; import com.atomgraph.linkeddatahub.apps.model.Application; @@ -36,7 +33,6 @@ import jakarta.inject.Inject; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.GET; -import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.EntityTag; import jakarta.ws.rs.core.Request; @@ -47,7 +43,6 @@ import java.util.List; import java.util.Optional; import org.apache.jena.query.ParameterizedSparqlString; -import org.apache.jena.query.Query; import org.apache.jena.query.QuerySolutionMap; import org.apache.jena.query.ResultSetRewindable; import org.apache.jena.rdf.model.Model; @@ -106,14 +101,10 @@ public Access(@Context Request request, @Context UriInfo uriInfo, MediaTypes med /** * Implements the HTTP GET method for retrieving access control information. * - * @param unused SPARQL query parameter (unused) - * @param defaultGraphUris default graph URIs - * @param namedGraphUris named graph URIs * @return response with access control data */ @GET - public Response get(@QueryParam(QUERY) Query unused, - @QueryParam(DEFAULT_GRAPH_URI) List defaultGraphUris, @QueryParam(NAMED_GRAPH_URI) List namedGraphUris) + public Response get() { final Agent agent = getAgentContext().map(AgentContext::getAgent).orElse(null); diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/acl/AccessRequest.java b/src/main/java/com/atomgraph/linkeddatahub/resource/acl/AccessRequest.java index 8e2a60699..859efa01f 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/acl/AccessRequest.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/acl/AccessRequest.java @@ -30,12 +30,10 @@ import com.atomgraph.linkeddatahub.vocabulary.SIOC; import jakarta.inject.Inject; import jakarta.servlet.ServletConfig; -import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.InternalServerErrorException; import jakarta.ws.rs.NotAllowedException; import jakarta.ws.rs.POST; -import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriBuilder; @@ -100,12 +98,10 @@ public AccessRequest(com.atomgraph.linkeddatahub.apps.model.Application applicat /** * Implements the HTTP GET method. * - * @param defaultGraph default graph flag - * @param graphUri graph URI * @return response object */ @GET - public Response get(@QueryParam("default") @DefaultValue("false") Boolean defaultGraph, @QueryParam("graph") URI graphUri) + public Response get() { throw new NotAllowedException("GET is not allowed on this endpoint"); } @@ -114,12 +110,10 @@ public Response get(@QueryParam("default") @DefaultValue("false") Boolean defaul * Implements the HTTP POST method for submitting access requests. * * @param model RDF model with access request data - * @param defaultGraph default graph flag - * @param graphUri graph URI * @return response object */ @POST - public Response post(Model model, @QueryParam("default") @DefaultValue("false") Boolean defaultGraph, @QueryParam("graph") URI graphUri) + public Response post(Model model) { ResIterator it = model.listResourcesWithProperty(RDF.type, ACL.Authorization); try @@ -128,7 +122,7 @@ public Response post(Model model, @QueryParam("default") @DefaultValue("false") { Resource authorization = it.next(); - graphUri = getAuthRequestContainerUriBuilder().path(UUID.randomUUID().toString() + "/").build(); // URI of the new access request graph + URI graphUri = getAuthRequestContainerUriBuilder().path(UUID.randomUUID().toString() + "/").build(); // URI of the new access request graph Model requestModel = ModelFactory.createDefaultModel(); Resource agent = authorization.getPropertyResourceValue(ACL.agent); diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/SignUp.java b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/SignUp.java index c8f65f42a..bb161a509 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/SignUp.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/SignUp.java @@ -26,7 +26,7 @@ import com.atomgraph.linkeddatahub.model.Service; import com.atomgraph.linkeddatahub.listener.EMailListener; import com.atomgraph.linkeddatahub.server.filter.response.CacheInvalidationFilter; -import com.atomgraph.linkeddatahub.server.model.impl.GraphStoreImpl; +import com.atomgraph.linkeddatahub.server.model.impl.DirectGraphStoreImpl; import com.atomgraph.linkeddatahub.server.security.AgentContext; import com.atomgraph.linkeddatahub.server.util.MessageBuilder; import com.atomgraph.linkeddatahub.server.util.Skolemizer; @@ -64,10 +64,7 @@ import jakarta.inject.Inject; import jakarta.mail.MessagingException; import jakarta.servlet.ServletConfig; -import jakarta.ws.rs.DefaultValue; -import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; -import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.InternalServerErrorException; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; @@ -101,7 +98,7 @@ * * @author Martynas Jusevičius {@literal } */ -public class SignUp extends GraphStoreImpl +public class SignUp extends DirectGraphStoreImpl { private static final Logger log = LoggerFactory.getLogger(SignUp.class); @@ -180,16 +177,9 @@ public SignUp(@Context Request request, @Context UriInfo uriInfo, MediaTypes med download = uriInfo.getQueryParameters().containsKey("download"); // debug param that allows downloading the certificate } - @GET - @Override - public Response get(@QueryParam("default") @DefaultValue("false") Boolean defaultGraph, @QueryParam("graph") URI graphUri) - { - return super.get(false, getURI()); - } - @POST @Override - public Response post(Model agentModel, @QueryParam("default") @DefaultValue("false") Boolean defaultGraph, @QueryParam("graph") URI graphUri) + public Response post(Model agentModel) { URI agentGraphUri = getUriInfo().getBaseUriBuilder().path(AGENT_PATH).path("{slug}/").build(UUID.randomUUID().toString()); new Skolemizer(agentGraphUri.toString()).apply(agentModel); @@ -519,16 +509,6 @@ public Service getAgentService() { return getApplication().getService(); } - - /** - * Returns URI of this resource. - * - * @return resource URI - */ - public URI getURI() - { - return getUriInfo().getAbsolutePath(); - } /** * Returns the number of days until the WebID certificate expires. diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/upload/Item.java b/src/main/java/com/atomgraph/linkeddatahub/resource/upload/Item.java index 41876ac2d..90fae8a00 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/upload/Item.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/upload/Item.java @@ -29,29 +29,24 @@ import com.atomgraph.core.MediaTypes; import com.atomgraph.linkeddatahub.model.Service; import com.atomgraph.linkeddatahub.server.io.FileRangeOutput; -import com.atomgraph.linkeddatahub.server.security.AgentContext; -import java.util.ArrayList; -import java.util.Collections; import java.util.Date; import java.util.Optional; import jakarta.annotation.PostConstruct; import jakarta.inject.Inject; -import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotAcceptableException; import jakarta.ws.rs.NotFoundException; -import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.EntityTag; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response.Status; -import jakarta.ws.rs.core.SecurityContext; import jakarta.ws.rs.core.UriInfo; -import org.apache.jena.ontology.Ontology; +import jakarta.ws.rs.core.Variant.VariantListBuilder; import org.apache.jena.query.QueryFactory; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.Resource; +import org.apache.jena.sparql.vocabulary.FOAF; import org.apache.jena.vocabulary.DCTerms; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -61,8 +56,9 @@ * * @author Martynas Jusevičius {@literal } */ -public class Item extends com.atomgraph.linkeddatahub.resource.Graph +public class Item { + private static final Logger log = LoggerFactory.getLogger(Item.class); private static final String ACCEPT_RANGES = "Accept-Ranges"; @@ -71,7 +67,11 @@ public class Item extends com.atomgraph.linkeddatahub.resource.Graph private static final String CONTENT_RANGE = "Content-Range"; private static final int CHUNK_SIZE = 1024 * 1024; // 1MB chunks + private final Request request; + private final UriInfo uriInfo; + private final Service service; private final Resource resource; + private final com.atomgraph.linkeddatahub.Application system; private final HttpHeaders httpHeaders; /** @@ -80,26 +80,24 @@ public class Item extends com.atomgraph.linkeddatahub.resource.Graph * @param request current request * @param uriInfo URI information of the current request * @param mediaTypes a registry of readable/writable media types - * @param application current application - * @param ontology ontology of the current application * @param service SPARQL service of the current application - * @param securityContext JAX-RS security context - * @param agentContext authenticated agent's context * @param providers JAX-RS provider registry * @param system system application * @param httpHeaders request headers */ @Inject public Item(@Context Request request, @Context UriInfo uriInfo, MediaTypes mediaTypes, - com.atomgraph.linkeddatahub.apps.model.Application application, Optional ontology, Optional service, - @Context SecurityContext securityContext, Optional agentContext, + Optional service, @Context Providers providers, com.atomgraph.linkeddatahub.Application system, @Context HttpHeaders httpHeaders) { - super(request, uriInfo, mediaTypes, application, ontology, service, securityContext, agentContext, providers, system); + this.request = request; + this.uriInfo = uriInfo; + this.service = service.get(); this.resource = ModelFactory.createDefaultModel().createResource(uriInfo.getAbsolutePath().toString()); - if (log.isDebugEnabled()) log.debug("Constructing {}", getClass()); + this.system = system; this.httpHeaders = httpHeaders; + if (log.isDebugEnabled()) log.debug("Constructing {}", getClass()); } /** @@ -110,38 +108,52 @@ public void init() { getResource().getModel().add(describe()); } - + + /** + * Handles GET requests for uploaded files. + * Evaluates HTTP preconditions and serves file content with appropriate Content-Security-Policy headers. + * + * @return HTTP response with file content or 304 Not Modified + */ @GET - @Override - public Response get(@QueryParam("default") @DefaultValue("false") Boolean defaultGraph, @QueryParam("graph") URI graphUri) + public Response get() { - return getResponseBuilder(getResource().getModel(), graphUri).build(); + return getResponseBuilder(getResource().getModel(), getURI()).build(); } - - @Override + + /** + * Builds HTTP response for file requests. + * Handles content negotiation, HTTP precondition evaluation (ETag-based caching), + * byte-range requests, and applies Content-Security-Policy headers. + * + * @param model RDF model describing the file + * @param graphUri the graph URI (not used for binary file responses) + * @return response builder configured for file serving + */ public ResponseBuilder getResponseBuilder(Model model, URI graphUri) { // do not pass language list as languages do not apply to binary files - List variants = com.atomgraph.core.model.impl.Response.getVariants(getWritableMediaTypes(Model.class), Collections.emptyList(), getEncodings()); + List variants = VariantListBuilder.newInstance().mediaTypes(getMediaType()).build(); Variant variant = getRequest().selectVariant(variants); - if (variant == null) + if (variant == null || !getMediaType().isCompatible(variant.getMediaType())) { if (log.isTraceEnabled()) log.trace("Requested Variant {} is not on the list of acceptable Response Variants: {}", variant, variants); throw new NotAcceptableException(); } - // respond with file content if Variant is compatible with the File's MediaType. otherwise, send RDF - if (getMediaType().isCompatible(variant.getMediaType())) - { - URI fileURI = getSystem().getUploadRoot().resolve(getUriInfo().getPath()); - File file = new File(fileURI); + EntityTag entityTag = getEntityTag(); + ResponseBuilder rb = getRequest().evaluatePreconditions(entityTag); + if (rb != null) return rb; // file not modified + + URI fileURI = getSystem().getUploadRoot().resolve(getUriInfo().getPath()); + File file = new File(fileURI); - if (!file.exists()) throw new NotFoundException(new FileNotFoundException("File '" + getUriInfo().getPath() + "' not found")); + if (!file.exists()) throw new NotFoundException(new FileNotFoundException("File '" + getUriInfo().getPath() + "' not found")); + + if (getHttpHeaders().getRequestHeaders().containsKey(RANGE)) + { + String range = getHttpHeaders().getHeaderString(RANGE); - if (getHttpHeaders().getRequestHeaders().containsKey(RANGE)) - { - String range = getHttpHeaders().getHeaderString(RANGE); - // if (getHttpHeaders().getRequestHeaders().containsKey(IF_RANGE)) { // String ifRangeHeader = getHttpHeaders().getHeaderString(IF_RANGE); // @@ -157,34 +169,32 @@ public ResponseBuilder getResponseBuilder(Model model, URI graphUri) //// } // } // else - { - FileRangeOutput rangeOutput = getFileRangeOutput(file, range); - final long to = rangeOutput.getLength() + rangeOutput.getFrom(); - String contentRangeValue = String.format("bytes %d-%d/%d", rangeOutput.getFrom(), to - 1, rangeOutput.getFile().length()); - - return super.getResponseBuilder(model, graphUri). - status(Status.PARTIAL_CONTENT). - entity(rangeOutput). - type(variant.getMediaType()). - lastModified(getLastModified(file)). - header(HttpHeaders.CONTENT_LENGTH, rangeOutput.getLength()). // should override Transfer-Encoding: chunked - header(ACCEPT_RANGES, BYTES_RANGE). - header(CONTENT_RANGE, contentRangeValue). - header("Content-Security-Policy", "default-src 'none'; sandbox"); // LNK-011 fix: prevent XSS in uploaded HTML files - } - } + { + FileRangeOutput rangeOutput = getFileRangeOutput(file, range); + final long to = rangeOutput.getLength() + rangeOutput.getFrom(); + String contentRangeValue = String.format("bytes %d-%d/%d", rangeOutput.getFrom(), to - 1, rangeOutput.getFile().length()); - return super.getResponseBuilder(model, graphUri). - entity(file). - type(variant.getMediaType()). - lastModified(getLastModified(file)). - header(HttpHeaders.CONTENT_LENGTH, file.length()). // should override Transfer-Encoding: chunked - header(ACCEPT_RANGES, BYTES_RANGE). - header("Content-Security-Policy", "default-src 'none'; sandbox"); // LNK-011 fix: prevent XSS in uploaded HTML files - //header("Content-Disposition", "attachment; filename=\"" + getRequiredProperty(NFO.fileName).getString() + "\""). + return Response.status(Status.PARTIAL_CONTENT). + entity(rangeOutput). + type(variant.getMediaType()). + tag(entityTag). + lastModified(getLastModified(file)). + header(HttpHeaders.CONTENT_LENGTH, rangeOutput.getLength()). // should override Transfer-Encoding: chunked + header(ACCEPT_RANGES, BYTES_RANGE). + header(CONTENT_RANGE, contentRangeValue). + header("Content-Security-Policy", "default-src 'none'; sandbox"); // LNK-011 fix: prevent XSS in uploaded HTML files + } } - - return super.getResponseBuilder(model, graphUri); + + return Response.ok(). + entity(file). + type(variant.getMediaType()). + tag(entityTag). + lastModified(getLastModified(file)). + header(HttpHeaders.CONTENT_LENGTH, file.length()). // should override Transfer-Encoding: chunked + header(ACCEPT_RANGES, BYTES_RANGE). + header("Content-Security-Policy", "default-src 'none'; sandbox"); // LNK-011 fix: prevent XSS in uploaded HTML files + //header("Content-Disposition", "attachment; filename=\"" + getRequiredProperty(NFO.fileName).getString() + "\""). } /** @@ -237,11 +247,15 @@ public FileRangeOutput getFileRangeOutput(File file, String range) final long length = to - from; return new FileRangeOutput(file, from, length); } - - @Override - public EntityTag getEntityTag(Model model) + + /** + * Returns the ETag for HTTP caching based on the file's SHA1 hash. + * + * @return entity tag for cache validation + */ + public EntityTag getEntityTag() { - return null; // disable ETag based on Model hash + return new EntityTag(getSHA1Hash(getResource())); } /** @@ -271,14 +285,16 @@ public jakarta.ws.rs.core.MediaType getMediaType() return com.atomgraph.linkeddatahub.MediaType.valueOf(format); } - - @Override + + /** + * Returns the list of media types that can be used to write this file's content. + * + * @param clazz the class type (not used, file has single media type) + * @return list containing the file's media type + */ public List getWritableMediaTypes(Class clazz) { - List list = new ArrayList<>(); - list.add(getMediaType()); - - return list; + return List.of(getMediaType()); } /** @@ -293,14 +309,55 @@ public Model describe() } /** - * Returns URI of this file. + * Returns SHA1 property value of the specified resource. * - * @return file URI + * @param resource RDF resource + * @return SHA1 hash string + */ + public String getSHA1Hash(Resource resource) + { + return resource.getRequiredProperty(FOAF.sha1).getString(); + } + + /** + * Returns the absolute URI of this file resource. + * + * @return the file's URI */ public URI getURI() { return getUriInfo().getAbsolutePath(); } + + /** + * Returns the current JAX-RS request. + * + * @return request object + */ + public Request getRequest() + { + return request; + } + + /** + * Returns the URI information of the current request. + * + * @return URI info + */ + public UriInfo getUriInfo() + { + return uriInfo; + } + + /** + * Returns the SPARQL service of the current application. + * + * @return SPARQL service + */ + public Service getService() + { + return service; + } /** * Returns RDF resource of this file. @@ -311,7 +368,17 @@ public Resource getResource() { return resource; } - + + /** + * Returns the system application instance. + * + * @return system application + */ + public com.atomgraph.linkeddatahub.Application getSystem() + { + return system; + } + /** * Returns HTTP headers of the current request. * diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/upload/sha1/Item.java b/src/main/java/com/atomgraph/linkeddatahub/resource/upload/sha1/Item.java deleted file mode 100644 index a0120e520..000000000 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/upload/sha1/Item.java +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright 2019 Martynas Jusevičius - * - * 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.upload.sha1; - -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.Request; -import jakarta.ws.rs.ext.Providers; -import com.atomgraph.core.MediaTypes; -import com.atomgraph.linkeddatahub.model.Service; -import com.atomgraph.linkeddatahub.server.security.AgentContext; -import java.io.File; -import java.util.Date; -import java.util.Optional; -import jakarta.inject.Inject; -import jakarta.ws.rs.core.EntityTag; -import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.SecurityContext; -import jakarta.ws.rs.core.UriInfo; -import org.apache.jena.ontology.Ontology; -import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.Resource; -import org.apache.jena.sparql.vocabulary.FOAF; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * JAX-RS resource that serves content-addressed (using SHA1 hash) file data. - * - * @author Martynas Jusevičius {@literal } - */ -public class Item extends com.atomgraph.linkeddatahub.resource.upload.Item -{ - private static final Logger log = LoggerFactory.getLogger(Item.class); - - /** - * Constructs resource. - * - * @param request current request - * @param uriInfo URI information of the current request - * @param mediaTypes a registry of readable/writable media types - * @param application current application - * @param ontology ontology of the current application - * @param service SPARQL service of the current application - * @param securityContext JAX-RS security context - * @param agentContext authenticated agent's context - * @param providers JAX-RS provider registry - * @param system system application - * @param httpHeaders request headers - */ - @Inject - public Item(@Context Request request, @Context UriInfo uriInfo, MediaTypes mediaTypes, - com.atomgraph.linkeddatahub.apps.model.Application application, Optional ontology, Optional service, - @Context SecurityContext securityContext, Optional agentContext, - @Context Providers providers, com.atomgraph.linkeddatahub.Application system, - @Context HttpHeaders httpHeaders) - { - super(request, uriInfo, mediaTypes, application, ontology, service, securityContext, agentContext, providers, system, httpHeaders); - if (log.isDebugEnabled()) log.debug("Constructing {}", getClass()); - } - - @Override - protected Date getLastModified(File file) - { - return null; // disable Last-Modified because we're using ETag here - } - - @Override - public EntityTag getEntityTag(Model model) - { - return new EntityTag(getSHA1Hash(getResource())); - } - - /** - * Returns SHA1 property value of the specified resource. - * - * @param resource RDF resource - * @return SHA1 hash string - */ - public String getSHA1Hash(Resource resource) - { - return resource.getRequiredProperty(FOAF.sha1).getString(); - } - -} diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/model/Patchable.java b/src/main/java/com/atomgraph/linkeddatahub/server/model/Patchable.java index 773074251..9c60b8c96 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/model/Patchable.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/model/Patchable.java @@ -16,9 +16,7 @@ */ package com.atomgraph.linkeddatahub.server.model; -import java.net.URI; import jakarta.ws.rs.PATCH; -import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Response; import org.apache.jena.update.UpdateRequest; @@ -34,10 +32,9 @@ public interface Patchable * Handles PATCH request.SPARQL update is used as the patch format. * * @param updateRequest SPARQL update - * @param graphUri named graph URI * @return response * @see HTTP PATCH */ - @PATCH Response patch(UpdateRequest updateRequest, @QueryParam("graph") URI graphUri); + @PATCH Response patch(UpdateRequest updateRequest); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/Graph.java b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/DirectGraphStoreImpl.java similarity index 77% rename from src/main/java/com/atomgraph/linkeddatahub/resource/Graph.java rename to src/main/java/com/atomgraph/linkeddatahub/server/model/impl/DirectGraphStoreImpl.java index d170d7f56..e5a73fcd7 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/Graph.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/DirectGraphStoreImpl.java @@ -1,5 +1,5 @@ /** - * Copyright 2021 Martynas Jusevičius + * Copyright 2019 Martynas Jusevičius * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,13 @@ * limitations under the License. * */ -package com.atomgraph.linkeddatahub.resource; +package com.atomgraph.linkeddatahub.server.model.impl; import com.atomgraph.client.util.HTMLMediaTypePredicate; import com.atomgraph.client.vocabulary.AC; import com.atomgraph.core.MediaTypes; import com.atomgraph.core.model.EndpointAccessor; +import com.atomgraph.core.riot.lang.RDFPostReader; import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; import com.atomgraph.linkeddatahub.client.GraphStoreClient; import com.atomgraph.linkeddatahub.model.CSVImport; @@ -27,7 +28,6 @@ import com.atomgraph.linkeddatahub.model.Service; import com.atomgraph.linkeddatahub.server.io.ValidatingModelProvider; import com.atomgraph.linkeddatahub.server.model.Patchable; -import com.atomgraph.linkeddatahub.server.model.impl.GraphStoreImpl; import com.atomgraph.linkeddatahub.server.security.AgentContext; import com.atomgraph.linkeddatahub.server.util.PatchUpdateVisitor; import com.atomgraph.linkeddatahub.server.util.Skolemizer; @@ -38,16 +38,15 @@ import com.atomgraph.linkeddatahub.vocabulary.SIOC; import static com.atomgraph.server.status.UnprocessableEntityStatus.UNPROCESSABLE_ENTITY; import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; +import java.net.URISyntaxException; +import java.security.MessageDigest; import java.util.List; +import java.util.Locale; import java.util.Optional; import jakarta.inject.Inject; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.DefaultValue; -import jakarta.ws.rs.GET; import jakarta.ws.rs.HttpMethod; import jakarta.ws.rs.InternalServerErrorException; import jakarta.ws.rs.NotFoundException; @@ -55,16 +54,15 @@ import jakarta.ws.rs.PATCH; import jakarta.ws.rs.POST; import jakarta.ws.rs.PUT; -import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Request; import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.Response.ResponseBuilder; import static jakarta.ws.rs.core.Response.Status.PERMANENT_REDIRECT; import jakarta.ws.rs.core.SecurityContext; +import jakarta.ws.rs.core.UriBuilder; import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.ext.MessageBodyReader; import jakarta.ws.rs.ext.Providers; @@ -73,15 +71,18 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.net.URISyntaxException; import java.nio.channels.Channels; import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; import java.security.DigestInputStream; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; +import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.Map; import java.util.Set; import org.apache.commons.codec.binary.Hex; @@ -109,45 +110,83 @@ import org.apache.jena.util.iterator.ExtendedIterator; import org.apache.jena.vocabulary.DCTerms; import org.apache.jena.vocabulary.RDF; +import org.glassfish.jersey.media.multipart.BodyPart; import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataMultiPart; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * JAX-RS resource that handles requests to directly-identified named graphs. - * Direct identification is specified in the Graph Store Protocol. + * LinkedDataHub Graph Store implementation. + * We need to subclass the Core class because we're injecting a subclass of Service. * - * @author {@literal Martynas Jusevičius } + * @author Martynas Jusevičius {@literal } */ -public class Graph extends GraphStoreImpl implements Patchable +public class DirectGraphStoreImpl extends com.atomgraph.core.model.impl.DirectGraphStoreImpl implements Patchable { - private static final Logger log = LoggerFactory.getLogger(Graph.class); + private static final Logger log = LoggerFactory.getLogger(DirectGraphStoreImpl.class); - private final Set allowedMethods; + /** + * The relative path of the content-addressed file container. + */ + public static final String UPLOADS_PATH = "uploads"; + private final com.atomgraph.linkeddatahub.apps.model.Application application; + private final Ontology ontology; + private final Service service; + private final Providers providers; + private final com.atomgraph.linkeddatahub.Application system; + private final UriBuilder uploadsUriBuilder; + private final MessageDigest messageDigest; + /** The URIs for owner and secretary documents. */ + protected final URI ownerDocURI, secretaryDocURI; + private final SecurityContext securityContext; + private final Optional agentContext; + private final Set allowedMethods; + /** - * Constructs resource. + * Constructs Graph Store. * * @param request current request - * @param uriInfo URI information of the current request + * @param uriInfo URI info of the current request * @param mediaTypes a registry of readable/writable media types * @param application current application * @param ontology ontology of the current application * @param service SPARQL service of the current application * @param securityContext JAX-RS security context * @param agentContext authenticated agent's context - * @param providers JAX-RS provider registry + * @param providers registry of JAX-RS providers * @param system system application */ @Inject - public Graph(@Context Request request, @Context UriInfo uriInfo, MediaTypes mediaTypes, + public DirectGraphStoreImpl(@Context Request request, @Context UriInfo uriInfo, MediaTypes mediaTypes, com.atomgraph.linkeddatahub.apps.model.Application application, Optional ontology, Optional service, @Context SecurityContext securityContext, Optional agentContext, @Context Providers providers, com.atomgraph.linkeddatahub.Application system) { - super(request, uriInfo, mediaTypes, application, ontology, service, securityContext, agentContext, providers, system); + super(request, service.get(), mediaTypes, uriInfo); + if (ontology.isEmpty()) throw new InternalServerErrorException("Ontology is not specified"); + if (service.isEmpty()) throw new InternalServerErrorException("Service is not specified"); + this.application = application; + this.ontology = ontology.get(); + this.service = service.get(); + this.securityContext = securityContext; + this.agentContext = agentContext; + this.providers = providers; + this.system = system; + this.messageDigest = system.getMessageDigest(); + uploadsUriBuilder = uriInfo.getBaseUriBuilder().path(UPLOADS_PATH); + URI ownerURI = URI.create(application.getMaker().getURI()); + try + { + this.ownerDocURI = new URI(ownerURI.getScheme(), ownerURI.getSchemeSpecificPart(), null).normalize(); + this.secretaryDocURI = new URI(system.getSecretaryWebIDURI().getScheme(), system.getSecretaryWebIDURI().getSchemeSpecificPart(), null).normalize(); + } + catch (URISyntaxException ex) + { + throw new InternalServerErrorException(ex); + } URI uri = uriInfo.getAbsolutePath(); allowedMethods = new HashSet<>(); @@ -163,23 +202,23 @@ public Graph(@Context Request request, @Context UriInfo uriInfo, MediaTypes medi !secretaryDocURI.equals(uri)) allowedMethods.add(HttpMethod.DELETE); } - - @Override - @GET - public Response get(@QueryParam("default") @DefaultValue("false") Boolean defaultGraph, @QueryParam("graph") URI graphUriUnused) - { - return super.get(false, getURI()); - } + /** + * Implements POST method of SPARQL Graph Store Protocol. + * Adds triples to the existing graph, skolemizes blank nodes, updates modification timestamp, and submits any imports. + * + * @param model RDF model to add to the graph + * @return HTTP response with updated entity tag + */ @Override @POST - public Response post(Model model, @QueryParam("default") @DefaultValue("false") Boolean defaultGraph, @QueryParam("graph") URI graphUriUnused) + public Response post(Model model) { if (log.isTraceEnabled()) log.trace("POST Graph Store request with RDF payload: {} payload size(): {}", model, model.size()); final Model existingModel = getService().getGraphStoreClient().getModel(getURI().toString()); - ResponseBuilder rb = evaluatePreconditions(existingModel); + Response.ResponseBuilder rb = evaluatePreconditions(existingModel); if (rb != null) return rb.build(); // preconditions not met model.createResource(getURI().toString()). @@ -203,10 +242,18 @@ public Response post(Model model, @QueryParam("default") @DefaultValue("false") build(); } + /** + * Implements PUT method of SPARQL Graph Store Protocol. + * Creates a new graph or updates an existing one. Enforces trailing slash in URIs, skolemizes blank nodes, + * establishes parent/container relationships, and manages metadata (created, modified, creator, owner timestamps). + * + * @param model RDF model to create or update + * @return HTTP response with 201 Created for new graphs or 200 OK for updates + */ @Override @PUT // the AuthorizationFilter only allows creating new child URIs for existing containers (i.e. there has to be a .. container already) - public Response put(Model model, @QueryParam("default") @DefaultValue("false") Boolean defaultGraph, @QueryParam("graph") URI graphUriUnused) + public Response put(Model model) { if (log.isTraceEnabled()) log.trace("PUT Graph Store request with RDF payload: {} payload size(): {}", model, model.size()); @@ -239,7 +286,7 @@ public Response put(Model model, @QueryParam("default") @DefaultValue("false") B { existingModel = getService().getGraphStoreClient().getModel(getURI().toString()); - ResponseBuilder rb = evaluatePreconditions(existingModel); + Response.ResponseBuilder rb = evaluatePreconditions(existingModel); if (rb != null) return rb.build(); // preconditions not met } catch (NotFoundException ex) @@ -311,12 +358,11 @@ public Response put(Model model, @QueryParam("default") @DefaultValue("false") B * The GRAPH keyword is therefore not allowed in the update string. * * @param updateRequest SPARQL update - * @param graphUriUnused named graph URI (unused) * @return response response object */ @PATCH @Override - public Response patch(UpdateRequest updateRequest, @QueryParam("graph") URI graphUriUnused) + public Response patch(UpdateRequest updateRequest) { if (updateRequest == null) throw new BadRequestException("SPARQL update not specified"); if (log.isDebugEnabled()) log.debug("PATCH request on named graph with URI: {}", getURI()); @@ -343,7 +389,7 @@ public Response patch(UpdateRequest updateRequest, @QueryParam("graph") URI grap final Model existingModel = getService().getGraphStoreClient().getModel(getURI().toString()); if (existingModel == null) throw new NotFoundException("Named graph with URI <" + getURI() + "> not found"); - ResponseBuilder rb = evaluatePreconditions(existingModel); + Response.ResponseBuilder rb = evaluatePreconditions(existingModel); if (rb != null) return rb.build(); // preconditions not met Model beforeUpdateModel = ModelFactory.createDefaultModel().add(existingModel); @@ -358,7 +404,7 @@ public Response patch(UpdateRequest updateRequest, @QueryParam("graph") URI grap changedModel.add(existingModel.listStatements(resource, null, (RDFNode) null)); // if PATCH results in an empty model, treat it as a DELETE request - if (changedModel.isEmpty()) return delete(Boolean.FALSE, getURI()); + if (changedModel.isEmpty()) return delete(); validate(changedModel); // this would normally be done transparently by the ValidatingModelProvider put(dataset.getDefaultModel(), Boolean.FALSE, getURI()); @@ -371,32 +417,6 @@ public Response patch(UpdateRequest updateRequest, @QueryParam("graph") URI grap build(); } - /** - * Gets a diff of triples between two models and returns a set of their subject resources. - * - * @param beforeUpdateModel model before the update - * @param afterUpdateModel model after the update - * @return set of changed resources - */ - public Set getChangedResources(Model beforeUpdateModel, Model afterUpdateModel) - { - if (beforeUpdateModel == null) throw new IllegalArgumentException("Model before update cannot be null"); - if (afterUpdateModel == null) throw new IllegalArgumentException("Model after update cannot be null"); - - Model addedTriples = afterUpdateModel.difference(beforeUpdateModel); - Model removedTriples = beforeUpdateModel.difference(afterUpdateModel); - - Set changedResources = new HashSet<>(); - addedTriples.listStatements().forEachRemaining(statement -> { - changedResources.add(statement.getSubject()); - }); - removedTriples.listStatements().forEachRemaining(statement -> { - changedResources.add(statement.getSubject()); - }); - - return changedResources; - } - /** * Overrides OPTIONS HTTP header values.Specifies allowed methods. * @@ -420,13 +440,11 @@ public Response options() * Files are written to storage before the RDF data is passed to the default POST handler method. * * @param multiPart multipart form data - * @param defaultGraph true if default graph is requested - * @param graphUriUnused named graph URI (unused) * @return HTTP response */ @POST @Consumes(MediaType.MULTIPART_FORM_DATA) - public Response postMultipart(FormDataMultiPart multiPart, @QueryParam("default") @DefaultValue("false") Boolean defaultGraph, @QueryParam("graph") URI graphUriUnused) + public Response postMultipart(FormDataMultiPart multiPart) { if (log.isDebugEnabled()) log.debug("MultiPart fields: {} body parts: {}", multiPart.getFields(), multiPart.getBodyParts()); @@ -464,13 +482,11 @@ public Response postMultipart(FormDataMultiPart multiPart, @QueryParam("default" * Files are written to storage before the RDF data is passed to the default PUT handler method. * * @param multiPart multipart form data - * @param defaultGraph true if default graph is requested - * @param graphUriUnused named graph URI (unused) * @return HTTP response */ @PUT @Consumes(MediaType.MULTIPART_FORM_DATA) - public Response putMultipart(FormDataMultiPart multiPart, @QueryParam("default") @DefaultValue("false") Boolean defaultGraph, @QueryParam("graph") URI graphUriUnused) + public Response putMultipart(FormDataMultiPart multiPart) { if (log.isDebugEnabled()) log.debug("MultiPart fields: {} body parts: {}", multiPart.getFields(), multiPart.getBodyParts()); @@ -486,7 +502,7 @@ public Response putMultipart(FormDataMultiPart multiPart, @QueryParam("default") int fileCount = writeFiles(model, getFileNameBodyPartMap(multiPart)); if (log.isDebugEnabled()) log.debug("# of files uploaded: {} ", fileCount); - return put(model, defaultGraph, getURI()); // ignore the @QueryParam("graph") value + return put(model, false, getURI()); } catch (URISyntaxException ex) { @@ -503,13 +519,11 @@ public Response putMultipart(FormDataMultiPart multiPart, @QueryParam("default") /** * Implements DELETE method of SPARQL Graph Store Protocol. * - * @param defaultGraph true if default graph is requested - * @param graphUriUnused named graph URI (unused) * @return response */ @DELETE @Override - public Response delete(@QueryParam("default") @DefaultValue("false") Boolean defaultGraph, @QueryParam("graph") URI graphUriUnused) + public Response delete() { if (!getAllowedMethods().contains(HttpMethod.DELETE)) throw new WebApplicationException("Cannot delete document", Response.status(Response.Status.METHOD_NOT_ALLOWED).allow(getAllowedMethods()).build()); @@ -518,7 +532,7 @@ public Response delete(@QueryParam("default") @DefaultValue("false") Boolean def { Model existingModel = getService().getGraphStoreClient().getModel(getURI().toString()); - ResponseBuilder rb = evaluatePreconditions(existingModel); + Response.ResponseBuilder rb = evaluatePreconditions(existingModel); if (rb != null) return rb.build(); // preconditions not met } catch (NotFoundException ex) @@ -529,6 +543,32 @@ public Response delete(@QueryParam("default") @DefaultValue("false") Boolean def return super.delete(false, getURI()); } + /** + * Gets a diff of triples between two models and returns a set of their subject resources. + * + * @param beforeUpdateModel model before the update + * @param afterUpdateModel model after the update + * @return set of changed resources + */ + public Set getChangedResources(Model beforeUpdateModel, Model afterUpdateModel) + { + if (beforeUpdateModel == null) throw new IllegalArgumentException("Model before update cannot be null"); + if (afterUpdateModel == null) throw new IllegalArgumentException("Model after update cannot be null"); + + Model addedTriples = afterUpdateModel.difference(beforeUpdateModel); + Model removedTriples = beforeUpdateModel.difference(afterUpdateModel); + + Set changedResources = new HashSet<>(); + addedTriples.listStatements().forEachRemaining(statement -> { + changedResources.add(statement.getSubject()); + }); + removedTriples.listStatements().forEachRemaining(statement -> { + changedResources.add(statement.getSubject()); + }); + + return changedResources; + } + /** * Get internal response object. * @@ -556,22 +596,11 @@ public com.atomgraph.core.model.impl.Response getInternalResponse(Model model, U * @return response builder */ @Override - public ResponseBuilder getResponseBuilder(Model model, URI graphUri) + public Response.ResponseBuilder getResponseBuilder(Model model, URI graphUri) { return getInternalResponse(model, graphUri).getResponseBuilder(); } - /** - * List allowed HTTP methods for the current graph URI. - * Exceptions apply to the application's Root document, owner's WebID document, and secretary's WebID document. - * - * @return list of HTTP methods - */ - public Set getAllowedMethods() - { - return allowedMethods; - } - /** * Writes all files from the multipart RDF/POST request body. * @@ -860,19 +889,88 @@ public Model validate(Model model) * @param model RDF model * @return {@code jakarta.ws.rs.core.Response.ResponseBuilder} instance. null if preconditions are not met. */ - public ResponseBuilder evaluatePreconditions(Model model) + public Response.ResponseBuilder evaluatePreconditions(Model model) { return getInternalResponse(model, getURI()).evaluatePreconditions(); } /** - * Returns the named graph URI. + * Parses multipart RDF/POST request. + * + * @param multiPart multipart form data + * @return RDF graph + * @throws URISyntaxException thrown if there is a syntax error in RDF/POST data + * @see RDF/POST Encoding for RDF + */ + public Model parseModel(FormDataMultiPart multiPart) throws URISyntaxException + { + if (multiPart == null) throw new IllegalArgumentException("FormDataMultiPart cannot be null"); + + List keys = new ArrayList<>(), values = new ArrayList<>(); + Iterator it = multiPart.getBodyParts().iterator(); // not using getFields() to retain ordering + + while (it.hasNext()) + { + FormDataBodyPart bodyPart = (FormDataBodyPart)it.next(); + if (log.isDebugEnabled()) log.debug("Body part media type: {} headers: {}", bodyPart.getMediaType(), bodyPart.getHeaders()); + + // it's a file (if the filename is not empty) + if (bodyPart.getContentDisposition().getFileName() != null && + !bodyPart.getContentDisposition().getFileName().isEmpty()) + { + keys.add(bodyPart.getName()); + if (log.isDebugEnabled()) log.debug("FormDataBodyPart name: {} value: {}", bodyPart.getName(), bodyPart.getContentDisposition().getFileName()); + values.add(bodyPart.getContentDisposition().getFileName()); + } + else + { + if (bodyPart.isSimple() && !bodyPart.getValue().isEmpty()) + { + keys.add(bodyPart.getName()); + if (log.isDebugEnabled()) log.debug("FormDataBodyPart name: {} value: {}", bodyPart.getName(), bodyPart.getValue()); + values.add(bodyPart.getValue()); + } + } + } + + return RDFPostReader.parse(keys, values); + } + + /** + * Gets a map of file parts from multipart form data. * - * @return graph URI + * @param multiPart multipart form data + * @return map of file parts */ - public URI getURI() + public Map getFileNameBodyPartMap(FormDataMultiPart multiPart) { - return getUriInfo().getAbsolutePath(); + if (multiPart == null) throw new IllegalArgumentException("FormDataMultiPart cannot be null"); + + Map fileNameBodyPartMap = new HashMap<>(); + Iterator it = multiPart.getBodyParts().iterator(); // not using getFields() to retain ordering + while (it.hasNext()) + { + FormDataBodyPart bodyPart = (FormDataBodyPart)it.next(); + if (log.isDebugEnabled()) log.debug("Body part media type: {} headers: {}", bodyPart.getMediaType(), bodyPart.getHeaders()); + + if (bodyPart.getContentDisposition().getFileName() != null) // it's a file + { + if (log.isDebugEnabled()) log.debug("FormDataBodyPart name: {} value: {}", bodyPart.getName(), bodyPart.getContentDisposition().getFileName()); + fileNameBodyPartMap.put(bodyPart.getContentDisposition().getFileName(), bodyPart); + } + } + return fileNameBodyPartMap; + } + + /** + * List allowed HTTP methods for the current graph URI. + * Exceptions apply to the application's Root document, owner's WebID document, and secretary's WebID document. + * + * @return list of HTTP methods + */ + public Set getAllowedMethods() + { + return allowedMethods; } /** @@ -885,4 +983,125 @@ public EndpointAccessor getEndpointAccessor() return getService().getEndpointAccessor(); } -} + /** + * Returns a list of supported languages. + * + * @return list of languages + */ + @Override + public List getLanguages() + { + return getSystem().getSupportedLanguages(); + } + + /** + * Returns URI builder for uploaded file resources. + * + * @return URI builder + */ + public UriBuilder getUploadsUriBuilder() + { + return uploadsUriBuilder.clone(); + } + + /** + * Returns message digest used in SHA1 hashing. + * + * @return message digest + */ + public MessageDigest getMessageDigest() + { + return messageDigest; + } + + /** + * Returns the current application. + * + * @return application resource + */ + public com.atomgraph.linkeddatahub.apps.model.Application getApplication() + { + return application; + } + + /** + * Returns the ontology of the current application. + * + * @return ontology resource + */ + public Ontology getOntology() + { + return ontology; + } + + /** + * Returns the SPARQL service of the current application. + * + * @return service resource + */ + public Service getService() + { + return service; + } + + /** + * Get JAX-RS security context + * + * @return security context object + */ + public SecurityContext getSecurityContext() + { + return securityContext; + } + + /** + * Gets authenticated agent's context + * + * @return optional agent's context + */ + public Optional getAgentContext() + { + return agentContext; + } + + /** + * Returns a registry of JAX-RS providers. + * + * @return provider registry + */ + public Providers getProviders() + { + return providers; + } + + /** + * Returns the system application. + * + * @return JAX-RS application + */ + public com.atomgraph.linkeddatahub.Application getSystem() + { + return system; + } + + /** + * Returns URI of the WebID document of the applications owner. + * + * @return document URI + */ + public URI getOwnerDocURI() + { + return ownerDocURI; + } + + /** + * Returns URI of the WebID document of the applications secretary. + * + * @return document URI + */ + public URI getSecretaryDocURI() + { + return secretaryDocURI; + } + +} \ No newline at end of file diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/Dispatcher.java b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/Dispatcher.java index 451dc874f..1ad8f8002 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/Dispatcher.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/Dispatcher.java @@ -27,7 +27,6 @@ import com.atomgraph.linkeddatahub.resource.admin.pkg.UninstallPackage; import com.atomgraph.linkeddatahub.resource.Settings; import com.atomgraph.linkeddatahub.resource.admin.SignUp; -import com.atomgraph.linkeddatahub.resource.Graph; import com.atomgraph.linkeddatahub.resource.acl.Access; import com.atomgraph.linkeddatahub.resource.acl.AccessRequest; import java.util.Optional; @@ -82,12 +81,12 @@ public Optional getProxyClass() if (getUriInfo().getQueryParameters().containsKey(AC.uri.getLocalName())) { if (log.isDebugEnabled()) log.debug("No Application matched request URI <{}>, dispatching to ProxyResourceBase", getUriInfo().getQueryParameters().getFirst(AC.uri.getLocalName())); - return Optional.of(ProxyResourceBase.class); + return Optional.of(ProxiedGraph.class); } if (getDataset().isPresent()) { if (log.isDebugEnabled()) log.debug("Serving request URI <{}> from Dataset <{}>, dispatching to ProxyResourceBase", getUriInfo().getAbsolutePath(), getDataset().get()); - return Optional.of(ProxyResourceBase.class); + return Optional.of(ProxiedGraph.class); } return Optional.empty(); @@ -181,7 +180,7 @@ public Class getAccessRequest() @Path("uploads/{sha1sum}") public Class getFileItem() { - return getProxyClass().orElse(com.atomgraph.linkeddatahub.resource.upload.sha1.Item.class); + return getProxyClass().orElse(com.atomgraph.linkeddatahub.resource.upload.Item.class); } /** @@ -263,12 +262,13 @@ public Class getSettingsEndpoint() /** * Returns the default JAX-RS resource class. + * Only directly identified access to named graphs is allowed (the Graph Store Protocol endpoint is not exposed). * * @return resource class */ public Class getDocumentClass() { - return Graph.class; + return DirectGraphStoreImpl.class; } /** diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/GraphStoreImpl.java b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/GraphStoreImpl.java deleted file mode 100644 index 1c25cd2a2..000000000 --- a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/GraphStoreImpl.java +++ /dev/null @@ -1,322 +0,0 @@ -/** - * Copyright 2019 Martynas Jusevičius - * - * 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.server.model.impl; - -import com.atomgraph.core.MediaTypes; -import com.atomgraph.core.riot.lang.RDFPostReader; -import com.atomgraph.linkeddatahub.model.Service; -import com.atomgraph.linkeddatahub.server.security.AgentContext; -import java.net.URI; -import java.net.URISyntaxException; -import java.security.MessageDigest; -import java.util.List; -import java.util.Locale; -import java.util.Optional; -import jakarta.inject.Inject; -import jakarta.ws.rs.InternalServerErrorException; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.Request; -import jakarta.ws.rs.core.SecurityContext; -import jakarta.ws.rs.core.UriBuilder; -import jakarta.ws.rs.core.UriInfo; -import jakarta.ws.rs.ext.Providers; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import org.apache.jena.ontology.Ontology; -import org.apache.jena.rdf.model.Model; -import org.glassfish.jersey.media.multipart.BodyPart; -import org.glassfish.jersey.media.multipart.FormDataBodyPart; -import org.glassfish.jersey.media.multipart.FormDataMultiPart; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * LinkedDataHub Graph Store implementation. - * We need to subclass the Core class because we're injecting a subclass of Service. - * - * @author Martynas Jusevičius {@literal } - */ -public abstract class GraphStoreImpl extends com.atomgraph.core.model.impl.GraphStoreImpl -{ - - private static final Logger log = LoggerFactory.getLogger(GraphStoreImpl.class); - - /** - * The relative path of the content-addressed file container. - */ - public static final String UPLOADS_PATH = "uploads"; - - private final UriInfo uriInfo; - private final com.atomgraph.linkeddatahub.apps.model.Application application; - private final Ontology ontology; - private final Service service; - private final Providers providers; - private final com.atomgraph.linkeddatahub.Application system; - private final UriBuilder uploadsUriBuilder; - private final MessageDigest messageDigest; - /** The URIs for owner and secretary documents. */ - protected final URI ownerDocURI, secretaryDocURI; - private final SecurityContext securityContext; - private final Optional agentContext; - - /** - * Constructs Graph Store. - * - * @param request current request - * @param uriInfo URI info of the current request - * @param mediaTypes a registry of readable/writable media types - * @param application current application - * @param ontology ontology of the current application - * @param service SPARQL service of the current application - * @param securityContext JAX-RS security context - * @param agentContext authenticated agent's context - * @param providers registry of JAX-RS providers - * @param system system application - */ - @Inject - public GraphStoreImpl(@Context Request request, @Context UriInfo uriInfo, MediaTypes mediaTypes, - com.atomgraph.linkeddatahub.apps.model.Application application, Optional ontology, Optional service, - @Context SecurityContext securityContext, Optional agentContext, - @Context Providers providers, com.atomgraph.linkeddatahub.Application system) - { - super(request, service.get(), mediaTypes); - if (ontology.isEmpty()) throw new InternalServerErrorException("Ontology is not specified"); - if (service.isEmpty()) throw new InternalServerErrorException("Service is not specified"); - this.uriInfo = uriInfo; - this.application = application; - this.ontology = ontology.get(); - this.service = service.get(); - this.securityContext = securityContext; - this.agentContext = agentContext; - this.providers = providers; - this.system = system; - this.messageDigest = system.getMessageDigest(); - uploadsUriBuilder = uriInfo.getBaseUriBuilder().path(UPLOADS_PATH); - URI ownerURI = URI.create(application.getMaker().getURI()); - try - { - this.ownerDocURI = new URI(ownerURI.getScheme(), ownerURI.getSchemeSpecificPart(), null).normalize(); - this.secretaryDocURI = new URI(system.getSecretaryWebIDURI().getScheme(), system.getSecretaryWebIDURI().getSchemeSpecificPart(), null).normalize(); - } - catch (URISyntaxException ex) - { - throw new InternalServerErrorException(ex); - } - } - - /** - * Parses multipart RDF/POST request. - * - * @param multiPart multipart form data - * @return RDF graph - * @throws URISyntaxException thrown if there is a syntax error in RDF/POST data - * @see RDF/POST Encoding for RDF - */ - public Model parseModel(FormDataMultiPart multiPart) throws URISyntaxException - { - if (multiPart == null) throw new IllegalArgumentException("FormDataMultiPart cannot be null"); - - List keys = new ArrayList<>(), values = new ArrayList<>(); - Iterator it = multiPart.getBodyParts().iterator(); // not using getFields() to retain ordering - - while (it.hasNext()) - { - FormDataBodyPart bodyPart = (FormDataBodyPart)it.next(); - if (log.isDebugEnabled()) log.debug("Body part media type: {} headers: {}", bodyPart.getMediaType(), bodyPart.getHeaders()); - - // it's a file (if the filename is not empty) - if (bodyPart.getContentDisposition().getFileName() != null && - !bodyPart.getContentDisposition().getFileName().isEmpty()) - { - keys.add(bodyPart.getName()); - if (log.isDebugEnabled()) log.debug("FormDataBodyPart name: {} value: {}", bodyPart.getName(), bodyPart.getContentDisposition().getFileName()); - values.add(bodyPart.getContentDisposition().getFileName()); - } - else - { - if (bodyPart.isSimple() && !bodyPart.getValue().isEmpty()) - { - keys.add(bodyPart.getName()); - if (log.isDebugEnabled()) log.debug("FormDataBodyPart name: {} value: {}", bodyPart.getName(), bodyPart.getValue()); - values.add(bodyPart.getValue()); - } - } - } - - return RDFPostReader.parse(keys, values); - } - - /** - * Gets a map of file parts from multipart form data. - * - * @param multiPart multipart form data - * @return map of file parts - */ - public Map getFileNameBodyPartMap(FormDataMultiPart multiPart) - { - if (multiPart == null) throw new IllegalArgumentException("FormDataMultiPart cannot be null"); - - Map fileNameBodyPartMap = new HashMap<>(); - Iterator it = multiPart.getBodyParts().iterator(); // not using getFields() to retain ordering - while (it.hasNext()) - { - FormDataBodyPart bodyPart = (FormDataBodyPart)it.next(); - if (log.isDebugEnabled()) log.debug("Body part media type: {} headers: {}", bodyPart.getMediaType(), bodyPart.getHeaders()); - - if (bodyPart.getContentDisposition().getFileName() != null) // it's a file - { - if (log.isDebugEnabled()) log.debug("FormDataBodyPart name: {} value: {}", bodyPart.getName(), bodyPart.getContentDisposition().getFileName()); - fileNameBodyPartMap.put(bodyPart.getContentDisposition().getFileName(), bodyPart); - } - } - return fileNameBodyPartMap; - } - - /** - * Returns a list of supported languages. - * - * @return list of languages - */ - @Override - public List getLanguages() - { - return getSystem().getSupportedLanguages(); - } - - /** - * Returns URI builder for uploaded file resources. - * - * @return URI builder - */ - public UriBuilder getUploadsUriBuilder() - { - return uploadsUriBuilder.clone(); - } - - /** - * Returns message digest used in SHA1 hashing. - * - * @return message digest - */ - public MessageDigest getMessageDigest() - { - return messageDigest; - } - - /** - * Returns the request URI information. - * - * @return URI info - */ - public UriInfo getUriInfo() - { - return uriInfo; - } - - /** - * Returns the current application. - * - * @return application resource - */ - public com.atomgraph.linkeddatahub.apps.model.Application getApplication() - { - return application; - } - - /** - * Returns the ontology of the current application. - * - * @return ontology resource - */ - public Ontology getOntology() - { - return ontology; - } - - /** - * Returns the SPARQL service of the current application. - * - * @return service resource - */ - public Service getService() - { - return service; - } - - /** - * Get JAX-RS security context - * - * @return security context object - */ - public SecurityContext getSecurityContext() - { - return securityContext; - } - - /** - * Gets authenticated agent's context - * - * @return optional agent's context - */ - public Optional getAgentContext() - { - return agentContext; - } - - /** - * Returns a registry of JAX-RS providers. - * - * @return provider registry - */ - public Providers getProviders() - { - return providers; - } - - /** - * Returns the system application. - * - * @return JAX-RS application - */ - public com.atomgraph.linkeddatahub.Application getSystem() - { - return system; - } - - /** - * Returns URI of the WebID document of the applications owner. - * - * @return document URI - */ - public URI getOwnerDocURI() - { - return ownerDocURI; - } - - /** - * Returns URI of the WebID document of the applications secretary. - * - * @return document URI - */ - public URI getSecretaryDocURI() - { - return secretaryDocURI; - } - -} \ No newline at end of file diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/ProxyResourceBase.java b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/ProxiedGraph.java similarity index 98% rename from src/main/java/com/atomgraph/linkeddatahub/server/model/impl/ProxyResourceBase.java rename to src/main/java/com/atomgraph/linkeddatahub/server/model/impl/ProxiedGraph.java index 5665a8e72..02eb59f62 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/ProxyResourceBase.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/ProxiedGraph.java @@ -83,10 +83,10 @@ * * @author {@literal Martynas Jusevičius } */ -public class ProxyResourceBase extends com.atomgraph.client.model.impl.ProxyResourceBase +public class ProxiedGraph extends com.atomgraph.client.model.impl.ProxiedGraph { - private static final Logger log = LoggerFactory.getLogger(ProxyResourceBase.class); + private static final Logger log = LoggerFactory.getLogger(ProxiedGraph.class); private final UriInfo uriInfo; private final ContainerRequestContext crc; @@ -117,7 +117,7 @@ public class ProxyResourceBase extends com.atomgraph.client.model.impl.ProxyReso * @param dataset optional dataset */ @Inject - public ProxyResourceBase(@Context UriInfo uriInfo, @Context Request request, @Context HttpHeaders httpHeaders, MediaTypes mediaTypes, + public ProxiedGraph(@Context UriInfo uriInfo, @Context Request request, @Context HttpHeaders httpHeaders, MediaTypes mediaTypes, com.atomgraph.linkeddatahub.apps.model.Application application, Optional service, @Context SecurityContext securityContext, @Context ContainerRequestContext crc, com.atomgraph.linkeddatahub.Application system, @Context HttpServletRequest httpServletRequest, DataManager dataManager, Optional agentContext, @@ -157,7 +157,7 @@ public ProxyResourceBase(@Context UriInfo uriInfo, @Context Request request, @Co * @param agentContext authenticated agent's context * @param providers registry of JAX-RS providers */ - protected ProxyResourceBase(@Context UriInfo uriInfo, @Context Request request, @Context HttpHeaders httpHeaders, MediaTypes mediaTypes, + protected ProxiedGraph(@Context UriInfo uriInfo, @Context Request request, @Context HttpHeaders httpHeaders, MediaTypes mediaTypes, com.atomgraph.linkeddatahub.apps.model.Application application, Optional service, @Context SecurityContext securityContext, @Context ContainerRequestContext crc, @QueryParam("uri") URI uri, @QueryParam("endpoint") URI endpoint, @QueryParam("query") String query, @QueryParam("accept") MediaType accept, @QueryParam("mode") URI mode, diff --git a/src/test/java/com/atomgraph/linkeddatahub/server/model/impl/ProxyResourceBaseTest.java b/src/test/java/com/atomgraph/linkeddatahub/server/model/impl/ProxyResourceBaseTest.java index 7e9cc8c43..4e8687796 100644 --- a/src/test/java/com/atomgraph/linkeddatahub/server/model/impl/ProxyResourceBaseTest.java +++ b/src/test/java/com/atomgraph/linkeddatahub/server/model/impl/ProxyResourceBaseTest.java @@ -33,51 +33,51 @@ public class ProxyResourceBaseTest @Test(expected = IllegalArgumentException.class) public void testNullURI() { - ProxyResourceBase.validateNotInternalURL(null); + ProxiedGraph.validateNotInternalURL(null); } @Test(expected = ForbiddenException.class) public void testLinkLocalIPv4Blocked() { - ProxyResourceBase.validateNotInternalURL(URI.create("http://169.254.1.1:8080/test")); + ProxiedGraph.validateNotInternalURL(URI.create("http://169.254.1.1:8080/test")); } @Test(expected = ForbiddenException.class) public void testPrivateClass10Blocked() { - ProxyResourceBase.validateNotInternalURL(URI.create("http://10.0.0.1:8080/test")); + ProxiedGraph.validateNotInternalURL(URI.create("http://10.0.0.1:8080/test")); } @Test(expected = ForbiddenException.class) public void testPrivateClass172Blocked() { - ProxyResourceBase.validateNotInternalURL(URI.create("http://172.16.0.0:8080/test")); + ProxiedGraph.validateNotInternalURL(URI.create("http://172.16.0.0:8080/test")); } @Test(expected = ForbiddenException.class) public void testPrivateClass192Blocked() { - ProxyResourceBase.validateNotInternalURL(URI.create("http://192.168.1.1:8080/test")); + ProxiedGraph.validateNotInternalURL(URI.create("http://192.168.1.1:8080/test")); } @Test public void testExternalURLAllowed() { // Public IPs should be allowed (no exception thrown) - ProxyResourceBase.validateNotInternalURL(URI.create("http://8.8.8.8:80/test")); + ProxiedGraph.validateNotInternalURL(URI.create("http://8.8.8.8:80/test")); } @Test public void testPublicDomainAllowed() { // Public domains should be allowed (no exception thrown) - ProxyResourceBase.validateNotInternalURL(URI.create("http://example.org/test")); + ProxiedGraph.validateNotInternalURL(URI.create("http://example.org/test")); } @Test public void testHTTPSAllowed() { // HTTPS to public domain should be allowed (no exception thrown) - ProxyResourceBase.validateNotInternalURL(URI.create("https://www.w3.org/ns/ldp")); + ProxiedGraph.validateNotInternalURL(URI.create("https://www.w3.org/ns/ldp")); } }