+ *
+ * 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.filter.request;
+
+import com.atomgraph.client.MediaTypes;
+import com.atomgraph.client.util.HTMLMediaTypePredicate;
+import com.atomgraph.client.vocabulary.AC;
+import com.atomgraph.core.exception.BadGatewayException;
+import com.atomgraph.core.util.ModelUtils;
+import com.atomgraph.core.util.ResultSetUtils;
+import com.atomgraph.linkeddatahub.apps.model.Dataset;
+import com.atomgraph.linkeddatahub.client.GraphStoreClient;
+import com.atomgraph.linkeddatahub.client.filter.auth.IDTokenDelegationFilter;
+import com.atomgraph.linkeddatahub.client.filter.auth.WebIDDelegationFilter;
+import com.atomgraph.linkeddatahub.server.security.AgentContext;
+import com.atomgraph.linkeddatahub.server.security.IDTokenSecurityContext;
+import com.atomgraph.linkeddatahub.server.security.WebIDSecurityContext;
+import com.atomgraph.linkeddatahub.vocabulary.LAPP;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import jakarta.annotation.Priority;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.HttpMethod;
+import jakarta.ws.rs.NotAcceptableException;
+import jakarta.ws.rs.NotAllowedException;
+import jakarta.ws.rs.Priorities;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.container.PreMatching;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.EntityTag;
+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.Variant;
+import org.apache.jena.query.ResultSet;
+import org.apache.jena.query.ResultSetRewindable;
+import org.apache.jena.rdf.model.Model;
+import org.apache.jena.riot.Lang;
+import org.apache.jena.riot.RDFLanguages;
+import org.apache.jena.riot.resultset.ResultSetReaderRegistry;
+import org.glassfish.jersey.message.internal.MessageBodyProviderNotFoundException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * JAX-RS request filter that intercepts proxy requests and short-circuits the pipeline
+ * via {@link ContainerRequestContext#abortWith(Response)} before {@link AuthorizationFilter} runs.
+ *
+ * Two proxy modes are supported, resolved in order by {@link #resolveTargetURI}:
+ *
+ * - Explicit {@code ?uri=} query parameter pointing to an external URI (not relative to the
+ * current application base). Requests relative to the app base are ignored here because
+ * {@link ApplicationFilter} already rewrote the request URI for those.
+ * - {@code lapp:Dataset} proxy: the request URI matched a URL-path pattern defined in the
+ * system dataset configuration, and the dataset provides a proxied target URI.
+ *
+ * ACL is not checked for proxy requests: the proxy is a global transport function, not a document
+ * operation. Access control is enforced by the target endpoint.
+ *
+ * @author Martynas Jusevičius {@literal }
+ */
+@PreMatching
+@Priority(Priorities.USER + 50) // after auth filters (Priorities.USER = 4000), before AuthorizationFilter (Priorities.USER + 100)
+public class ProxyRequestFilter implements ContainerRequestFilter
+{
+
+ private static final Logger log = LoggerFactory.getLogger(ProxyRequestFilter.class);
+
+ @Inject com.atomgraph.linkeddatahub.Application system;
+ @Inject MediaTypes mediaTypes;
+ @Context Request request;
+
+ @Override
+ public void filter(ContainerRequestContext requestContext) throws IOException
+ {
+ Optional targetOpt = resolveTargetURI(requestContext);
+ if (targetOpt.isEmpty()) return; // not a proxy request
+
+ URI targetURI = targetOpt.get();
+
+ // strip #fragment (servers do not receive fragment identifiers)
+ if (targetURI.getFragment() != null)
+ {
+ try
+ {
+ targetURI = new URI(targetURI.getScheme(), targetURI.getAuthority(), targetURI.getPath(), targetURI.getQuery(), null);
+ }
+ catch (URISyntaxException ex)
+ {
+ // should not happen when only removing the fragment
+ }
+ }
+
+ // serve mapped URIs (e.g. system ontologies) directly from the DataManager cache
+ if (getSystem().getDataManager().isMapped(targetURI.toString()))
+ {
+ if (log.isDebugEnabled()) log.debug("Serving mapped URI from DataManager cache: {}", targetURI);
+ Model model = getSystem().getDataManager().loadModel(targetURI.toString());
+ requestContext.abortWith(getResponse(model, Response.Status.OK));
+ return;
+ }
+
+ if (!getSystem().isEnableLinkedDataProxy()) throw new NotAllowedException("Linked Data proxy not enabled");
+ // LNK-009: validate that the target URI is not an internal/private address (SSRF protection)
+ getSystem().getURLValidator().validate(targetURI);
+
+ WebTarget target = getSystem().getExternalClient().target(targetURI);
+
+ // forward agent identity to the target endpoint
+ AgentContext agentContext = (AgentContext) requestContext.getProperty(AgentContext.class.getCanonicalName());
+ if (agentContext != null)
+ {
+ if (agentContext instanceof WebIDSecurityContext)
+ target.register(new WebIDDelegationFilter(agentContext.getAgent()));
+ else if (agentContext instanceof IDTokenSecurityContext idTokenSecurityContext)
+ target.register(new IDTokenDelegationFilter(agentContext.getAgent(),
+ idTokenSecurityContext.getJWTToken(), requestContext.getUriInfo().getBaseUri().getPath(), null));
+ }
+
+ List readableMediaTypesList = new ArrayList<>();
+ readableMediaTypesList.addAll(mediaTypes.getReadable(Model.class));
+ readableMediaTypesList.addAll(mediaTypes.getReadable(ResultSet.class));
+ MediaType[] readableMediaTypesArray = readableMediaTypesList.toArray(MediaType[]::new);
+
+ if (log.isDebugEnabled()) log.debug("Proxying {} {} → {}", requestContext.getMethod(), requestContext.getUriInfo().getRequestUri(), targetURI);
+
+ try
+ {
+ Response clientResponse = switch (requestContext.getMethod())
+ {
+ case HttpMethod.GET ->
+ target.request(readableMediaTypesArray)
+ .header(HttpHeaders.USER_AGENT, GraphStoreClient.USER_AGENT)
+ .get();
+ case HttpMethod.POST ->
+ target.request()
+ .accept(readableMediaTypesArray)
+ .header(HttpHeaders.USER_AGENT, GraphStoreClient.USER_AGENT)
+ .post(Entity.entity(requestContext.getEntityStream(), requestContext.getMediaType()));
+ case "PATCH" ->
+ target.request()
+ .accept(readableMediaTypesArray)
+ .header(HttpHeaders.USER_AGENT, GraphStoreClient.USER_AGENT)
+ .method("PATCH", Entity.entity(requestContext.getEntityStream(), requestContext.getMediaType()));
+ case HttpMethod.PUT ->
+ target.request()
+ .accept(readableMediaTypesArray)
+ .header(HttpHeaders.USER_AGENT, GraphStoreClient.USER_AGENT)
+ .put(Entity.entity(requestContext.getEntityStream(), requestContext.getMediaType()));
+ case HttpMethod.DELETE ->
+ target.request()
+ .header(HttpHeaders.USER_AGENT, GraphStoreClient.USER_AGENT)
+ .delete();
+ default -> throw new NotAllowedException(requestContext.getMethod());
+ };
+
+ try (clientResponse)
+ {
+ // provide the target URI as a base URI hint so ModelProvider / HtmlJsonLDReader can resolve relative references
+ clientResponse.getHeaders().putSingle(com.atomgraph.core.io.ModelProvider.REQUEST_URI_HEADER, targetURI.toString());
+ requestContext.abortWith(getResponse(clientResponse));
+ }
+ }
+ catch (MessageBodyProviderNotFoundException ex)
+ {
+ if (log.isWarnEnabled()) log.warn("Proxied URI {} returned non-RDF media type", targetURI);
+ throw new NotAcceptableException(ex);
+ }
+ catch (ProcessingException ex)
+ {
+ if (log.isWarnEnabled()) log.warn("Could not dereference proxied URI: {}", targetURI);
+ throw new BadGatewayException(ex);
+ }
+ }
+
+ /**
+ * Resolves the proxy target URI for the current request.
+ * Returns empty if this request should not be proxied.
+ *
+ * @param requestContext the current request context
+ * @return optional target URI to proxy to
+ */
+ protected Optional resolveTargetURI(ContainerRequestContext requestContext)
+ {
+ // Case 1: explicit ?uri= query parameter
+ String uriParam = requestContext.getUriInfo().getQueryParameters().getFirst(AC.uri.getLocalName());
+ if (uriParam != null)
+ {
+ URI targetURI = URI.create(uriParam);
+ @SuppressWarnings("unchecked")
+ Optional appOpt =
+ (Optional) requestContext.getProperty(LAPP.Application.getURI());
+ // ApplicationFilter rewrites ?uri= values that are relative to the app base URI; skip those
+ if (appOpt != null && appOpt.isPresent() && !appOpt.get().getBaseURI().relativize(targetURI).isAbsolute())
+ return Optional.empty();
+ return Optional.of(targetURI);
+ }
+
+ // Case 2: lapp:Dataset proxy
+ @SuppressWarnings("unchecked")
+ Optional datasetOpt =
+ (Optional) requestContext.getProperty(LAPP.Dataset.getURI());
+ if (datasetOpt != null && datasetOpt.isPresent())
+ {
+ URI proxied = datasetOpt.get().getProxied(requestContext.getUriInfo().getAbsolutePath());
+ if (proxied != null) return Optional.of(proxied);
+ }
+
+ return Optional.empty();
+ }
+
+ /**
+ * Converts a client response from the proxy target into a JAX-RS response.
+ *
+ * @param clientResponse response from the proxy target
+ * @return JAX-RS response to return to the original caller
+ */
+ protected Response getResponse(Response clientResponse)
+ {
+ if (clientResponse.getMediaType() == null) return Response.status(clientResponse.getStatus()).build();
+ return getResponse(clientResponse, clientResponse.getStatusInfo());
+ }
+
+ /**
+ * Converts a client response from the proxy target into a JAX-RS response with the given status.
+ *
+ * @param clientResponse response from the proxy target
+ * @param statusType status to use in the returned response
+ * @return JAX-RS response
+ */
+ protected Response getResponse(Response clientResponse, Response.StatusType statusType)
+ {
+ MediaType formatType = new MediaType(clientResponse.getMediaType().getType(), clientResponse.getMediaType().getSubtype()); // discard charset param
+
+ Lang lang = RDFLanguages.contentTypeToLang(formatType.toString());
+ if (lang != null && ResultSetReaderRegistry.isRegistered(lang))
+ {
+ ResultSetRewindable results = clientResponse.readEntity(ResultSetRewindable.class);
+ return getResponse(results, statusType);
+ }
+
+ Model model = clientResponse.readEntity(Model.class);
+ return getResponse(model, statusType);
+ }
+
+ /**
+ * Builds a content-negotiated response for the given RDF model.
+ *
+ * @param model RDF model
+ * @param statusType response status
+ * @return JAX-RS response
+ */
+ protected Response getResponse(Model model, Response.StatusType statusType)
+ {
+ List variants = com.atomgraph.core.model.impl.Response.getVariants(
+ mediaTypes.getWritable(Model.class),
+ getSystem().getSupportedLanguages(),
+ new ArrayList<>());
+
+ return new com.atomgraph.core.model.impl.Response(request,
+ model,
+ null,
+ new EntityTag(Long.toHexString(ModelUtils.hashModel(model))),
+ variants,
+ new HTMLMediaTypePredicate()).
+ getResponseBuilder().
+ status(statusType).
+ build();
+ }
+
+ /**
+ * Builds a content-negotiated response for the given SPARQL result set.
+ *
+ * @param resultSet SPARQL result set
+ * @param statusType response status
+ * @return JAX-RS response
+ */
+ protected Response getResponse(ResultSetRewindable resultSet, Response.StatusType statusType)
+ {
+ long hash = ResultSetUtils.hashResultSet(resultSet);
+ resultSet.reset();
+
+ List variants = com.atomgraph.core.model.impl.Response.getVariants(
+ mediaTypes.getWritable(ResultSet.class),
+ getSystem().getSupportedLanguages(),
+ new ArrayList<>());
+
+ return new com.atomgraph.core.model.impl.Response(request,
+ resultSet,
+ null,
+ new EntityTag(Long.toHexString(hash)),
+ variants,
+ new HTMLMediaTypePredicate()).
+ getResponseBuilder().
+ status(statusType).
+ build();
+ }
+
+ /**
+ * Returns the system application.
+ *
+ * @return system application
+ */
+ public com.atomgraph.linkeddatahub.Application getSystem()
+ {
+ return system;
+ }
+
+}
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 4241519f1..670c8a0d2 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
@@ -16,8 +16,6 @@
*/
package com.atomgraph.linkeddatahub.server.model.impl;
-import com.atomgraph.client.vocabulary.AC;
-import com.atomgraph.linkeddatahub.apps.model.Dataset;
import com.atomgraph.linkeddatahub.resource.Add;
import com.atomgraph.linkeddatahub.resource.Generate;
import com.atomgraph.linkeddatahub.resource.Namespace;
@@ -29,146 +27,100 @@
import com.atomgraph.linkeddatahub.resource.admin.SignUp;
import com.atomgraph.linkeddatahub.resource.acl.Access;
import com.atomgraph.linkeddatahub.resource.acl.AccessRequest;
-import java.util.Optional;
-import jakarta.inject.Inject;
import jakarta.ws.rs.Path;
-import jakarta.ws.rs.core.Context;
-import jakarta.ws.rs.core.UriInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A catch-all JAX-RS resource that routes requests to sub-resources.
- *
+ * Proxy requests ({@code ?uri=} and {@code lapp:Dataset}) are handled earlier by
+ * {@link com.atomgraph.linkeddatahub.server.filter.request.ProxyRequestFilter} and never reach this class.
+ *
* @author Martynas Jusevičius {@literal }
*/
@Path("/")
public class Dispatcher
{
-
+
private static final Logger log = LoggerFactory.getLogger(Dispatcher.class);
- private final UriInfo uriInfo;
- private final Optional dataset;
- private final com.atomgraph.linkeddatahub.Application system;
-
- /**
- * Constructs resource which dispatches requests to sub-resources.
- *
- * @param uriInfo URI info
- * @param dataset optional dataset
- * @param system system application
- */
- @Inject
- public Dispatcher(@Context UriInfo uriInfo, Optional dataset, com.atomgraph.linkeddatahub.Application system)
- {
- this.uriInfo = uriInfo;
- this.dataset = dataset;
- this.system = system;
- }
-
- /**
- * Returns proxy class that takes precedence over the default JAX-RS path matching.
- * The request is proxied in two cases:
- *
- * - externally (URI specified by the
?uri query param, when ?graph query param not set)
- * - internally if it matches a
lapp:Dataset specified in the system app config
- *
- * @return optional class
- */
- 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(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(ProxiedGraph.class);
- }
-
- return Optional.empty();
- }
-
/**
* Returns JAX-RS resource that will handle this request.
- *
+ *
* @return resource
*/
@Path("{path: .*}")
public Class getSubResource()
{
- return getProxyClass().orElse(getDocumentClass());
+ return getDocumentClass();
}
-
+
// TO-DO: move @Path annotations onto respective classes?
-
+
/**
* Returns SPARQL protocol endpoint.
- *
+ *
* @return endpoint resource
*/
@Path("sparql")
public Class getSPARQLEndpoint()
{
- return getProxyClass().orElse(SPARQLEndpointImpl.class);
+ return SPARQLEndpointImpl.class;
}
/**
* Returns SPARQL endpoint for the in-memory ontology model.
- *
+ *
* @return endpoint resource
*/
@Path("ns")
public Class getNamespace()
{
- return getProxyClass().orElse(Namespace.class);
+ return Namespace.class;
}
/**
* Returns second-level ontology documents.
- *
+ *
* @return namespace resource
*/
@Path("ns/{slug}/")
public Class getSubOntology()
{
- return getProxyClass().orElse(Namespace.class);
+ return Namespace.class;
}
-
+
/**
* Returns signup endpoint.
- *
+ *
* @return endpoint resource
*/
@Path("sign up")
public Class getSignUp()
{
- return getProxyClass().orElse(SignUp.class);
+ return SignUp.class;
}
-
+
/**
* Returns the access description endpoint.
- *
+ *
* @return endpoint resource
*/
@Path("access")
public Class getAccess()
{
- return getProxyClass().orElse(Access.class);
+ return Access.class;
}
/**
- * Returns the access description endpoint.
- *
+ * Returns the access request endpoint.
+ *
* @return endpoint resource
*/
@Path("access/request")
public Class getAccessRequest()
{
- return getProxyClass().orElse(AccessRequest.class);
+ return AccessRequest.class;
}
/**
@@ -179,31 +131,31 @@ public Class getAccessRequest()
@Path("uploads/{sha1sum}")
public Class getFileItem()
{
- return getProxyClass().orElse(com.atomgraph.linkeddatahub.resource.upload.Item.class);
+ return com.atomgraph.linkeddatahub.resource.upload.Item.class;
}
/**
* Returns the endpoint for synchronous RDF imports.
- *
+ *
* @return endpoint resource
*/
@Path("add")
public Class getAddEndpoint()
{
- return getProxyClass().orElse(Add.class);
+ return Add.class;
}
-
+
/**
* Returns the endpoint for synchronous RDF imports with a CONSTRUCT query transformation.
- *
+ *
* @return endpoint resource
*/
@Path("transform")
public Class getTransformEndpoint()
{
- return getProxyClass().orElse(Transform.class);
+ return Transform.class;
}
-
+
/**
* Returns the endpoint for container generation.
*
@@ -212,9 +164,9 @@ public Class getTransformEndpoint()
@Path("generate")
public Class getGenerateEndpoint()
{
- return getProxyClass().orElse(Generate.class);
+ return Generate.class;
}
-
+
/**
* Returns the endpoint that allows clearing ontologies from cache by URI.
*
@@ -223,7 +175,7 @@ public Class getGenerateEndpoint()
@Path("clear")
public Class getClearEndpoint()
{
- return getProxyClass().orElse(ClearOntology.class);
+ return ClearOntology.class;
}
/**
@@ -234,7 +186,7 @@ public Class getClearEndpoint()
@Path("packages/install")
public Class getInstallPackageEndpoint()
{
- return getProxyClass().orElse(InstallPackage.class);
+ return InstallPackage.class;
}
/**
@@ -245,7 +197,7 @@ public Class getInstallPackageEndpoint()
@Path("packages/uninstall")
public Class getUninstallPackageEndpoint()
{
- return getProxyClass().orElse(UninstallPackage.class);
+ return UninstallPackage.class;
}
/**
@@ -256,7 +208,7 @@ public Class getUninstallPackageEndpoint()
@Path("settings")
public Class getSettingsEndpoint()
{
- return getProxyClass().orElse(Settings.class);
+ return Settings.class;
}
/**
@@ -269,35 +221,5 @@ public Class getDocumentClass()
{
return DocumentHierarchyGraphStoreImpl.class;
}
-
- /**
- * Returns request URI information.
- *
- * @return URI info
- */
- public UriInfo getUriInfo()
- {
- return uriInfo;
- }
- /**
- * Returns the matched dataset (optional).
- *
- * @return optional dataset
- */
- public Optional getDataset()
- {
- return dataset;
- }
-
- /**
- * Returns the system application.
- *
- * @return JAX-RS application
- */
- public com.atomgraph.linkeddatahub.Application getSystem()
- {
- return system;
- }
-
}
diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/ProxiedGraph.java b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/ProxiedGraph.java
deleted file mode 100644
index f9944e214..000000000
--- a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/ProxiedGraph.java
+++ /dev/null
@@ -1,579 +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.client.MediaTypes;
-import com.atomgraph.client.util.DataManager;
-import com.atomgraph.client.util.HTMLMediaTypePredicate;
-import com.atomgraph.client.vocabulary.AC;
-import com.atomgraph.core.exception.BadGatewayException;
-import com.atomgraph.core.util.ModelUtils;
-import com.atomgraph.core.util.ResultSetUtils;
-import com.atomgraph.linkeddatahub.apps.model.Dataset;
-import com.atomgraph.linkeddatahub.client.GraphStoreClient;
-import com.atomgraph.linkeddatahub.client.filter.auth.IDTokenDelegationFilter;
-import com.atomgraph.linkeddatahub.client.filter.auth.WebIDDelegationFilter;
-import com.atomgraph.linkeddatahub.model.Service;
-import com.atomgraph.linkeddatahub.server.security.AgentContext;
-import com.atomgraph.linkeddatahub.server.security.IDTokenSecurityContext;
-import com.atomgraph.linkeddatahub.server.security.WebIDSecurityContext;
-import java.net.URI;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-import java.util.Optional;
-import jakarta.inject.Inject;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.ws.rs.ForbiddenException;
-import jakarta.ws.rs.NotAllowedException;
-import jakarta.ws.rs.Consumes;
-import jakarta.ws.rs.NotAcceptableException;
-import jakarta.ws.rs.NotFoundException;
-import jakarta.ws.rs.PATCH;
-import jakarta.ws.rs.POST;
-import jakarta.ws.rs.PUT;
-import jakarta.ws.rs.ProcessingException;
-import jakarta.ws.rs.QueryParam;
-import jakarta.ws.rs.client.Entity;
-import jakarta.ws.rs.client.Invocation;
-import jakarta.ws.rs.client.WebTarget;
-import jakarta.ws.rs.container.ContainerRequestContext;
-import jakarta.ws.rs.core.Context;
-import jakarta.ws.rs.core.EntityTag;
-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.StatusType;
-import jakarta.ws.rs.core.SecurityContext;
-import jakarta.ws.rs.core.UriInfo;
-import jakarta.ws.rs.core.Variant;
-import jakarta.ws.rs.ext.Providers;
-import org.apache.jena.query.ResultSet;
-import org.apache.jena.query.ResultSetRewindable;
-import org.apache.jena.rdf.model.Model;
-import org.apache.jena.riot.Lang;
-import org.apache.jena.riot.RDFLanguages;
-import org.apache.jena.riot.resultset.ResultSetReaderRegistry;
-import org.apache.jena.util.FileManager;
-import org.glassfish.jersey.media.multipart.FormDataMultiPart;
-import org.glassfish.jersey.message.internal.MessageBodyProviderNotFoundException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * JAX-RS resource that proxies Linked Data documents.
- * It uses an HTTP client to dereference URIs and sends back the client response.
- *
- * @author {@literal Martynas Jusevičius }
- */
-public class ProxiedGraph extends com.atomgraph.client.model.impl.ProxiedGraph
-{
-
- private static final Logger log = LoggerFactory.getLogger(ProxiedGraph.class);
-
- private final UriInfo uriInfo;
- private final ContainerRequestContext crc;
- private final com.atomgraph.linkeddatahub.apps.model.Application application;
- private final Service service;
- private final DataManager dataManager;
- private final Optional agentContext;
- private final MediaType[] readableMediaTypes;
- private final Providers providers;
- private final com.atomgraph.linkeddatahub.Application system;
-
- /**
- * Constructs the resource.
- *
- * @param uriInfo current request URI info
- * @param request current request
- * @param httpHeaders HTTP header info
- * @param mediaTypes registry of readable/writable media types
- * @param application current application
- * @param service application's SPARQL service
- * @param securityContext JAX-RS security context
- * @param crc request context
- * @param system system application
- * @param httpServletRequest servlet request
- * @param dataManager RDFdata manager
- * @param agentContext authenticated agent's context
- * @param providers registry of JAX-RS providers
- * @param dataset optional dataset
- */
- @Inject
- 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,
- @Context Providers providers, Optional dataset)
- {
- this(uriInfo, request, httpHeaders, mediaTypes, application, service, securityContext, crc,
- uriInfo.getQueryParameters().getFirst(AC.uri.getLocalName()) == null ?
- dataset.isEmpty() ? null : dataset.get().getProxied(uriInfo.getAbsolutePath())
- :
- URI.create(uriInfo.getQueryParameters().getFirst(AC.uri.getLocalName())),
- uriInfo.getQueryParameters().getFirst(AC.endpoint.getLocalName()) == null ? null : URI.create(uriInfo.getQueryParameters().getFirst(AC.endpoint.getLocalName())),
- uriInfo.getQueryParameters().getFirst(AC.query.getLocalName()) == null ? null : uriInfo.getQueryParameters().getFirst(AC.query.getLocalName()),
- uriInfo.getQueryParameters().getFirst(AC.accept.getLocalName()) == null ? null : MediaType.valueOf(uriInfo.getQueryParameters().getFirst(AC.accept.getLocalName())),
- uriInfo.getQueryParameters().getFirst(AC.mode.getLocalName()) == null ? null : URI.create(uriInfo.getQueryParameters().getFirst(AC.mode.getLocalName())),
- system, httpServletRequest, dataManager, agentContext, providers);
- }
-
- /**
- * Constructs the resource.
- *
- * @param uriInfo current request URI info
- * @param request current request
- * @param httpHeaders HTTP header info
- * @param mediaTypes registry of readable/writable media types
- * @param application current application
- * @param service application's SPARQL service
- * @param securityContext JAX-RS security context
- * @param crc request context
- * @param uri Linked Data URI
- * @param endpoint SPARQL endpoint URI
- * @param query SPARQL query
- * @param accept accept URL param
- * @param mode mode URL param
- * @param system system application
- * @param httpServletRequest servlet request
- * @param dataManager RDFdata manager
- * @param agentContext authenticated agent's context
- * @param providers registry of JAX-RS providers
- */
- 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,
- com.atomgraph.linkeddatahub.Application system, @Context HttpServletRequest httpServletRequest, DataManager dataManager, Optional agentContext,
- @Context Providers providers)
- {
- super(uriInfo, request, httpHeaders, mediaTypes, uri, endpoint, query, accept, mode, system.getExternalClient(), httpServletRequest);
-
- this.uriInfo = uriInfo;
- this.application = application;
- this.service = service.get();
- this.crc = crc;
- this.dataManager = dataManager;
- this.agentContext = agentContext;
- this.providers = providers;
- this.system = system;
-
- List readableMediaTypesList = new ArrayList<>();
- readableMediaTypesList.addAll(mediaTypes.getReadable(Model.class));
- readableMediaTypesList.addAll(mediaTypes.getReadable(ResultSet.class));
- this.readableMediaTypes = readableMediaTypesList.toArray(MediaType[]::new);
-
- if (agentContext.isPresent())
- {
- if (agentContext.get() instanceof WebIDSecurityContext)
- super.getWebTarget().register(new WebIDDelegationFilter(agentContext.get().getAgent()));
-
- if (agentContext.get() instanceof IDTokenSecurityContext iDTokenSecurityContext)
- super.getWebTarget().register(new IDTokenDelegationFilter(agentContext.get().getAgent(),
- iDTokenSecurityContext.getJWTToken(), uriInfo.getBaseUri().getPath(), null));
- }
- }
-
- /**
- * Gets a request invocation builder for the given target.
- *
- * @param target web target
- * @return invocation builder
- */
- @Override
- public Invocation.Builder getBuilder(WebTarget target)
- {
- return target.request(getReadableMediaTypes()).
- header(HttpHeaders.USER_AGENT, getUserAgentHeaderValue());
- }
-
- /**
- * Returns response for the given client response.
- * Handles responses without media type (e.g., 204 No Content).
- *
- * @param clientResponse client response
- * @return response
- */
- @Override
- public Response getResponse(Response clientResponse)
- {
- if (clientResponse.getMediaType() == null) return Response.status(clientResponse.getStatus()).build();
-
- return getResponse(clientResponse, clientResponse.getStatusInfo());
- }
-
- public Response getResponse(Response clientResponse, StatusType statusType)
- {
- MediaType formatType = new MediaType(clientResponse.getMediaType().getType(), clientResponse.getMediaType().getSubtype()); // discard charset param
- Lang lang = RDFLanguages.contentTypeToLang(formatType.toString());
-
- // check if we got SPARQL results first
- if (lang != null && ResultSetReaderRegistry.isRegistered(lang))
- {
- ResultSetRewindable results = clientResponse.readEntity(ResultSetRewindable.class);
- return getResponse(results, statusType);
- }
-
- // fallback to RDF graph
- Model description = clientResponse.readEntity(Model.class);
- return getResponse(description, statusType);
- }
-
- /**
- * Returns response for the given RDF model.
- * TO-DO: move down to Web-Client
- *
- * @param model RDF model
- * @param statusType response status
- * @return response object
- */
- public Response getResponse(Model model, StatusType statusType)
- {
- List variants = com.atomgraph.core.model.impl.Response.getVariants(getWritableMediaTypes(Model.class),
- getLanguages(),
- getEncodings());
-
- return new com.atomgraph.core.model.impl.Response(getRequest(),
- model,
- null,
- new EntityTag(Long.toHexString(ModelUtils.hashModel(model))),
- variants,
- new HTMLMediaTypePredicate()).
- getResponseBuilder().
- status(statusType).
- build();
- }
-
- /**
- * Returns response for the given SPARQL results.
- * TO-DO: move down to Web-Client
- *
- * @param resultSet SPARQL results
- * @param statusType response status
- * @return response object
- */
- public Response getResponse(ResultSetRewindable resultSet, StatusType statusType)
- {
- long hash = ResultSetUtils.hashResultSet(resultSet);
- resultSet.reset();
-
- List variants = com.atomgraph.core.model.impl.Response.getVariants(getWritableMediaTypes(ResultSet.class),
- getLanguages(),
- getEncodings());
-
- return new com.atomgraph.core.model.impl.Response(getRequest(),
- resultSet,
- null,
- new EntityTag(Long.toHexString(hash)),
- variants,
- new HTMLMediaTypePredicate()).
- getResponseBuilder().
- status(statusType).
- build();
- }
-
- /**
- * Forwards GET request and returns response from remote resource.
- *
- * @param target target URI
- * @param builder invocation builder
- * @return response
- */
- @Override
- public Response get(WebTarget target, Invocation.Builder builder)
- {
- // 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(target.getUri().toString()) ||
- (getDataManager().isResolvingMapped() && getDataManager().isMapped(target.getUri().toString()))) // read mapped URIs (such as system ontologies) from a file
- {
- if (log.isDebugEnabled()) log.debug("hasCachedModel({}): {}", target.getUri(), ((FileManager)getDataManager()).hasCachedModel(target.getUri().toString()));
- if (log.isDebugEnabled()) log.debug("isMapped({}): {}", target.getUri(), getDataManager().isMapped(target.getUri().toString()));
- return getResponse(getDataManager().loadModel(target.getUri().toString()));
- }
-
- if (!getSystem().isEnableLinkedDataProxy()) throw new NotAllowedException("Linked Data proxy not enabled");
- // LNK-009: Validate that proxied URI is not internal/private (SSRF protection)
- getSystem().getURLValidator().validate(target.getUri());
-
- return super.get(target, builder);
- }
-
- /**
- * Forwards POST request with SPARQL query body and returns response from remote resource.
- *
- * @param sparqlQuery SPARQL query string
- * @return response
- */
- @POST
- @Consumes(com.atomgraph.core.MediaType.APPLICATION_SPARQL_QUERY)
- public Response post(String sparqlQuery)
- {
- if (getWebTarget() == null) throw new NotFoundException("Resource URI not supplied");
- // LNK-009: Validate that proxied URI is not internal/private (SSRF protection)
- getSystem().getURLValidator().validate(getWebTarget().getUri());
-
- if (log.isDebugEnabled()) log.debug("POSTing SPARQL query to URI: {}", getWebTarget().getUri());
-
- try (Response cr = getWebTarget().request()
- .accept(getReadableMediaTypes())
- .post(Entity.entity(sparqlQuery, com.atomgraph.core.MediaType.APPLICATION_SPARQL_QUERY_TYPE)))
- {
- return getResponse(cr);
- }
- catch (MessageBodyProviderNotFoundException ex)
- {
- if (log.isWarnEnabled()) log.debug("Dereferenced URI {} returned non-RDF media type", getWebTarget().getUri());
- throw new NotAcceptableException(ex);
- }
- catch (ProcessingException ex)
- {
- if (log.isWarnEnabled()) log.debug("Could not dereference URI: {}", getWebTarget().getUri());
- throw new BadGatewayException(ex);
- }
- }
-
- /**
- * Forwards POST request with form data and returns response from remote resource.
- *
- * @param formData form data string
- * @return response
- */
- @POST
- @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
- public Response postForm(String formData)
- {
- if (getWebTarget() == null) throw new NotFoundException("Resource URI not supplied");
- // LNK-009: Validate that proxied URI is not internal/private (SSRF protection)
- getSystem().getURLValidator().validate(getWebTarget().getUri());
-
- if (log.isDebugEnabled()) log.debug("POSTing form data to URI: {}", getWebTarget().getUri());
-
- try (Response cr = getWebTarget().request()
- .accept(getReadableMediaTypes())
- .post(Entity.entity(formData, MediaType.APPLICATION_FORM_URLENCODED_TYPE)))
- {
- return getResponse(cr);
- }
- catch (MessageBodyProviderNotFoundException ex)
- {
- if (log.isWarnEnabled()) log.debug("Dereferenced URI {} returned non-RDF media type", getWebTarget().getUri());
- throw new NotAcceptableException(ex);
- }
- catch (ProcessingException ex)
- {
- if (log.isWarnEnabled()) log.debug("Could not dereference URI: {}", getWebTarget().getUri());
- throw new BadGatewayException(ex);
- }
- }
-
- /**
- * Forwards PATCH request with SPARQL update body and returns response from remote resource.
- *
- * @param sparqlUpdate SPARQL update string
- * @return response
- */
- @PATCH
- @Consumes(com.atomgraph.core.MediaType.APPLICATION_SPARQL_UPDATE)
- public Response patch(String sparqlUpdate)
- {
- if (getWebTarget() == null) throw new NotFoundException("Resource URI not supplied");
- // LNK-009: Validate that proxied URI is not internal/private (SSRF protection)
- getSystem().getURLValidator().validate(getWebTarget().getUri());
-
- if (log.isDebugEnabled()) log.debug("PATCHing SPARQL update to URI: {}", getWebTarget().getUri());
-
- try (Response cr = getWebTarget().request()
- .accept(getReadableMediaTypes())
- .method("PATCH", Entity.entity(sparqlUpdate, com.atomgraph.core.MediaType.APPLICATION_SPARQL_UPDATE_TYPE)))
- {
- return getResponse(cr);
- }
- catch (MessageBodyProviderNotFoundException ex)
- {
- if (log.isWarnEnabled()) log.debug("Dereferenced URI {} returned non-RDF media type", getWebTarget().getUri());
- throw new NotAcceptableException(ex);
- }
- catch (ProcessingException ex)
- {
- if (log.isWarnEnabled()) log.debug("Could not dereference URI: {}", getWebTarget().getUri());
- throw new BadGatewayException(ex);
- }
- }
-
- /**
- * Forwards a multipart POST request returns RDF response from remote resource.
- *
- * @param multiPart form data
- * @return response
- */
- @POST
- @Consumes(MediaType.MULTIPART_FORM_DATA)
- public Response postMultipart(FormDataMultiPart multiPart)
- {
- if (!getSystem().isEnableLinkedDataProxy()) throw new NotAllowedException("Linked Data proxy not enabled");
- if (getWebTarget() == null) throw new NotFoundException("Resource URI not supplied"); // cannot throw Exception in constructor: https://github.com/eclipse-ee4j/jersey/issues/4436
- // LNK-009: Validate that proxied URI is not internal/private (SSRF protection)
- getSystem().getURLValidator().validate(getWebTarget().getUri());
-
- try (Response cr = getWebTarget().request().
- accept(getMediaTypes().getReadable(Model.class).toArray(jakarta.ws.rs.core.MediaType[]::new)).
- post(Entity.entity(multiPart, multiPart.getMediaType())))
- {
- if (log.isDebugEnabled()) log.debug("POSTing multipart data to URI: {}", getWebTarget().getUri());
- return getResponse(cr);
- }
- }
-
- /**
- * Forwards a multipart PUT request returns RDF response from remote resource.
- *
- * @param multiPart form data
- * @return response
- */
- @PUT
- @Consumes(MediaType.MULTIPART_FORM_DATA)
- public Response putMultipart(FormDataMultiPart multiPart)
- {
- if (!getSystem().isEnableLinkedDataProxy()) throw new NotAllowedException("Linked Data proxy not enabled");
- if (getWebTarget() == null) throw new NotFoundException("Resource URI not supplied"); // cannot throw Exception in constructor: https://github.com/eclipse-ee4j/jersey/issues/4436
- // LNK-009: Validate that proxied URI is not internal/private (SSRF protection)
- getSystem().getURLValidator().validate(getWebTarget().getUri());
-
- try (Response cr = getWebTarget().request().
- accept(getMediaTypes().getReadable(Model.class).toArray(jakarta.ws.rs.core.MediaType[]::new)).
- put(Entity.entity(multiPart, multiPart.getMediaType())))
- {
- if (log.isDebugEnabled()) log.debug("PUTing multipart data to URI: {}", getWebTarget().getUri());
- return getResponse(cr);
- }
- }
-
- /**
- * Returns a list of supported languages.
- *
- * @return list of languages
- */
- @Override
- public List getLanguages()
- {
- return getSystem().getSupportedLanguages();
- }
-
- /**
- * Returns the current application.
- *
- * @return application resource
- */
- public com.atomgraph.linkeddatahub.apps.model.Application getApplication()
- {
- return application;
- }
-
- /**
- * Returns the SPARQL service of the current application.
- *
- * @return service resource
- */
- public Service getService()
- {
- return service;
- }
-
- /**
- * Returns request context.
- *
- * @return request context
- */
- public ContainerRequestContext getContainerRequestContext()
- {
- return crc;
- }
-
- /**
- * Returns request URI information.
- *
- * @return URI info
- */
- @Override
- public UriInfo getUriInfo()
- {
- return uriInfo;
- }
-
- /**
- * Returns RDF data manager.
- *
- * @return RDF data manager
- */
- public DataManager getDataManager()
- {
- return dataManager;
- }
-
- /**
- * Returns the context of authenticated agent.
- *
- * @return agent context
- */
- public Optional getAgentContext()
- {
- return agentContext;
- }
-
- /**
- * Returns readable media types.
- *
- * @return media types
- */
- @Override
- public MediaType[] getReadableMediaTypes()
- {
- return readableMediaTypes;
- }
-
- /**
- * 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 the value of the User-Agent request header.
- *
- * @return header value
- */
- public String getUserAgentHeaderValue()
- {
- return GraphStoreClient.USER_AGENT;
- }
-
-}
diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/chart.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/chart.xsl
index ac5f2cccf..0f17e1fdb 100644
--- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/chart.xsl
+++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/chart.xsl
@@ -812,6 +812,8 @@ exclude-result-prefixes="#all"
+
+
+
+
-
+
\ No newline at end of file
diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/object.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/object.xsl
index 56b0d1d5d..a34ab9727 100644
--- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/object.xsl
+++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/object.xsl
@@ -112,6 +112,7 @@ exclude-result-prefixes="#all"
+
@@ -354,6 +355,7 @@ exclude-result-prefixes="#all"
+
diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/query.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/query.xsl
index 5fa27b1ce..d1aef2a84 100644
--- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/query.xsl
+++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/query.xsl
@@ -610,8 +610,7 @@ exclude-result-prefixes="#all"
-
-
+
+
+
-
-
+
+
+
+
+
-
+
-
-
@@ -915,7 +918,7 @@ exclude-result-prefixes="#all"
'object-var-name': $object-var-name
}"/>
@@ -1769,6 +1772,8 @@ exclude-result-prefixes="#all"
+
+
@@ -1784,7 +1789,9 @@ exclude-result-prefixes="#all"
-
+
+
+
-
+
@@ -1969,13 +1976,15 @@ exclude-result-prefixes="#all"
+
+
-
+