diff --git a/CLAUDE.md b/CLAUDE.md index ad076f9f..f5d4c8d5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,10 +36,10 @@ Release workflow uses `release.sh` which runs `mvn release:clean release:prepare ### XSLT Rendering -Stylesheets live in `src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/`. The main entry points are: +Stylesheets live in `src/main/webapp/static/com/atomgraph/client/xsl/`. The main entry points are: - `external-layout.xsl` — for external Linked Data (default in `web.xml`) - `internal-layout.xsl` — for internal graph stores -- `layout.xsl` — Bootstrap 2.3.2 based, handles RDF-to-HTML transformation +- `layout.xsl` — handles RDF-to-HTML transformation, emitting the LinkedDataHub design system's `ac-*` primitives against the vendored kit in `static/com/atomgraph/client/css/` XSLT extension functions in `writer/function/` (`Construct`, `ConstructForClass`, `UUID`) are registered in `Application.java` and callable from stylesheets. @@ -70,3 +70,9 @@ Multi-stage build targeting Tomcat 10.1.4. The `entrypoint.sh` transforms `conte - **AtomGraph Core 4.1.x** — shared infrastructure (DataManager base, StartupListener) - **Jakarta Servlet 5.0 / Jersey** — JAX-RS implementation - **Java 21**, packaged as `ROOT.war` for Tomcat + +## XSLT Mode Namespaces + +- `xhtml:` modes are childless element primitives: the mode's local name is the XHTML element it emits (`xhtml:Input`, `xhtml:Anchor`, `xhtml:Option`, `xhtml:Title`, `xhtml:Meta`, `xhtml:Script`) — one element, attributes as parameters, content at most a text label, no design-system classes baked in. +- Emitters with internal element structure are components and take `ac:` with the design system's component name (`ac:AppShell`, `ac:Head`, `ac:Stylesheets`, the `ac:ResultsTable*` family, `ac:PropertyListLabel`/`ac:PropertyListValue`, `ac:FieldShell`). Downstream consumers (LinkedDataHub) name their own components in their own namespace. +- The unnamed mode is the value leaf: mode-less `apply-templates` renders an object/literal via the default-mode value emitters. diff --git a/README.md b/README.md index 81729166..19a354c3 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Features What AWC provides for users as out-of-the-box generic features: * loading RDF data from remote Linked Data sources -* multilingual, responsive user interface built with Twitter Bootstrap (currently [2.3.2](https://getbootstrap.com/2.3.2/)) +* multilingual, responsive user interface built on the [LinkedDataHub design system](https://github.com/AtomGraph/LinkedDataHub) — vendored design tokens and `ac-*` component primitives, no CSS framework * multiple RDF rendering modes (currently item/list/table/map/graph/grid) * RDF editing mode based on [RDF/POST](https://atomgraph.github.io/RDF-POST/) encoding * SPARQL endpoint with interactive results @@ -45,7 +45,7 @@ It accepts the following environment variables (that become webapp context param Requires Java 21. -Run Web-Client with the [default XSLT stylesheet](https://github.com/AtomGraph/Web-Client/blob/master/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/layout.xsl) like this: +Run Web-Client with the [default XSLT stylesheet](https://github.com/AtomGraph/Web-Client/blob/master/src/main/webapp/static/com/atomgraph/client/xsl/layout.xsl) like this: docker run -p 8080:8080 atomgraph/web-client diff --git a/pom.xml b/pom.xml index e9087a8b..a28b0b99 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ com.atomgraph client - 5.0.5-SNAPSHOT + 6.0.0-SNAPSHOT ${packaging.type} AtomGraph Web-Client diff --git a/src/main/java/com/atomgraph/client/Application.java b/src/main/java/com/atomgraph/client/Application.java index 4208615f..79b544b4 100644 --- a/src/main/java/com/atomgraph/client/Application.java +++ b/src/main/java/com/atomgraph/client/Application.java @@ -30,14 +30,15 @@ import com.atomgraph.client.mapper.ClientErrorExceptionMapper; import com.atomgraph.client.mapper.NotFoundExceptionMapper; import com.atomgraph.client.mapper.RiotExceptionMapper; -import com.atomgraph.client.model.impl.ProxiedGraph; +import com.atomgraph.client.filter.request.ProxyRequestFilter; +import com.atomgraph.client.resource.Root; import com.atomgraph.core.provider.QueryParamProvider; import com.atomgraph.core.io.ResultSetProvider; import com.atomgraph.core.io.UpdateRequestProvider; import com.atomgraph.client.vocabulary.AC; import com.atomgraph.client.writer.ModelXSLTWriter; import com.atomgraph.client.writer.ResultSetXSLTWriter; -import com.atomgraph.client.writer.function.ConstructForClass; +import com.atomgraph.client.interceptor.RDFPostMediaTypeInterceptor; import com.atomgraph.client.writer.function.UUID; import com.atomgraph.core.client.GraphStoreClient; import com.atomgraph.core.io.ModelProvider; @@ -129,7 +130,6 @@ public Application(final MediaTypes mediaTypes, final Client client, final Integ RDFWriterRegistry.register(Lang.RDFXML, RDFFormat.RDFXML_PLAIN); xsltProc.registerExtensionFunction(new UUID()); - xsltProc.registerExtensionFunction(new ConstructForClass(xsltProc, resolver.getRepository())); try { @@ -150,9 +150,11 @@ public Application(final MediaTypes mediaTypes, final Client client, final Integ @PostConstruct public void init() { - register(ProxiedGraph.class); + register(Root.class); + register(ProxyRequestFilter.class); register(new HttpMethodOverrideFilter()); + register(new RDFPostMediaTypeInterceptor()); // for application/x-www-form-urlencoded register(new ModelProvider()); register(new ResultSetProvider()); register(new QueryParamProvider()); diff --git a/src/main/java/com/atomgraph/client/filter/request/ProxyRequestFilter.java b/src/main/java/com/atomgraph/client/filter/request/ProxyRequestFilter.java new file mode 100644 index 00000000..5fa37155 --- /dev/null +++ b/src/main/java/com/atomgraph/client/filter/request/ProxyRequestFilter.java @@ -0,0 +1,446 @@ +/** + * Copyright 2025 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.client.filter.request; + +import com.atomgraph.client.MediaTypes; +import com.atomgraph.client.util.HTMLMediaTypePredicate; +import com.atomgraph.core.exception.BadGatewayException; +import com.atomgraph.core.io.ModelProvider; +import com.atomgraph.core.util.ModelUtils; +import com.atomgraph.core.util.ResultSetUtils; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; +import jakarta.annotation.Priority; +import jakarta.inject.Inject; +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.NotAcceptableException; +import jakarta.ws.rs.Priorities; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.client.Client; +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.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.RiotException; +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 {@code ?uri=} proxy requests and short-circuits the pipeline + * via {@link ContainerRequestContext#abortWith(Response)}. The proxy is a global transport function, + * not a document operation, so it is not modelled as a resource class: the filter forwards the + * request method, entity stream and conditional headers to the target verbatim - no local entity + * parsing on writes (an RDF/POST form body reaches the origin as + * {@code application/x-www-form-urlencoded} for the origin to parse) - and converts the target's + * response for the original caller. + *

+ * External HTTP responses are dispatched on upstream {@code Content-Type} via Jena's live RIOT + * registry (the same predicate {@code ModelProvider.isReadable} consults, and unlike the + * {@code MediaTypes} snapshot it includes langs registered after class-loading, such as RDF/POST): + * RDF langs parse into a {@link Model} and SPARQL results langs into a {@link ResultSet}, both + * re-served through content negotiation - including (X)HTML via the XSLT writers, which is this + * application's purpose. Error responses keep the origin's status and validators but their RDF + * diagnostic bodies re-serve through the same negotiation, so a browser gets the rendered error + * page rather than raw RDF bytes (a rejected write - 412 on a stale {@code If-Match}, 401/403 on + * an unauthorized delta - must surface as that status); non-RDF and unparseable bodies relay verbatim. + * + * @author Martynas Jusevičius {@literal } + */ +@PreMatching +@Priority(Priorities.USER + 50) // after HttpMethodOverrideFilter (Priorities.USER), so ?_method= is already applied +public class ProxyRequestFilter implements ContainerRequestFilter +{ + + private static final Logger log = LoggerFactory.getLogger(ProxyRequestFilter.class); + private static final Pattern LINK_SPLITTER = Pattern.compile(",(?=\\s*<)"); + /** + * End-to-end response headers forwarded verbatim from the upstream. Excludes hop-by-hop headers + * (RFC 7230 §6.1), framing headers re-emitted by the container, origin-bound security headers + * (CSP, HSTS, CORS), cookies, and {@code Content-Type}/{@code Link} which are set explicitly. + */ + private static final Set FORWARDED_RESPONSE_HEADERS = Set.of( + HttpHeaders.ETAG, + HttpHeaders.LAST_MODIFIED, + HttpHeaders.CACHE_CONTROL, + HttpHeaders.VARY, + HttpHeaders.EXPIRES, + HttpHeaders.CONTENT_LANGUAGE, + HttpHeaders.CONTENT_DISPOSITION, + HttpHeaders.CONTENT_LOCATION, + HttpHeaders.LOCATION, + HttpHeaders.RETRY_AFTER, + "Age"); + /** + * Conditional request headers forwarded verbatim to the upstream so preconditions are evaluated + * at the origin: {@code If-Match}/{@code If-Unmodified-Since} carry optimistic-concurrency + * validators on writes, {@code If-None-Match}/{@code If-Modified-Since} carry cache validation on + * reads. Excludes {@code Authorization}/{@code Cookie} and {@code Range}, whose byte offsets do + * not survive the Model re-serialization the proxy performs. + */ + private static final Set FORWARDED_REQUEST_HEADERS = Set.of( + HttpHeaders.IF_MATCH, + HttpHeaders.IF_NONE_MATCH, + HttpHeaders.IF_MODIFIED_SINCE, + HttpHeaders.IF_UNMODIFIED_SINCE); + + @Inject MediaTypes mediaTypes; + @Inject Client client; + @Context Request request; + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException + { + URI targetURI = resolveTargetURI(requestContext); + if (targetURI == null) return; // not a proxy request - the root resource handles it + + // the ?accept= query param overrides content negotiation (used by the RDF export links) + String acceptParam = requestContext.getUriInfo().getQueryParameters().getFirst("accept"); + if (acceptParam != null) requestContext.getHeaders().putSingle(HttpHeaders.ACCEPT, acceptParam); + + if (log.isDebugEnabled()) log.debug("Proxying {} {} → {}", requestContext.getMethod(), requestContext.getUriInfo().getRequestUri(), targetURI); + requestContext.abortWith(proxy(requestContext, getClient().target(targetURI))); + } + + /** + * Resolves the proxy target URI from the {@code ?uri=} query parameter, with the + * {@code #fragment} stripped (servers do not receive fragment identifiers). + * Returns null if this request should not be proxied. + * + * @param requestContext the current request context + * @return target URI to proxy to, or null + */ + protected URI resolveTargetURI(ContainerRequestContext requestContext) + { + String uriParam = requestContext.getUriInfo().getQueryParameters().getFirst("uri"); + if (uriParam == null) return null; + + URI targetURI = URI.create(uriParam); + 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 + } + } + + return targetURI; + } + + /** + * Forwards the current request to the target and converts the target's response. + * + * @param requestContext the current request context + * @param target proxy target + * @return response for the original caller + */ + protected Response proxy(ContainerRequestContext requestContext, WebTarget target) + { + try + { + Invocation.Builder builder = target.request(getReadableMediaTypes()); + + // forward conditional request headers so preconditions reach the origin, which owns the + // validators - without this the origin sees an unconditional request and a proxied If-Match + // write silently loses its optimistic-concurrency guard + for (String name : FORWARDED_REQUEST_HEADERS) + { + String value = requestContext.getHeaderString(name); + if (value != null) builder.header(name, value); + } + + Response clientResponse = requestContext.hasEntity() + ? builder.method(requestContext.getMethod(), + Entity.entity(requestContext.getEntityStream(), requestContext.getMediaType())) + : builder.method(requestContext.getMethod()); + + try (clientResponse) + { + // special case for http <-> https 301/303 redirection, which the connector does not follow across schemes + if (("GET".equalsIgnoreCase(requestContext.getMethod()) || HttpMethod.HEAD.equalsIgnoreCase(requestContext.getMethod())) && + (clientResponse.getStatusInfo().toEnum().equals(Response.Status.SEE_OTHER) || clientResponse.getStatusInfo().toEnum().equals(Response.Status.MOVED_PERMANENTLY)) && + ((target.getUri().getScheme().equals("http") && clientResponse.getLocation().getScheme().equals("https")) || + (target.getUri().getScheme().equals("https") && clientResponse.getLocation().getScheme().equals("http")))) + return proxy(requestContext, getClient().target(clientResponse.getLocation())); + + return getResponse(clientResponse, target.getUri(), requestContext.getMethod()); + } + } + catch (MessageBodyProviderNotFoundException ex) + { + if (log.isWarnEnabled()) log.warn("Proxied URI {} returned non-RDF media type", target.getUri()); + throw new NotAcceptableException(ex); + } + catch (RiotException ex) + { + if (log.isWarnEnabled()) log.warn("Proxied URI {} returned body typed as RDF but unparseable", target.getUri()); + throw new BadGatewayException(ex); + } + catch (ProcessingException ex) + { + if (log.isWarnEnabled()) log.warn("Could not dereference proxied URI: {}", target.getUri()); + throw new BadGatewayException(ex); + } + } + + /** + * Converts the proxy target's HTTP response into a JAX-RS response for the original caller. + * RDF and SPARQL results bodies parse and re-serve through content negotiation (including + * (X)HTML via the XSLT writers); HEAD, error and non-RDF responses relay verbatim. + * + * @param clientResponse response from the proxy target + * @param targetURI upstream URI (used as the parse base URI hint for {@code ModelProvider}) + * @param method HTTP method + * @return JAX-RS response to return to the original caller + */ + protected Response getResponse(Response clientResponse, URI targetURI, String method) + { + // HEAD responses have no body by HTTP semantics. Routing them through the typed branches + // below would parse an empty entity into an empty Model/ResultSet, then re-stamp + // ETag/Last-Modified off that empty value - producing validators that disagree with the + // upstream GET. Forward the upstream headers (including ETag) verbatim instead. + if (HttpMethod.HEAD.equalsIgnoreCase(method)) + { + Response.ResponseBuilder rb = Response.status(clientResponse.getStatus()); + if (clientResponse.getMediaType() != null) rb.type(clientResponse.getMediaType()); + return overlayHeaders(rb.build(), clientResponse, true); + } + + if (clientResponse.getMediaType() == null) + { + Response.ResponseBuilder rb = Response.status(clientResponse.getStatus()); + return overlayHeaders(rb.build(), clientResponse, true); + } + + // error responses keep the origin's status but re-serve an RDF diagnostic body through content + // negotiation - this client's consumer is a browser, and relaying e.g. RDF/Thrift bytes verbatim + // on a 403 gives it nothing it can render. The entity is buffered first so a body that is not + // the RDF its Content-Type claims falls back to the verbatim relay instead of masking the + // origin's status as 502/406. A proxied write that the origin rejects (412 on a stale If-Match, + // 401/403 on an unauthorized delta) must reach the client as that status either way, with the + // origin's validators forwarded + Response.Status.Family family = clientResponse.getStatusInfo().getFamily(); + if (family == Response.Status.Family.CLIENT_ERROR || family == Response.Status.Family.SERVER_ERROR) + { + clientResponse.bufferEntity(); + + MediaType errorType = clientResponse.getMediaType(); + if (RDFLanguages.contentTypeToLang(new MediaType(errorType.getType(), errorType.getSubtype()).toString()) != null) + try + { + clientResponse.getHeaders().putSingle(ModelProvider.REQUEST_URI_HEADER, targetURI.toString()); + Model errorModel = clientResponse.readEntity(Model.class); + return overlayHeaders(getResponse(errorModel, clientResponse.getStatusInfo()), clientResponse, true); + } + catch (ProcessingException | RiotException ex) + { + if (log.isWarnEnabled()) log.warn("Error body from proxied URI {} typed as RDF but unparseable - relaying verbatim", targetURI); + } + + Response.ResponseBuilder rb = Response.status(clientResponse.getStatus()). + type(clientResponse.getMediaType()). + entity(clientResponse.readEntity(InputStream.class)); + return overlayHeaders(rb.build(), clientResponse, true); + } + + // dispatch on the live Jena RIOT registry - the same predicate ModelProvider.isReadable uses, + // so any RDF lang Jersey can read into a Model routes to the Model branch, including langs + // registered after the MediaTypes static snapshot was captured (e.g. RDF/POST) + MediaType upstreamCT = clientResponse.getMediaType(); + MediaType formatType = new MediaType(upstreamCT.getType(), upstreamCT.getSubtype()); // strip charset + Lang lang = RDFLanguages.contentTypeToLang(formatType.toString()); + + if (lang != null && ResultSetReaderRegistry.isRegistered(lang)) + { + ResultSetRewindable results = clientResponse.readEntity(ResultSetRewindable.class); + return overlayHeaders(getResponse(results, clientResponse.getStatusInfo()), clientResponse, false); + } + + if (lang != null) + { + // base URI hint so ModelProvider resolves relative IRIs against the upstream URI + clientResponse.getHeaders().putSingle(ModelProvider.REQUEST_URI_HEADER, targetURI.toString()); + Model model = clientResponse.readEntity(Model.class); + // forward the origin's validators (replacing the ones the Model builder stamps off the re-serialized + // bytes): a client editing the proxied document sends If-Match through this proxy to the origin, which + // compares against its own ETag - a re-serialization validator would 412 every proxied write + return overlayHeaders(getResponse(model, clientResponse.getStatusInfo()), clientResponse, true); + } + + // upstream is neither RDF nor SPARQL results - pipe raw bytes + // buffer so the stream remains readable after try-with-resources closes the client response + clientResponse.bufferEntity(); + InputStream entity = clientResponse.readEntity(InputStream.class); + + Response.ResponseBuilder rb = Response.status(clientResponse.getStatus()). + type(upstreamCT). + entity(entity); + + return overlayHeaders(rb.build(), clientResponse, true); + } + + /** + * Copies the upstream {@code Link} and end-to-end cache/content headers onto the given + * built response, replacing any locally stamped values. {@code ETag}/{@code Last-Modified} + * are skipped when {@code copyValidators} is {@code false} (the ResultSet branch), where the + * builder-stamped validators stand. + * + * @param response the response built by the typed or raw branch + * @param clientResponse upstream response to copy headers from + * @param copyValidators whether to forward {@code ETag} and {@code Last-Modified} + * @return response with overlaid upstream headers + */ + protected Response overlayHeaders(Response response, Response clientResponse, boolean copyValidators) + { + Response.ResponseBuilder rb = Response.fromResponse(response); + + // forward all Link headers from the external response so the client receives remote hypermedia + String linkHeader = clientResponse.getHeaderString(HttpHeaders.LINK); + if (linkHeader != null) + for (String part : LINK_SPLITTER.split(linkHeader)) + rb.header(HttpHeaders.LINK, part.trim()); + + for (String name : FORWARDED_RESPONSE_HEADERS) + { + if (!copyValidators && (HttpHeaders.ETAG.equalsIgnoreCase(name) || HttpHeaders.LAST_MODIFIED.equalsIgnoreCase(name))) continue; + String value = clientResponse.getHeaderString(name); + if (value != null) rb.header(name, null).header(name, value); // replace, not append - the upstream value overlays any locally stamped one + } + + return rb.build(); + } + + /** + * Builds a response for the given RDF model with content negotiation, including (X)HTML. + * + * @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(getMediaTypes().getWritable(Model.class), + new ArrayList<>(), + new ArrayList<>()); + + 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(); + } + + /** + * Builds a response for the given SPARQL result set with content negotiation, including (X)HTML. + * + * @param resultSet SPARQL results (rewindable so we can hash without consuming) + * @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(getMediaTypes().getWritable(ResultSet.class), + new ArrayList<>(), + new ArrayList<>()); + + return new com.atomgraph.core.model.impl.Response(getRequest(), + resultSet, + null, + new EntityTag(Long.toHexString(hash)), + variants, + new HTMLMediaTypePredicate()). + getResponseBuilder(). + status(statusType). + build(); + } + + /** + * Returns the outbound {@code Accept} types: everything readable as a model or SPARQL results. + * + * @return readable media types + */ + protected MediaType[] getReadableMediaTypes() + { + List readable = new ArrayList<>(); + readable.addAll(getMediaTypes().getReadable(Model.class)); + readable.addAll(getMediaTypes().getReadable(ResultSet.class)); + return readable.toArray(MediaType[]::new); + } + + /** + * Returns the media types registry used for content negotiation and outbound {@code Accept} headers. + * + * @return media types + */ + public MediaTypes getMediaTypes() + { + return mediaTypes; + } + + /** + * Returns the HTTP client used to reach proxy targets. + * + * @return HTTP client + */ + public Client getClient() + { + return client; + } + + /** + * Returns the JAX-RS request. + * + * @return request + */ + public Request getRequest() + { + return request; + } + +} diff --git a/src/main/java/com/atomgraph/client/interceptor/RDFPostMediaTypeInterceptor.java b/src/main/java/com/atomgraph/client/interceptor/RDFPostMediaTypeInterceptor.java new file mode 100644 index 00000000..5d019ded --- /dev/null +++ b/src/main/java/com/atomgraph/client/interceptor/RDFPostMediaTypeInterceptor.java @@ -0,0 +1,66 @@ +/** + * 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.client.interceptor; + +import com.atomgraph.core.MediaType; +import com.atomgraph.core.riot.lang.TokenizerRDFPost; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import jakarta.annotation.Priority; +import jakarta.ws.rs.Priorities; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.ext.ReaderInterceptor; +import jakarta.ws.rs.ext.ReaderInterceptorContext; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Request interceptor that fixes RDF/POST media type. + * + * @author Martynas Jusevičius {@literal } + */ +@Priority(Priorities.ENTITY_CODER) +public class RDFPostMediaTypeInterceptor implements ReaderInterceptor +{ + + private static final Logger log = LoggerFactory.getLogger(RDFPostMediaTypeInterceptor.class); + + @Override + public Object aroundReadFrom(ReaderInterceptorContext context) throws IOException, WebApplicationException + { + // cannot use the RDF/POST-specific MediaType.APPLICATION_RDF_URLENCODED_TYPE because browsers do not support it as form/@enctype: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-enctype --> + if (context.getMediaType() != null && context.getMediaType().isCompatible(MediaType.APPLICATION_FORM_URLENCODED_TYPE)) + { + StringWriter writer = new StringWriter(); + IOUtils.copy(context.getInputStream(), writer, StandardCharsets.UTF_8); + + String formData = writer.toString(); + + if (formData.startsWith(TokenizerRDFPost.RDF)) + // replace the generic "application/x-www-form-urlencoded" media type with RDF/POST + context.setMediaType(MediaType.APPLICATION_RDF_URLENCODED_TYPE); + + context.setInputStream(new ByteArrayInputStream(formData.getBytes(StandardCharsets.UTF_8))); // restore the request entity + } + + return context.proceed(); + } + +} diff --git a/src/main/java/com/atomgraph/client/model/impl/ProxiedGraph.java b/src/main/java/com/atomgraph/client/model/impl/ProxiedGraph.java deleted file mode 100644 index 063496d1..00000000 --- a/src/main/java/com/atomgraph/client/model/impl/ProxiedGraph.java +++ /dev/null @@ -1,449 +0,0 @@ -/** - * Copyright 2013 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.client.model.impl; - -import com.atomgraph.client.MediaTypes; -import com.atomgraph.client.util.HTMLMediaTypePredicate; -import com.atomgraph.client.vocabulary.AC; -import java.net.URI; -import java.util.ArrayList; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.QueryParam; -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.Status; -import jakarta.ws.rs.core.UriInfo; -import com.atomgraph.core.exception.BadGatewayException; -import com.atomgraph.core.io.ModelProvider; -import com.atomgraph.core.model.DirectGraphStore; -import com.atomgraph.core.util.ModelUtils; -import com.atomgraph.core.util.ResultSetUtils; -import java.net.URISyntaxException; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import jakarta.inject.Inject; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.ws.rs.NotAcceptableException; -import jakarta.ws.rs.NotFoundException; -import jakarta.ws.rs.ProcessingException; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.client.Invocation; -import jakarta.ws.rs.client.WebTarget; -import jakarta.ws.rs.core.EntityTag; -import jakarta.ws.rs.core.UriBuilder; -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.glassfish.jersey.uri.UriComponent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Resource that can publish Linked Data and (X)HTML as well as load RDF from remote sources. - * - * @author Martynas Jusevičius {@literal } - */ -@Path("/") -public class ProxiedGraph implements DirectGraphStore -{ - private static final Logger log = LoggerFactory.getLogger(ProxiedGraph.class); - - private final UriInfo uriInfo; - private final Request request; - private final HttpHeaders httpHeaders; - private final MediaTypes mediaTypes; - private final MediaType accept; - private final MediaType[] readableMediaTypes; - private final Client client; - private final WebTarget webTarget; - private final URI endpoint; - private final String query; - - private final HttpServletRequest httpServletRequest; - - /** - * JAX-RS compatible resource constructor with injected initialization objects. - * - * @param uriInfo URI information - * @param request request - * @param httpHeaders HTTP headers - * @param mediaTypes supported media types - * @param uri RDF resource URI - * @param endpoint SPARQL endpoint URI - * @param query SPARQL query - * @param accept response media type - * @param mode layout mode - * @param client HTTP client - * @param httpServletRequest HTTP request - */ - @Inject - public ProxiedGraph(@Context UriInfo uriInfo, @Context Request request, @Context HttpHeaders httpHeaders, MediaTypes mediaTypes, - @QueryParam("uri") URI uri, @QueryParam("endpoint") URI endpoint, @QueryParam("query") String query, @QueryParam("accept") MediaType accept, @QueryParam("mode") URI mode, - Client client, @Context HttpServletRequest httpServletRequest) - { - this.uriInfo = uriInfo; - this.request = request; - this.httpHeaders = httpHeaders; - this.mediaTypes = mediaTypes; - this.endpoint = endpoint; - this.query = query; - this.accept = accept; - this.client = client; - List readableMediaTypesList = new ArrayList<>(); - readableMediaTypesList.addAll(mediaTypes.getReadable(Model.class)); - readableMediaTypesList.addAll(mediaTypes.getReadable(ResultSet.class)); - this.readableMediaTypes = readableMediaTypesList.toArray(MediaType[]::new); - - if (uri != null) - { - if (uri.getFragment() != null) - try - { - // strip #fragment as we don't want to use it in the request to server - uri = new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), uri.getQuery(), null); - } - catch (URISyntaxException ex) - { - // should not happen - } - - webTarget = client.target(uri); - //webTarget.register(new RedirectFilter()); // TO-DO - } - else - { - webTarget = null; - } - this.httpServletRequest = httpServletRequest; - } - - public URI getURI() - { - return getWebTarget().getUri(); - } - - public MediaType[] getReadableMediaTypes() - { - return readableMediaTypes; - } - - public List getWritableMediaTypes(Class clazz) - { - // restrict writable MediaTypes to the requested one (usually by RDF export feature) - if (getAcceptMediaType() != null) return Arrays.asList(getAcceptMediaType()); - - return getMediaTypes().getWritable(clazz); - } - - /** - * Forwards GET request and returns response from remote resource. - * - * @return response - */ - @GET - @Override - public Response get() - { - return get(getWebTarget()); - } - - public Response get(WebTarget target) - { - if (target == null) - { - // if SPARQL endpoint and query are provided, build a SPARQL Protocol URI and then redirect to a URI that proxies it - if (getEndpoint() != null && getQuery() != null) - { - if (log.isDebugEnabled()) log.debug("Redirecting from endpoint/query URL to a proxied URL"); - String encodedQuery = UriComponent.encode(getQuery(), UriComponent.Type.UNRESERVED); // manually encode query string because UriBuilder::build will complain about {} - URI sparqlUrl = UriBuilder.fromUri(getEndpoint()).queryParam(AC.query.getLocalName(), encodedQuery).build(); - String encodedSparqlUrl = UriComponent.encode(sparqlUrl.toString(), UriComponent.Type.UNRESERVED); // manually encode URL - URI uri = getUriInfo().getBaseUriBuilder().queryParam(AC.uri.getLocalName(), encodedSparqlUrl).build(); - - return Response.seeOther(uri).build(); - } - - throw new NotFoundException("Resource URI not supplied"); - } - - return get(target, getBuilder(target)); - } - - public Invocation.Builder getBuilder(WebTarget target) - { - return target.request(getReadableMediaTypes()); - } - - public Response get(WebTarget target, Invocation.Builder builder) - { - if (target == null) throw new NotFoundException("Resource URI not supplied"); // cannot throw Exception in constructor: https://github.com/eclipse-ee4j/jersey/issues/4436 - - try (Response cr = builder.get()) - { - // special case for http <-> https 301/303 redirection - if ((cr.getStatusInfo().toEnum().equals(Status.SEE_OTHER) || cr.getStatusInfo().toEnum().equals(Status.MOVED_PERMANENTLY)) && - ((target.getUri().getScheme().equals("http") && cr.getLocation().getScheme().equals("https")) || - (target.getUri().getScheme().equals("https") && cr.getLocation().getScheme().equals("http"))) ) - return get(getClient().target(cr.getLocation())); - - cr.getHeaders().putSingle(ModelProvider.REQUEST_URI_HEADER, target.getUri().toString()); // provide a base URI hint to ModelProvider - - if (log.isDebugEnabled()) log.debug("GETing response from URI: {}", target.getUri()); - - Response response = getResponse(cr); - - List linkValues = cr.getHeaders().get(HttpHeaders.LINK); - if (linkValues != null) setLinks(linkValues, response); - - return response; - } - catch (MessageBodyProviderNotFoundException ex) - { - if (log.isWarnEnabled()) log.debug("Dereferenced URI {} returned non-RDF media type", ex); - throw new NotAcceptableException(ex); - } - catch (ProcessingException ex) - { - if (log.isWarnEnabled()) log.debug("Could not dereference URI: {}", webTarget.getUri()); - throw new BadGatewayException(ex); - } - } - - public Response getResponse(Response clientResponse) - { - 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); - } - - // fallback to RDF graph - Model description = clientResponse.readEntity(Model.class); - return getResponse(description); - } - - /** - * Returns response for the given RDF model. - * - * @param model RDF model - * @return response object - */ - public Response getResponse(Model model) - { - 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(). - build(); - } - - /** - * Returns response for the given SPARQL results. - * - * @param resultSet SPARQL results - * @return response object - */ - public Response getResponse(ResultSetRewindable resultSet) - { - 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(). - build(); - } - - /** - * Forwards Link header values. - * - * @param linkValues header values - * @param response proxy response - * @return the response - */ - protected Response setLinks(List linkValues, Response response) - { - linkValues.forEach(linkValue -> { - response.getHeaders().add(HttpHeaders.LINK, linkValue); - }); - - return response; - } - - /** - * Forwards POST request with RDF dataset body and returns RDF response from remote resource. - * - * @param model - * @return response - */ - @POST - @Override - public Response post(Model model) - { - if (getWebTarget() == null) throw new NotFoundException("Resource URI not supplied"); // cannot throw Exception in constructor: https://github.com/eclipse-ee4j/jersey/issues/4436 - - if (log.isDebugEnabled()) log.debug("POSTing Dataset to URI: {}", getWebTarget().getUri()); - return getWebTarget().request(). - accept(getMediaTypes().getReadable(Model.class).toArray(jakarta.ws.rs.core.MediaType[]::new)). - post(Entity.entity(model, com.atomgraph.core.MediaType.APPLICATION_NTRIPLES_TYPE)); - } - - /** - * Forwards PUT request with RDF dataset body and returns response from remote resource. - * - * @param model RDF payload - * @return response - */ - @PUT - @Override - public Response put(Model model) - { - if (getWebTarget() == null) throw new NotFoundException("Resource URI not supplied"); // cannot throw Exception in constructor: https://github.com/eclipse-ee4j/jersey/issues/4436 - - if (log.isDebugEnabled()) log.debug("PUTting Dataset to URI: {}", getWebTarget().getUri()); - return getWebTarget().request(). - accept(getMediaTypes().getReadable(Model.class).toArray(jakarta.ws.rs.core.MediaType[]::new)). - put(Entity.entity(model, com.atomgraph.core.MediaType.APPLICATION_NTRIPLES_TYPE)); - } - - /** - * Forwards DELETE request and returns response from remote resource. - * @return response - */ - @DELETE - @Override - public Response delete() - { - if (getWebTarget() == null) throw new NotFoundException("Resource URI not supplied"); // cannot throw Exception in constructor: https://github.com/eclipse-ee4j/jersey/issues/4436 - - if (log.isDebugEnabled()) log.debug("DELETEing Dataset from URI: {}", getWebTarget().getUri()); - return getWebTarget().request(). - accept(getMediaTypes().getReadable(Model.class).toArray(jakarta.ws.rs.core.MediaType[]::new)). - delete(Response.class); - } - - public UriInfo getUriInfo() - { - return uriInfo; - } - - public HttpHeaders getHttpHeaders() - { - return httpHeaders; - } - - /** - * Returns media type requested by the client ("accept" query string parameter). - * This mechanism overrides the normally used content negotiation. - * - * @return media type parsed from query param - */ - public MediaType getAcceptMediaType() - { - return accept; - } - - public Client getClient() - { - return client; - } - - public final WebTarget getWebTarget() - { - return webTarget; - } - - public final URI getEndpoint() - { - return endpoint; - } - - public final String getQuery() - { - return query; - } - - public Request getRequest() - { - return request; - } - - public MediaTypes getMediaTypes() - { - return mediaTypes; - } - - public HttpServletRequest getHttpServletRequest() - { - return httpServletRequest; - } - - /** - * Returns a list of supported languages. - * - * @return list of languages - */ - public List getLanguages() - { - return new ArrayList<>(); - } - - /** - * Returns a list of supported HTTP encodings. - * Note: this is different from content encodings such as UTF-8. - * - * @return list of encodings - */ - public List getEncodings() - { - return new ArrayList<>(); - } - -} diff --git a/src/main/java/com/atomgraph/client/resource/Root.java b/src/main/java/com/atomgraph/client/resource/Root.java new file mode 100644 index 00000000..faf9206c --- /dev/null +++ b/src/main/java/com/atomgraph/client/resource/Root.java @@ -0,0 +1,73 @@ +/** + * Copyright 2025 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.client.resource; + +import com.atomgraph.client.vocabulary.AC; +import java.net.URI; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriBuilder; +import jakarta.ws.rs.core.UriInfo; +import org.glassfish.jersey.uri.UriComponent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The application root. Proxy requests ({@code ?uri=}) never reach it - the + * {@link com.atomgraph.client.filter.request.ProxyRequestFilter} aborts them pre-matching - so it + * only handles the non-proxy cases: turning a SPARQL Protocol {@code ?endpoint=}/{@code ?query=} + * pair into a redirect to the proxied query URL, and 404 when no resource URI is supplied. + * + * @author Martynas Jusevičius {@literal } + */ +@Path("/") +public class Root +{ + + private static final Logger log = LoggerFactory.getLogger(Root.class); + + /** + * Handles non-proxy GET requests. + * + * @param endpoint SPARQL endpoint URI + * @param query SPARQL query string + * @param uriInfo URI information + * @return redirect to the proxied SPARQL Protocol URL + */ + @GET + public Response get(@QueryParam("endpoint") URI endpoint, @QueryParam("query") String query, @Context UriInfo uriInfo) + { + // if SPARQL endpoint and query are provided, build a SPARQL Protocol URI and then redirect to a URI that proxies it + if (endpoint != null && query != null) + { + if (log.isDebugEnabled()) log.debug("Redirecting from endpoint/query URL to a proxied URL"); + String encodedQuery = UriComponent.encode(query, UriComponent.Type.UNRESERVED); // manually encode query string because UriBuilder::build will complain about {} + URI sparqlUrl = UriBuilder.fromUri(endpoint).queryParam(AC.query.getLocalName(), encodedQuery).build(); + String encodedSparqlUrl = UriComponent.encode(sparqlUrl.toString(), UriComponent.Type.UNRESERVED); // manually encode URL + URI uri = uriInfo.getBaseUriBuilder().queryParam(AC.uri.getLocalName(), encodedSparqlUrl).build(); + + return Response.seeOther(uri).build(); + } + + throw new NotFoundException("Resource URI not supplied"); + } + +} diff --git a/src/main/java/com/atomgraph/client/util/Constructor.java b/src/main/java/com/atomgraph/client/util/Constructor.java deleted file mode 100644 index 2d947902..00000000 --- a/src/main/java/com/atomgraph/client/util/Constructor.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2015 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.client.util; - -import com.atomgraph.client.exception.OntologyException; -import com.atomgraph.client.vocabulary.SP; -import com.atomgraph.client.vocabulary.SPIN; -import org.apache.jena.ontapi.model.OntClass; -import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.Property; -import org.apache.jena.rdf.model.Resource; -import org.apache.jena.rdf.model.Statement; -import org.apache.jena.rdf.model.StmtIterator; -import org.apache.jena.vocabulary.RDF; -import org.apache.jena.query.Query; -import org.apache.jena.query.QueryExecution; -import org.apache.jena.query.QueryFactory; -import org.apache.jena.query.QueryParseException; -import org.apache.jena.rdf.model.RDFNode; -import org.apache.jena.sparql.core.Var; -import org.apache.jena.sparql.engine.binding.Binding; -import org.apache.jena.sparql.engine.binding.BindingFactory; -import org.apache.jena.sparql.expr.ExprVar; -import org.apache.jena.sparql.syntax.ElementBind; -import org.apache.jena.sparql.syntax.ElementGroup; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * - * @author Martynas Jusevičius {@literal } - */ -public class Constructor -{ - - private static final Logger log = LoggerFactory.getLogger(Constructor.class); - - public Resource construct(OntClass forClass, Model targetModel, String baseURI) - { - return construct(forClass, targetModel, baseURI, null); - } - - public Resource construct(OntClass forClass, Model targetModel, String baseURI, String resourceURI) - { - if (targetModel == null) throw new IllegalArgumentException("Model cannot be null"); - - final Resource resource; - if (resourceURI == null) resource = targetModel.createResource(); // blank node - else resource = targetModel.createResource(resourceURI); // URI resource - - return constructInstance(forClass, SPIN.constructor, resource, baseURI). - addProperty(RDF.type, forClass); - } - - /** - * Constructs new anonymous individual of an ontology class. - * It walks up the superclass chains and executes SPIN constructors. - * - * @param forClass class for which to construct new instance - * @param property property that attaches CONSTRUCT query resource to class resource, usually spin:constructor - * @param instance the instance resource - * @param baseURI base URI of the query - * @return the instance resource with constructed properties - */ - public Resource constructInstance(OntClass forClass, Property property, Resource instance, String baseURI) - { - if (forClass == null) throw new IllegalArgumentException("OntClass cannot be null"); - if (instance == null) throw new IllegalArgumentException("Instance Resource cannot be null"); - if (baseURI == null) throw new IllegalArgumentException("Base URI cannot be null"); - - StmtIterator constructorIt = forClass.listProperties(property); - try - { - while (constructorIt.hasNext()) // traverse all constructors - { - RDFNode constructor = constructorIt.next().getObject(); - if (!constructor.isResource()) - { - if (log.isErrorEnabled()) log.error("Constructor is invoked but {} is not defined for class <{}>", property, forClass.getURI()); - throw new OntologyException("Constructor property not defined", forClass, property); - } - - Statement queryText = constructor.asResource().getProperty(SP.text); - if (queryText == null || !queryText.getObject().isLiteral()) - { - if (log.isErrorEnabled()) log.error("Constructor resource <{}> does not have sp:text property", constructor); - throw new OntologyException("Query property not defined", constructor.asResource(), SP.text); - } - - try - { - Query query = QueryFactory.create(queryText.getString(), baseURI); - - // Inject BIND(?_this_bind AS ?this) into the WHERE clause. - // This keeps ?this as a variable in the CONSTRUCT template (not a written blank node), - // so the template picks up the concrete node value — preserving blank node identity. - // See: https://github.com/apache/jena/issues/3267 - Var bindVar = Var.alloc("_this_bind"); - ElementGroup group = new ElementGroup(); - group.addElement(query.getQueryPattern()); - group.addElement(new ElementBind(Var.alloc(SPIN.THIS_VAR_NAME), new ExprVar(bindVar))); - query.setQueryPattern(group); - - Binding binding = BindingFactory.binding(bindVar, instance.asNode()); - - try (QueryExecution qex = QueryExecution.create(). - query(query). - model(instance.getModel()). - substitution(binding). - build()) - { - instance.getModel().add(qex.execConstruct()); - } - } - catch (QueryParseException ex) - { - if (log.isErrorEnabled()) log.error("Constructor resource '{}' sp:text property contains an invalid SPARQL CONSTRUCT", constructor); - throw new OntologyException("Invalid SPARQL CONSTRUCT", ex, constructor.asResource(), SP.text); - } - } - } - finally - { - constructorIt.close(); - } - - forClass.superClasses(true).forEach(superClass -> constructInstance(superClass, property, instance, baseURI)); - - return instance; - } - -} diff --git a/src/main/java/com/atomgraph/client/vocabulary/AC.java b/src/main/java/com/atomgraph/client/vocabulary/AC.java index 9d6aa1da..27e4affa 100644 --- a/src/main/java/com/atomgraph/client/vocabulary/AC.java +++ b/src/main/java/com/atomgraph/client/vocabulary/AC.java @@ -65,8 +65,6 @@ public static String getURI() public static final Resource MapMode = m_model.createOntClass( NS + "MapMode" ); public static final Resource ReadMode = m_model.createOntClass( NS + "ReadMode" ); - - public static final Resource ConstructMode = m_model.createOntClass( NS + "ConstructMode" ); public static final Property contextUri = m_model.createObjectProperty( NS + "contextUri" ); @@ -97,8 +95,6 @@ public static String getURI() /** The language the representation is composed in, as opposed to the languages the reader accepts. */ public static final Property contentLang = m_model.createDataProperty( NS + "contentLang" ); - public static final Property forClass = m_model.createObjectProperty( NS + "forClass" ); - public static final Property instance = m_model.createDataProperty( NS + "instance" ); // CONFIG - separate? diff --git a/src/main/java/com/atomgraph/client/vocabulary/SPIN.java b/src/main/java/com/atomgraph/client/vocabulary/SPIN.java deleted file mode 100644 index d02f5866..00000000 --- a/src/main/java/com/atomgraph/client/vocabulary/SPIN.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2018 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.client.vocabulary; - -import org.apache.jena.ontapi.OntModelFactory; -import org.apache.jena.ontapi.OntSpecification; -import org.apache.jena.ontapi.model.OntModel; - -import org.apache.jena.rdf.model.Property; -import org.apache.jena.rdf.model.ResourceFactory; - -/** - * - * @author Martynas Jusevičius {@literal } - */ -public class SPIN -{ - - static - { - org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it - } - - public final static String THIS_VAR_NAME = "this"; - - /**

The RDF model that holds the vocabulary terms

*/ - private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); - - /**

The namespace of the vocabulary as a string

*/ - public static final String NS = "http://spinrdf.org/spin#"; - - /**

The namespace of the vocabulary as a string

- * @see #NS */ - public static String getURI() - { - return NS; - } - - public final static Property constructor = ResourceFactory.createProperty(NS + "constructor"); - -} diff --git a/src/main/java/com/atomgraph/client/writer/XSLTWriterBase.java b/src/main/java/com/atomgraph/client/writer/XSLTWriterBase.java index e31ae36d..79246cfe 100644 --- a/src/main/java/com/atomgraph/client/writer/XSLTWriterBase.java +++ b/src/main/java/com/atomgraph/client/writer/XSLTWriterBase.java @@ -17,7 +17,6 @@ import com.atomgraph.client.util.RDFSourceResolver; import com.atomgraph.client.vocabulary.AC; -import com.atomgraph.client.vocabulary.LDT; import com.atomgraph.core.util.Link; import java.net.URI; import java.net.URISyntaxException; @@ -148,8 +147,6 @@ public Map getParameters(MultivaluedMap params = new HashMap<>(); - params.put(new QName("ac", AC.httpHeaders.getNameSpace(), AC.httpHeaders.getLocalName()), new XdmAtomicValue(headerMap.toString())); - params.put(new QName("ac", AC.method.getNameSpace(), AC.method.getLocalName()), new XdmAtomicValue(getRequest().getMethod())); params.put(new QName("ac", AC.contextUri.getNameSpace(), AC.contextUri.getLocalName()), new XdmAtomicValue(getContextURI())); try @@ -161,15 +158,6 @@ public Map getParameters(MultivaluedMap modes = getModes(getSupportedNamespaces()); // check if explicit mode URL parameter is provided if (!modes.isEmpty()) params.put(new QName("ac", AC.mode.getNameSpace(), AC.mode.getLocalName()), XdmValue.makeSequence(modes)); - URI ontologyURI = getLinkURI(headerMap, LDT.ontology); - if (ontologyURI != null) params.put(new QName("ldt", LDT.ontology.getNameSpace(), LDT.ontology.getLocalName()), new XdmAtomicValue(ontologyURI)); - - URI baseURI = getLinkURI(headerMap, LDT.base); - if (baseURI != null) params.put(new QName("ldt", LDT.base.getNameSpace(), LDT.base.getLocalName()), new XdmAtomicValue(baseURI)); - - String forClassURI = getUriInfo().getQueryParameters().getFirst(AC.forClass.getLocalName()); - if (forClassURI != null) params.put(new QName("ac", AC.forClass.getNameSpace(), AC.forClass.getLocalName()), new XdmAtomicValue(URI.create(forClassURI))); - // ordered language preference list from Accept-Language List langs = getHttpHeaders().getAcceptableLanguages().stream(). map(Locale::getLanguage). diff --git a/src/main/java/com/atomgraph/client/writer/function/ConstructForClass.java b/src/main/java/com/atomgraph/client/writer/function/ConstructForClass.java deleted file mode 100644 index c7168da2..00000000 --- a/src/main/java/com/atomgraph/client/writer/function/ConstructForClass.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2020 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.client.writer.function; - -import com.atomgraph.client.util.Constructor; -import com.atomgraph.client.vocabulary.AC; -import static com.atomgraph.client.writer.ModelXSLTWriter.checkURI; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import javax.xml.transform.stream.StreamSource; -import net.sf.saxon.s9api.ExtensionFunction; -import net.sf.saxon.s9api.ItemType; -import net.sf.saxon.s9api.OccurrenceIndicator; -import net.sf.saxon.s9api.Processor; -import net.sf.saxon.s9api.QName; -import net.sf.saxon.s9api.SaxonApiException; -import net.sf.saxon.s9api.SequenceType; -import net.sf.saxon.s9api.XdmValue; -import com.atomgraph.client.util.jena.PrefixGraphRepository; -import org.apache.jena.ontapi.OntModelFactory; -import org.apache.jena.ontapi.OntSpecification; -import org.apache.jena.ontapi.model.OntClass; -import org.apache.jena.ontapi.model.OntModel; -import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.ModelFactory; -import org.apache.jena.riot.RDFFormat; -import org.apache.jena.riot.RDFWriter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * ac:construct() XSLT function that constructs instances for given classes from their constructors. - * Plugs into the Saxon processor. - * - * @author Martynas Jusevičius {@literal } - * @see Integrated extension functions - */ -public class ConstructForClass implements ExtensionFunction -{ - - private static final Logger log = LoggerFactory.getLogger(ConstructForClass.class); - - private final Processor processor; - private final PrefixGraphRepository repository; - - public ConstructForClass(Processor processor, PrefixGraphRepository repository) - { - this.processor = processor; - this.repository = repository; - } - - @Override - public QName getName() - { - return new QName(AC.NS, "construct"); - } - - @Override - public SequenceType getResultType() - { - return SequenceType.makeSequenceType(ItemType.DOCUMENT_NODE, OccurrenceIndicator.ZERO_OR_MORE); - } - - @Override - public SequenceType[] getArgumentTypes() - { - return new SequenceType[] - { - SequenceType.makeSequenceType(ItemType.ANY_URI, OccurrenceIndicator.ONE), - SequenceType.makeSequenceType(ItemType.ANY_URI, OccurrenceIndicator.ZERO_OR_MORE), - SequenceType.makeSequenceType(ItemType.ANY_URI, OccurrenceIndicator.ONE), - }; - } - - @Override - public XdmValue call(XdmValue[] arguments) throws SaxonApiException - { - try - { - String ontology = arguments[0].itemAt(0).getStringValue(); - String base = arguments[2].itemAt(0).getStringValue(); - - Model instances = ModelFactory.createDefaultModel(); - try - { - OntModel ontModel = OntModelFactory.createModel(getRepository().get(ontology), OntSpecification.OWL2_FULL_MEM, getRepository()); - - arguments[1].stream(). - map(forClass -> ontModel.getOntClass(checkURI(forClass.getStringValue()).toString())). - filter(forClass -> forClass != null). - forEach(forClass -> new Constructor().construct(forClass, instances, base)); - } - catch (RuntimeException ex) // ontology or one of its imports could not be loaded — return empty result instead of failing the transform - { - if (log.isWarnEnabled()) log.warn("Could not construct instances for ontology '{}': {}", ontology, ex.toString()); - } - - return getProcessor().newDocumentBuilder().build(getSource(instances)); - } - catch (IOException ex) - { - throw new SaxonApiException(ex); - } - } - - public StreamSource getSource(Model model) throws IOException - { - if (model == null) throw new IllegalArgumentException("Model cannot be null"); - - try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) - { - RDFWriter.create(). - format(RDFFormat.RDFXML_PLAIN). - source(model). - output(stream); - return new StreamSource(new ByteArrayInputStream(stream.toByteArray())); - } - } - - public Processor getProcessor() - { - return processor; - } - - public PrefixGraphRepository getRepository() - { - return repository; - } - -} diff --git a/src/main/resources/com/atomgraph/client/ns.ttl b/src/main/resources/com/atomgraph/client/ns.ttl index 5074815a..58c13e39 100644 --- a/src/main/resources/com/atomgraph/client/ns.ttl +++ b/src/main/resources/com/atomgraph/client/ns.ttl @@ -1,17 +1,15 @@ @base . @prefix : <#> . -@prefix rdf: . @prefix rdfs: . @prefix xsd: . @prefix owl: . @prefix ldt: . -@prefix foaf: . # ONTOLOGY : a owl:Ontology ; - rdfs:label "Atomgraph Client ontology" ; + rdfs:label "AtomGraph Client ontology" ; owl:versionInfo "2.1.0" . # PROPERTIES @@ -93,10 +91,6 @@ # UI keywords -# rename to :Create? -:ConstructMode rdfs:label "Create" ; - rdfs:isDefinedBy : . - :Delete rdfs:label "Delete" ; rdfs:isDefinedBy : . diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 2c41f952..39ccb143 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -8,7 +8,7 @@ Generic Linked Data client https://w3id.org/atomgraph/client#stylesheet - static/com/atomgraph/client/xsl/bootstrap/2.3.2/external-layout.xsl + static/com/atomgraph/client/xsl/external-layout.xsl https://w3id.org/atomgraph/core#resultLimit @@ -40,4 +40,27 @@ com.atomgraph.core.util.jena.StartupListener + + + css + text/css;charset=UTF-8 + + + js + text/javascript;charset=UTF-8 + + + txt + text/plain;charset=UTF-8 + + + xsl + text/xsl;charset=UTF-8 + diff --git a/src/main/webapp/static/com/atomgraph/client/css/bootstrap.css b/src/main/webapp/static/com/atomgraph/client/css/bootstrap.css deleted file mode 100644 index cfc30ec1..00000000 --- a/src/main/webapp/static/com/atomgraph/client/css/bootstrap.css +++ /dev/null @@ -1,19 +0,0 @@ -body { padding-top: 120px; padding-bottom: 40px; } -.brand img { height: 0.8em; width: auto; } -form.form-inline { margin: 0; } -ul.inline { margin-left: 0; max-height: 7em; overflow-y: auto; } -.inline li { display: inline; } -.btn-type.btn-primary { font-weight: bold; background: inherit; } -.well-small { background-color: #FAFAFA; } -.well-small dl { max-height: 60em; overflow-y: auto; } -textarea#query-string { font-family: monospace; } -.thumbnail img { display: block; margin: auto; } -.thumbnail { min-height: 15em; } -#map-canvas { height: 35em; width: 100%; } -ul.typeahead { max-height: 20em; overflow: auto; } -label.typeahead input { display: block; max-width: 160px; } -label.typeahead { float: left; width: 160px; } - -/* SVG */ - -g.subject:hover circle { stroke-width: 2; } \ No newline at end of file diff --git a/src/main/webapp/static/com/atomgraph/client/css/client.css b/src/main/webapp/static/com/atomgraph/client/css/client.css new file mode 100644 index 00000000..6227a4a1 --- /dev/null +++ b/src/main/webapp/static/com/atomgraph/client/css/client.css @@ -0,0 +1,175 @@ +/* ========================================================================= + Web-Client browser chrome — composition styles for the stylesheets' own + markup: page shell, resource blocks, collections, results table, form rows. + Tokens come from colors_and_type.css and the primitives from the core kit; + this file never redefines either. + ========================================================================= */ + +@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap'); + +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { background: var(--surface-1); color: var(--fg-1); font-family: var(--font-sans); font-size: var(--fs-base); line-height: var(--lh-base); } +a { color: var(--fg-link); text-decoration: none; } +a:hover { text-decoration: underline; } +img { max-width: 100%; } +pre { background: var(--surface-2); border: 1px solid var(--border-default); border-radius: var(--r-sm); padding: var(--sp-3); overflow-x: auto; font-family: var(--font-mono); font-size: var(--fs-sm); } + +/* ---------- page shell ---------- */ +.header { display: flex; align-items: center; gap: var(--sp-4); padding: var(--sp-3) var(--sp-6); background: var(--surface-0); border-bottom: 1px solid var(--border-default); flex-wrap: wrap; } +.header .logo { display: inline-flex; align-items: center; flex-shrink: 0; } +.header .logo img { display: block; max-height: 36px; } +.header .uri-form { display: flex; align-items: center; gap: var(--sp-2); flex: 1 1 24rem; min-width: 0; margin: 0; } +.header .uri-form .ac-field { flex: 1 1 auto; min-width: 0; } +.header .actions { display: flex; align-items: center; gap: var(--sp-4); margin-left: auto; } + +.action-bar { display: flex; align-items: center; gap: var(--sp-4); padding: var(--sp-2) var(--sp-6); background: var(--surface-0); border-bottom: 1px solid var(--border-default); flex-wrap: wrap; } +.ab-left { flex: 0 0 auto; } +/* the bar's controls sit at the end of the row. The breadcrumb used to do this as a side effect - + it was the only growing child, so it absorbed the free space and pushed everything past it - + which left the controls hard left the moment it went. Stating it on the container instead does + not depend on what else the bar happens to contain. */ +.ab-main { display: flex; align-items: center; justify-content: flex-end; gap: var(--sp-3); flex: 1 1 auto; min-width: 0; } +.ab-main .actions { display: flex; align-items: center; gap: var(--sp-2); } +.ab-main .actions form { margin: 0; } + + +.content { display: flex; align-items: flex-start; gap: var(--sp-5); width: 100%; max-width: 1400px; margin: 0 auto; padding: var(--sp-5) var(--sp-6); } +.main { flex: 1 1 auto; min-width: 0; } +.aside { flex: 0 0 320px; min-width: 0; } + +.footer { padding: var(--sp-5) var(--sp-6); border-top: 1px solid var(--border-default); color: var(--fg-muted); font-size: var(--fs-sm); text-align: center; } +.footer p { margin: 0; } + +/* ---------- menus:
-based dropdowns, no script ---------- */ +.menu { position: relative; display: inline-block; } +.menu > summary { list-style: none; cursor: pointer; } +.menu > summary::-webkit-details-marker { display: none; } +.menu > .menu-list { position: absolute; right: 0; top: calc(100% + 6px); z-index: 10; min-width: 180px; margin: 0; padding: var(--sp-1); list-style: none; background: var(--surface-0); border: 1px solid var(--border-default); border-radius: var(--r-md); box-shadow: var(--shadow-md); } +.menu-list li a { display: block; padding: var(--sp-2) var(--sp-3); border-radius: var(--r-sm); color: var(--fg-1); white-space: nowrap; } +.menu-list li a:hover { background: var(--bg-hover); text-decoration: none; } +.menu-list li.is-active a { background: var(--bg-selected); color: var(--fg-selected); } + +/* ---------- resource blocks ---------- */ +.block { background: var(--bg-card); border: 1px solid var(--border-default); border-radius: var(--r-lg); padding: var(--sp-5); margin-bottom: var(--sp-4); min-width: 0; container-type: inline-size; } +.block-header { position: relative; } +.block-header > .actions { position: absolute; top: 0; right: 0; display: flex; gap: var(--sp-2); } +.block-header > .actions form { margin: 0; } +.block-header h2 { margin: 0 0 var(--sp-1); font-size: var(--fs-xl); font-weight: var(--fw-semibold); letter-spacing: var(--tracking-snug); overflow-wrap: break-word; } +.block-header .description { margin: 0 0 var(--sp-2); color: var(--fg-2); max-width: 78ch; } +.depiction { display: block; margin-bottom: var(--sp-3); } +.depiction img { display: block; max-height: 200px; border-radius: var(--r-md); } + +.types { display: flex; flex-wrap: wrap; gap: var(--sp-2); list-style: none; margin: 0; padding: 0; font-size: var(--fs-sm); } +.types li { display: inline-flex; min-width: 0; } + +/* ---------- read-mode property list ---------- + One dt can be followed by N dds (multi-valued property; the grouping pass suppresses the repeated + terms), so this is float/clear column layout rather than a two-column grid: grid auto-placement + cannot keep a run of dds in the value column without per-group wrappers the flat dl does not have */ +/* Its own query container: the list lays out by the width IT has, which is not the viewport's. A + property list renders in the document column, in a sidebar and inside a nested resource, and at a + 1440px desktop the 300px case left the value 100px against a 200px label. See RESPONSIVE.md in + the design system - the stop and the reasoning are shared with the app kit's statement grid. */ +.properties { margin: var(--sp-4) 0 0; container-type: inline-size; } +.properties::after { content: ""; display: block; clear: both; } +.properties dt { float: left; clear: left; width: 200px; padding-right: var(--sp-4); box-sizing: border-box; font-weight: var(--fw-medium); color: var(--fg-2); overflow-wrap: break-word; } +.properties dd { margin: 0 0 var(--sp-1) 200px; min-width: 0; overflow-wrap: break-word; } +/* Narrow, the label stops being a column and becomes a line above its value. The float has to be + released as well as the indent: a floated dt with the dd's margin removed would have the value + wrap around it rather than start beneath it. */ +@container (max-width: 520px) { + .properties dt { float: none; clear: none; width: auto; padding-right: 0; } + .properties dd { margin-left: 0; } +} +.properties dd .ac-tag { margin-right: var(--sp-1); } + +/* ---------- collections ---------- */ +.resource-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-4); } +.resource-list > li { position: relative; background: var(--bg-card); border: 1px solid var(--border-default); border-radius: var(--r-lg); padding: var(--sp-5); min-width: 0; container-type: inline-size; } +.resource-list > li > .actions { position: absolute; top: var(--sp-4); right: var(--sp-4); display: flex; gap: var(--sp-2); } +.resource-list > li > .actions form { margin: 0; } +.resource-list h2 { margin: 0 0 var(--sp-1); font-size: var(--fs-lg); font-weight: var(--fw-semibold); } + +.resource-grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--sp-4); } +.resource-grid > li { min-width: 0; display: flex; } +.resource-grid .ac-card { flex: 1; min-width: 0; } +.resource-grid .depiction { margin: 0; } +.resource-grid .depiction img { width: 100%; max-height: 180px; object-fit: cover; border-radius: var(--r-md) var(--r-md) 0 0; } +.resource-grid h2 { margin: 0 0 var(--sp-1); font-size: var(--fs-base); font-weight: var(--fw-medium); } +.resource-grid .ac-card-body { position: relative; } +.resource-grid .ac-card-body > .actions { display: flex; gap: var(--sp-2); margin-bottom: var(--sp-2); } +.resource-grid .ac-card-body > .actions form { margin: 0; } + +/* ---------- results table: native table layout, the column count is data-driven ---------- */ +.results-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); background: var(--surface-0); border: 1px solid var(--border-default); border-radius: var(--r-sm); } +.results-table th, .results-table td { padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border-default); text-align: left; vertical-align: middle; } +.results-table tr:last-child > td { border-bottom: 0; } +.results-table thead th { font-family: var(--font-mono); font-size: 10px; letter-spacing: var(--tracking-wide); text-transform: uppercase; font-weight: var(--fw-regular); color: var(--fg-hint); background: var(--surface-2); } +.results-table tbody tr:hover > td { background: var(--surface-1); } +.results-table td > .values { max-height: 12em; overflow-y: auto; min-width: 0; overflow-wrap: break-word; } +.results-table td img { max-height: 60px; border-radius: var(--r-xs); vertical-align: middle; } + +/* ---------- forms ---------- */ +/* The fieldset is the statement rows' query container - a row cannot query itself, and the fieldset + is the one box always between a .statement and whatever surface holds the form. Nested fieldsets + (an inlined blank node sits in a value column) therefore give their own rows a narrower container + than the outer form's, which is exactly right. */ +fieldset { border: 0; padding: 0; margin: 0 0 var(--sp-5); min-inline-size: 0; container-type: inline-size; } +legend { padding: 0; margin: 0 0 var(--sp-3); font-size: var(--fs-lg); font-weight: var(--fw-semibold); color: var(--fg-1); } +fieldset .description { margin: 0 0 var(--sp-4); color: var(--fg-2); } + +/* one statement row: predicate label · value controls · term annotations */ +.statement { display: grid; grid-template-columns: minmax(140px, 220px) 1fr; column-gap: var(--sp-4); align-items: start; padding: var(--sp-2) 0; border-bottom: 1px solid var(--border-default); } +.statement:last-of-type { border-bottom: 0; } +/* minmax(140px, 220px) resolves to its MAXIMUM next to a 1fr sibling - the flexible track is sized + last, so the label takes 220px and the value takes what is left: 106px in a phone's content column + and 64px in a 300px container, at any viewport. Narrow, the label takes its own line. */ +@container (max-width: 520px) { + .statement { grid-template-columns: minmax(0, 1fr); row-gap: var(--sp-1); } + .statement > .values { grid-column: 1; } +} +.statement > .ac-label { padding-top: 6px; overflow-wrap: break-word; min-width: 0; } +/* nowrap keeps the type tag and the statement actions on the field's own line; the field shrinks instead */ +.statement > .values { grid-column: 2; display: flex; flex-wrap: nowrap; align-items: center; gap: var(--sp-2); min-width: 0; } +.statement > .ac-label ~ .values { grid-column: auto; } +.statement > .annotations { grid-column: 2; display: flex; align-items: center; gap: var(--sp-2); margin-top: var(--sp-1); } +.statement .ac-field { flex: 0 1 340px; min-width: 8rem; } +.statement .values > .ac-tag, .statement .values > .ac-iconbtn { flex-shrink: 0; } +.statement .ac-field:has(textarea) { flex: 1 1 100%; } +.statement.is-invalid .ac-field-box { border-color: var(--danger-500); } +.annotations .ac-field { width: 12ch; flex: 0 0 auto; } + +/* statement actions: the ghost icon buttons get a resting affordance here - without hover chrome around + them, a bare minus glyph reads as punctuation rather than a control */ +.statement .values > .ac-iconbtn { border: 1px solid var(--border-default); color: var(--fg-hint); } + +/* an inlined blank-node resource nests its own fieldset inside the value column: recess it so its rows + read as one contained sub-form. The nesting row wraps (the fieldset takes the full row), plain value + rows stay nowrap */ +.statement > .values:has(> fieldset) { flex-wrap: wrap; } +.statement > .values > fieldset { flex: 1 1 100%; margin: 0; padding: var(--sp-3) var(--sp-4); background: var(--surface-1); border: 1px solid var(--border-default); border-radius: var(--r-md); } +.statement > .values > fieldset legend { font-size: var(--fs-base); } + +.form-actions { display: flex; justify-content: flex-end; gap: var(--sp-2); padding: var(--sp-4) 0 0; } + +/* ---------- SPARQL editor ---------- */ +.query-form .ac-label { display: block; margin-bottom: var(--sp-1); } +.query-form .ac-field { margin-bottom: var(--sp-3); } + +/* ---------- map ---------- */ +.map-canvas { width: 100%; height: 480px; border-radius: var(--r-md); background: var(--surface-2); } + +/* map mode is a full-bleed canvas: the page becomes a viewport-high column and the map takes every + pixel below the bars; the footer and the (empty) aside yield. align-items MUST be stretch - the + base .content aligns flex-start, which sizes .main to its content, and a map canvas has no + intrinsic height, so the whole chain collapsed to 0 */ +.map-view { display: flex; flex-direction: column; height: 100vh; } +.map-view .header, .map-view .action-bar { flex-shrink: 0; } +.map-view .content { flex: 1 1 auto; align-items: stretch; min-height: 0; max-width: none; margin: 0; padding: 0; } +.map-view .main { display: flex; min-width: 0; min-height: 0; } +.map-view .map-canvas { flex: 1 1 auto; height: auto; border-radius: 0; } +.map-view .aside, .map-view .footer { display: none; } + +/* ---------- alerts fill their column ---------- */ +.main > .ac-alert { margin-bottom: var(--sp-4); } diff --git a/src/main/webapp/static/com/atomgraph/client/css/colors_and_type.css b/src/main/webapp/static/com/atomgraph/client/css/colors_and_type.css new file mode 100644 index 00000000..c4bed7f4 --- /dev/null +++ b/src/main/webapp/static/com/atomgraph/client/css/colors_and_type.css @@ -0,0 +1,626 @@ +/* ========================================================================= + LinkedDataHub Design System — Tokens + Base palette + typography for the LinkedDataHub Knowledge Graph platform. + Remix of the platform's legacy palette and Material UI with Base44-style whitespace, + pastel gradients and modern variable-font typography. + ========================================================================= */ + +/* Fonts loaded via Google Fonts — substitutions noted in README. */ +@import url('https://fonts.googleapis.com/css2?family=Geist:wght@300..800&family=Geist+Mono:wght@400..700&family=Instrument+Serif:ital@0;1&display=swap'); + +:root { + /* ---------- Color: Brand / Primary ---------- */ + /* Pulled from the live product. The blue is the action color, the violet the + "structured / semantic" accent (used historically on RDF-typed labels). */ + --ldh-blue-50: #eef5ff; + --ldh-blue-100: #dbeafe; + --ldh-blue-200: #b8d3fd; + --ldh-blue-300: #8bb7fb; + --ldh-blue-400: #4d94f8; + --ldh-blue-500: #0f82f5; /* Brand primary — matches legacy product */ + --ldh-blue-600: #086ad6; + --ldh-blue-700: #0a55a8; + --ldh-blue-800: #0d4382; + --ldh-blue-900: #0a2d57; + + --ldh-violet-50: #faf5ff; + --ldh-violet-100: #f1e6fb; + --ldh-violet-200: #e1cdf6; + --ldh-violet-300: #c9a6ed; + --ldh-violet-400: #ad7adf; + --ldh-violet-500: #9954bb; /* Brand accent — historical RDF tag color */ + --ldh-violet-600: #803fa1; + --ldh-violet-700: #63307c; + --ldh-violet-800: #482358; + --ldh-violet-900: #2e1638; + + /* ---------- Color: Pastel surface palette ---------- */ + /* These wash backgrounds for hero / landing / category surfaces. Always paired + with --ink-1 text. Use as `background: var(--surf-lavender)` or composed in + a mesh gradient (see Visual Foundations in README). */ + --surf-lavender: #efeaff; + --surf-mint: #e6f6ee; + --surf-peach: #ffece1; + --surf-sky: #e3f1ff; + --surf-blush: #ffe6ee; + --surf-sand: #fcf5e7; + + /* Pre-built gradients — used on hero / featured cards. */ + --grad-aurora: linear-gradient(135deg, #efeaff 0%, #e3f1ff 50%, #e6f6ee 100%); /* @kind color */ + --grad-dawn: linear-gradient(135deg, #ffece1 0%, #ffe6ee 60%, #efeaff 100%); /* @kind color */ + --grad-noon: linear-gradient(135deg, #e3f1ff 0%, #ffffff 50%, #efeaff 100%); /* @kind color */ + --grad-deep: linear-gradient(135deg, #0a2d57 0%, #482358 100%); /* @kind color */ + --grad-radial-spot: radial-gradient(60% 60% at 30% 20%, #efeaff 0%, transparent 60%), + radial-gradient(50% 50% at 80% 70%, #e3f1ff 0%, transparent 65%), + #ffffff; /* @kind color */ + + /* ---------- Color: Neutrals ---------- */ + /* Warm-white surfaces, near-black ink. NOT pure greys — slight blue undertone + to harmonise with the brand blue. */ + --ink-1: #0b0e14; /* primary text, headings */ + --ink-2: #2b3245; /* body text */ + --ink-3: #545b6e; /* secondary text, meta */ + --ink-4: #8a91a3; /* tertiary / hint */ + --ink-5: #b6bccc; /* disabled */ + + --surface-0: #ffffff; /* card / dialog */ + --surface-1: #fafbfd; /* page */ + --surface-2: #f3f5f9; /* recessed wells, table stripes */ + --surface-3: #e8ebf2; /* dividers, subtle borders */ + --line-1: #e2e6ee; /* default border */ + --line-2: #cdd3df; /* stronger border, focus ring outer */ + + /* ---------- Color: Semantic ---------- */ + --success-500: #14a06a; + --success-50: #e6f6ee; + --warning-500: #d48a08; + --warning-50: #fff4dc; + --danger-500: #d63a3a; + --danger-600: #b62f2f; + --danger-50: #ffe6e6; + /* Ink for text/icons sitting ON a --danger-500 fill. A token, not a literal: + skins whose danger is a light tint need dark ink here instead of white. */ + --fg-on-danger: #ffffff; + --info-500: var(--ldh-blue-500); + --info-50: var(--ldh-blue-50); + + /* ---------- RDF semantic colors ---------- + Used to colour-code RDF node types in graph / list / table views. + Pulled from the live product (force-graph palette). */ + --rdf-class: #9954bb; /* purple — owl:Class instances */ + --rdf-resource: #4d94f8; /* blue — generic resources */ + --rdf-literal: #14a06a; /* green — literals / values */ + --rdf-blank: #d48a08; /* amber — blank nodes */ + --rdf-named: #ad7adf; /* light violet — named graphs */ + + /* =========================================================================== + PALETTE GOVERNANCE — how to choose among the many hues without drift. + --------------------------------------------------------------------------- + 1. BLUE is the only interaction color. Buttons, links, focus, selected + state — all blue. Nothing else means "clickable". + 2. VIOLET is decoration ONLY where it overlaps RDF: note that --ldh-violet-500 + and --rdf-class are the SAME purple by design. Use the violet ramp for + chrome accents *sparingly* (it reads as "structured/semantic"); use the + --rdf-* tokens whenever the color is ENCODING a node type in data/viz. + Never use violet as a second action color — that competes with blue. + 3. RDF-* tokens are DATA ENCODING, not decoration. Only use them where the + hue carries meaning (graph nodes, type pills, legends). + 4. SEMANTIC (success/warning/danger) = status only, never decoration. + 5. PASTEL surfaces + GRADIENTS are HERO-ONLY: document intros, landing / + category wash, empty-state art. Never behind body content or as a + default card background — that's the fastest route to "gradient slop". + =========================================================================== */ + + /* ---------- Typography: Families ---------- */ + --font-sans: 'Geist', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; + --font-mono: 'Geist Mono', 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace; + --font-display: 'Instrument Serif', Georgia, 'Times New Roman', serif; + + /* ---------- Typography: Scale ---------- */ + /* Modular scale ~1.25 (major third). Tuned for dense info-dense UI plus the + big-headline pastel hero moments. + ROLE MAP (the low end is tight on purpose — assign by role, don't eyeball): + --fs-xs 12 mono meta: IRIs, counts, timestamps, eyebrows, tags + --fs-sm 13 secondary UI: labels, table cells, buttons, captions + --fs-base 15 DEFAULT body & UI text ← start here + --fs-md 17 lead paragraphs / emphasised body only + --fs-lg+ 20+ headings (use the .ldh-h* classes, not raw sizes) + Rule: body/UI is base; drop to sm only for genuinely secondary text; xs is + mono-meta only. If you reach for md on plain UI, you probably want base. */ + --fs-xs: 12px; + --fs-sm: 13px; + --fs-base: 15px; + --fs-md: 17px; + --fs-lg: 20px; + --fs-xl: 24px; + --fs-2xl: 30px; + /* The display end is FLUID; the UI end is not. A fixed 88px headline cannot fit a narrow + container - it does not shrink, it clips, and a clipped word is unreadable in a way a small + one is not (measured: the welcome modal's heading ran 387px inside a 356px body). The floor of + each clamp is the step two below it, so a display heading degrades into the scale rather than + out of it, and the ceiling is the value each step already had, so nothing moves at desktop. + Below 2xl the sizes stay fixed: body and UI text is already at its readable minimum, and + scaling it with the viewport is how interfaces end up with 11px labels on a phone. */ + --fs-3xl: clamp(30px, 6vw, 38px); + --fs-4xl: clamp(34px, 7.5vw, 48px); + --fs-5xl: clamp(38px, 9vw, 64px); + --fs-6xl: clamp(44px, 11vw, 88px); + + /* ---------- Typography: Weights ---------- */ + --fw-regular: 400; /* @kind font */ + --fw-medium: 500; /* @kind font */ + --fw-semibold: 600; /* @kind font */ + --fw-bold: 700; /* @kind font */ + + /* ---------- Typography: Line heights ---------- */ + --lh-tight: 1.1; /* @kind font */ + --lh-snug: 1.25; /* @kind font */ + --lh-base: 1.55; /* @kind font */ + --lh-relaxed: 1.7; /* @kind font */ + + /* ---------- Typography: Letter spacing ---------- */ + --tracking-tight: -0.03em; /* display */ + --tracking-snug: -0.015em; /* headings */ + --tracking-normal: 0; + --tracking-wide: 0.04em; /* eyebrows, all-caps labels */ + --tracking-widest: 0.18em; /* big over-line labels */ + + /* ---------- Radius ---------- */ + --r-xs: 4px; + --r-sm: 6px; + --r-md: 10px; + --r-lg: 16px; + --r-xl: 20px; + --r-2xl: 28px; + --r-pill: 999px; + + /* ---------- Spacing scale (4px base) ---------- */ + --sp-1: 4px; + --sp-2: 8px; + --sp-3: 12px; + --sp-4: 16px; + --sp-5: 20px; + --sp-6: 24px; + --sp-8: 32px; + --sp-10: 40px; + --sp-12: 48px; + --sp-16: 64px; + --sp-20: 80px; + --sp-24: 96px; + --sp-32: 128px; + + /* ---------- Elevation (shadows) ---------- */ + /* Soft, low-contrast shadows — Base44 / glass vibe, not Material's harder cast. */ + --shadow-xs: 0 1px 2px rgba(15, 23, 42, 0.04); + --shadow-sm: 0 2px 8px rgba(15, 23, 42, 0.05), 0 1px 2px rgba(15, 23, 42, 0.04); + --shadow-md: 0 8px 24px -8px rgba(15, 23, 42, 0.10), 0 2px 4px rgba(15, 23, 42, 0.04); + --shadow-lg: 0 24px 48px -16px rgba(15, 23, 42, 0.18), 0 4px 12px rgba(15, 23, 42, 0.05); + --shadow-xl: 0 40px 80px -24px rgba(15, 23, 42, 0.22), 0 8px 16px rgba(15, 23, 42, 0.06); + --shadow-glow-blue: 0 0 0 4px rgba(15, 130, 245, 0.18); + --shadow-glow-violet: 0 0 0 4px rgba(153, 84, 187, 0.16); + + /* Hairline inset highlight for cards on dark backgrounds */ + --inset-hairline: inset 0 1px 0 rgba(255,255,255,0.6); + + /* ---------- Motion ---------- */ + --ease-out-soft: cubic-bezier(0.22, 1, 0.36, 1); /* @kind other */ + --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* @kind other */ + --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* @kind other */ + --dur-fast: 120ms; /* @kind other */ + --dur-base: 200ms; /* @kind other */ + --dur-slow: 360ms; /* @kind other */ + --dur-xslow: 640ms; /* @kind other */ + + /* ---------- Layout ---------- */ + --container-sm: 720px; + --container-md: 960px; + --container-lg: 1200px; + --container-xl: 1440px; + + /* ---------- Layout: the responsive scale ---------- + THREE STOPS, and which query type each belongs to. Custom properties cannot appear in a media + or container condition, so these are documentation and the literals are written out at each + use - keep the two in step. + + 520px CONTAINER stop. A component collapses to one column. + 768px VIEWPORT stop. The page shell compacts: wordmark to its mark, tighter gutters. + 1024px VIEWPORT stop. Two-column page layouts drop the gutter. + + WHICH ONE depends on what the component's container actually is, and getting this wrong is + the bug this scale was introduced to fix: + + - The page shell (header, tab strip, action bar, footer, rails) sits directly under the + viewport, so the viewport IS its container: @media. + - Everything in the content column does not. The same statement grid renders at the + document's full width, inside a nested block, inside the 300px content aside and inside a + 372px modal body - four widths at ONE viewport. Measured: it got a 36px value column in + the aside at a 1440px desktop, worse than the 26px it got on a 390px phone. @container. + + 520 is derived, not picked: the statement grid spends 200px on its label and 64px on its row + actions, so three columns need 200 + 240 of readable value + 64 to hold. Three labelled + selects need about the same. Two components, one number - which is what makes it a scale + rather than a pile of breakpoints. It replaces four one-off values (560, 640, 860, 900) that + shared nothing. + + A component queries a container only if one is DECLARED above it. The declaring surfaces are + .ac-card (surfaces.css), .ac-modal-body (overlays.css), and .ldh-block / .ldh-nblock / + .ldh-content-aside in the app layer. Deliberately NOT .content-body: ldh:ShowModalForm appends + .ac-backdrop (position: fixed; inset: 0) to it. */ + --bp-container: 520px; + --bp-shell-sm: 768px; + --bp-shell-md: 1024px; + + /* ---------- Semantic: Foreground tokens (consume these in components) ---------- */ + --fg-1: var(--ink-1); + --fg-2: var(--ink-2); + --fg-muted: var(--ink-3); + /* Hint text: deliberately NOT ink-4 — ink-4 (#8a91a3) is ~3:1 on white and + fails WCAG AA for text. ink-4 is reserved for non-text use (icons that pair + with a label, hairline borders, disabled glyphs). For actual hint *text* + use this AA-safe value (~5.7:1 on the page surface). */ + --fg-hint: #5f6677; + --fg-on-accent: #ffffff; + --fg-link: var(--ldh-blue-600); + + --bg-page: var(--surface-1); + --bg-card: var(--surface-0); + --bg-recess: var(--surface-2); + /* Action color for text-bearing fills (filled buttons). blue-500 (#0f82f5) + is only ~3.3:1 against white text — it FAILS AA. blue-600 (#086ad6) clears + it at ~4.6:1, so the accent fill is pinned to 600, not 500. blue-500 stays + available as a primitive for non-text accents (dots, focus glints). */ + --bg-accent: var(--ldh-blue-600); + --bg-accent-hover: var(--ldh-blue-700); + --bg-accent-quiet: var(--ldh-blue-50); + + /* Inverse ground — a FIXED white, identical in every theme, for controls that + sit on a dark bar or over imagery where the surrounding theme cannot be + relied on. Deliberately not --surface-0, which flips with the theme. */ + --bg-inverse: #ffffff; /* @kind color */ + --bg-inverse-hover: rgba(255,255,255,0.88); /* @kind color */ + + /* ---------- Semantic: Interactive states ---------- + First-class state tokens so components stop hand-rolling `surface-2` for + every hover. Use these for any hoverable/selectable surface. */ + --bg-hover: var(--surface-2); /* neutral hover (rows, ghost buttons, tabs) */ + --bg-active: var(--surface-3); /* pressed / held */ + --bg-selected: var(--bg-accent-quiet); /* current / on / selected surface */ + --fg-selected: var(--ldh-blue-700); /* text/icon on a selected surface */ + + --border-default: var(--line-1); + --border-strong: var(--line-2); + --focus-ring: var(--shadow-glow-blue); +} + +/* ========================================================================= + DARK THEME — set [data-theme="dark"] on . + The light theme funnels nearly everything through the neutral primitives + (ink-*, surface-*, line-*) and a small set of accent tints, so dark mode is + mostly a re-mapping of those primitives — the semantic layer and components + inherit it automatically. Accent *text* tints (blue/violet 600/700) flip + light; accent *fills* are pinned to literals so filled buttons keep AA. + ========================================================================= */ +:root[data-theme="dark"] { + /* Neutrals — cool dark slate, mirroring the light theme's blue undertone. */ + --ink-1: #f3f6fc; + --ink-2: #cfd6e3; + --ink-3: #9aa3b5; + --ink-4: #717a8d; + --ink-5: #4c5365; + + --surface-0: #171b24; /* card / dialog */ + --surface-1: #0f131a; /* page */ + --surface-2: #1e2430; /* recessed wells, hover */ + --surface-3: #2b323f; /* pressed, dividers */ + --line-1: #272e3a; + --line-2: #39414f; + + /* Accent TEXT tints flip light so they stay legible on dark surfaces. */ + --ldh-blue-50: #11243d; + --ldh-blue-100: #173558; + --ldh-blue-200: #1f4d7a; + --ldh-blue-600: #6aa6f7; + --ldh-blue-700: #93bdfb; + --ldh-violet-50: #241836; + --ldh-violet-100: #33214d; + --ldh-violet-700: #c9a6ed; + --success-500: #3ecb8f; --success-50: #102619; + --warning-500: #e6a52a; --warning-50: #2c2410; + --danger-500: #f06a6a; --danger-600: #d94f4f; --danger-50: #2e1414; + --fg-on-danger: #2e0f0f; + + /* Accent FILL stays a literal saturated blue so white button text keeps + AA (≈4.6:1) — independent of the now-light blue-600 text primitive. */ + --bg-accent: #086ad6; + --bg-accent-hover: #0a55a8; + + --fg-hint: #8a92a4; /* ~4.8:1 on the dark page */ + + /* Shadows need more weight to read on dark surfaces. */ + --shadow-xs: 0 1px 2px rgba(0,0,0,0.40); + --shadow-sm: 0 2px 8px rgba(0,0,0,0.45), 0 1px 2px rgba(0,0,0,0.40); + --shadow-md: 0 8px 24px -8px rgba(0,0,0,0.55), 0 2px 4px rgba(0,0,0,0.40); + --shadow-lg: 0 24px 48px -16px rgba(0,0,0,0.60), 0 4px 12px rgba(0,0,0,0.45); + --shadow-xl: 0 40px 80px -24px rgba(0,0,0,0.66), 0 8px 16px rgba(0,0,0,0.45); + --inset-hairline: inset 0 1px 0 rgba(255,255,255,0.06); +} + +/* ========================================================================= + Semantic typography classes — apply directly, or use as a reference for + component-level styles. + ========================================================================= */ + +.ldh-display { + font-family: var(--font-display); + font-weight: 400; + font-size: var(--fs-6xl); + line-height: var(--lh-tight); + letter-spacing: var(--tracking-tight); + color: var(--fg-1); + font-style: italic; +} + +.ldh-h1 { + font-family: var(--font-sans); + font-weight: var(--fw-semibold); + font-size: var(--fs-4xl); + line-height: var(--lh-tight); + letter-spacing: var(--tracking-tight); + color: var(--fg-1); +} + +.ldh-h2 { + font-family: var(--font-sans); + font-weight: var(--fw-semibold); + font-size: var(--fs-3xl); + line-height: var(--lh-snug); + letter-spacing: var(--tracking-snug); + color: var(--fg-1); +} + +.ldh-h3 { + font-family: var(--font-sans); + font-weight: var(--fw-semibold); + font-size: var(--fs-2xl); + line-height: var(--lh-snug); + letter-spacing: var(--tracking-snug); + color: var(--fg-1); +} + +.ldh-h4 { + font-family: var(--font-sans); + font-weight: var(--fw-semibold); + font-size: var(--fs-xl); + line-height: var(--lh-snug); + color: var(--fg-1); +} + +.ldh-h5 { + font-family: var(--font-sans); + font-weight: var(--fw-medium); + font-size: var(--fs-lg); + line-height: var(--lh-snug); + color: var(--fg-1); +} + +.ldh-body { + font-family: var(--font-sans); + font-weight: var(--fw-regular); + font-size: var(--fs-base); + line-height: var(--lh-base); + color: var(--fg-2); +} + +.ldh-body-lg { + font-family: var(--font-sans); + font-weight: var(--fw-regular); + font-size: var(--fs-md); + line-height: var(--lh-base); + color: var(--fg-2); +} + +.ldh-small { + font-family: var(--font-sans); + font-weight: var(--fw-regular); + font-size: var(--fs-sm); + line-height: var(--lh-base); + color: var(--fg-muted); +} + +.ldh-meta { + font-family: var(--font-sans); + font-weight: var(--fw-medium); + font-size: var(--fs-xs); + line-height: var(--lh-snug); + letter-spacing: var(--tracking-wide); + text-transform: uppercase; + color: var(--fg-muted); +} + +.ldh-eyebrow { + font-family: var(--font-mono); + font-weight: var(--fw-medium); + font-size: var(--fs-xs); + line-height: var(--lh-snug); + letter-spacing: var(--tracking-widest); + text-transform: uppercase; + color: var(--ldh-blue-600); +} + +.ldh-code { + font-family: var(--font-mono); + font-weight: var(--fw-regular); + font-size: 0.92em; + line-height: var(--lh-base); + color: var(--ink-2); + background: var(--surface-2); + padding: 1px 6px; + border-radius: var(--r-xs); +} + +/* Inline IRI / URI styling — a recurring element across LDH. */ +.ldh-iri { + font-family: var(--font-mono); + font-size: 0.94em; + color: var(--ldh-blue-700); + word-break: break-all; +} + +/* Reset-ish baseline for design previews */ +html, body { background: var(--bg-page); color: var(--fg-1); } +body { + font-family: var(--font-sans); + font-size: var(--fs-base); + line-height: var(--lh-base); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +/* ========================================================================= + LONG-FORM ELEMENTS + + Documentation pages, XHTML content blocks and marketing copy are authored as + plain semantic XHTML, so their dress belongs on the ELEMENT. A class earns + its place in that markup only when the author is choosing something the tag + cannot express — a lede paragraph, a table density — never to restate what + the tag already says. already says it is code. + + There is a second reason it has to be the element rather than a class or a + wrapper scope. The same literal renders in two places: the documentation + site, wrapped in .docs-main, and the product, wrapped in the XHTML block. An + element rule is the only form that works in both without either stylesheet + having to know the other's wrapper — which is exactly what the docs site's + own element layer was standing in for. + + Everything here binds to the .ldh-* roles above rather than inventing values, + so the two cannot drift. The classes keep no margin of their own, so they + stay usable inline on a component; flow spacing lives on the element. + + NOT covered yet, deliberately: ul/ol/li, because one emitter (the endpoint + class list in the search modal) is a bare
    with no class to outrank a + prose rule — a control list wearing list bullets. And p, which renders + acceptably on UA margins today; giving it one would hand every alert and card + body a trailing margin to undo. + ========================================================================= */ + +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-sans); + color: var(--fg-1); + text-wrap: balance; +} + +/* Sizes match the .ldh-h* role of the same name — the classes are named after + the levels, so an h2 that is not .ldh-h2 would make the names lie. */ +h1 { + font-size: var(--fs-4xl); + font-weight: var(--fw-semibold); + line-height: var(--lh-tight); + letter-spacing: var(--tracking-tight); + margin: 0 0 var(--sp-6); +} +h2 { + font-size: var(--fs-3xl); + font-weight: var(--fw-semibold); + line-height: var(--lh-snug); + letter-spacing: var(--tracking-snug); + margin: var(--sp-10) 0 var(--sp-4); +} +h3 { + font-size: var(--fs-2xl); + font-weight: var(--fw-semibold); + line-height: var(--lh-snug); + letter-spacing: var(--tracking-snug); + margin: var(--sp-8) 0 var(--sp-3); +} +h4 { + font-size: var(--fs-xl); + font-weight: var(--fw-semibold); + line-height: var(--lh-snug); + margin: var(--sp-6) 0 var(--sp-2); +} +h5, h6 { + font-size: var(--fs-lg); + font-weight: var(--fw-medium); + line-height: var(--lh-snug); + margin: var(--sp-5) 0 var(--sp-2); +} +/* A heading opening its section has nothing above it to be spaced from. */ +h1:first-child, h2:first-child, h3:first-child, +h4:first-child, h5:first-child, h6:first-child { margin-top: 0; } + +/* Inline code, sample output and key caps. The README's content rule — "inline + code, file paths and IRIs are set in mono" — applied to the tags that say so. + kbd adds the cap edge; samp and code are the same treatment because the + distinction between them is semantic, not visual. */ +code, samp, kbd { + font-family: var(--font-mono); + font-weight: var(--fw-regular); + font-size: 0.92em; + color: var(--ink-2); + background: var(--surface-2); + padding: 1px 6px; + border-radius: var(--r-xs); +} +kbd { + border: 1px solid var(--border-default); + /* the cap edge is a shadow, not a thicker border — the system has no 2px border */ + box-shadow: 0 1px 0 var(--border-strong); + padding: 1px 5px; +} + +/* Code blocks sit in a recessed well with no border: the fill already separates + them, and a border plus a fill reads as two containers. */ +pre { + margin: var(--sp-4) 0; + padding: var(--sp-3) var(--sp-4); + background: var(--surface-2); + border-radius: var(--r-sm); + font-family: var(--font-mono); + font-size: 0.92em; + line-height: var(--lh-base); + color: var(--ink-2); + overflow-x: auto; +} +/* Inside a block the chip would be a second well. */ +pre code, pre samp, pre kbd { + background: none; + padding: 0; + border: 0; + border-radius: 0; + font-size: inherit; +} + +/* Description lists. The app's property lists are a dl too — read mode and the + edit form both emit dl.ldh-prop-form > dt.label + dd.ldh-prop-row — and they + are immune to these rules by construction: .ldh-prop-form already sets + margin: 0 to cancel the UA block margin, .ldh-prop-row is display: contents so + it generates no box for a margin to land on, and the label cell is reached at + .ldh-prop-group > .label. The graph panels scope their own dl/dt/dd. So what + is left for these rules is prose. */ +dl { margin: var(--sp-4) 0; } +dt { font-weight: var(--fw-medium); color: var(--fg-1); } +dt:not(:first-child) { margin-top: var(--sp-3); } +dd { margin: var(--sp-1) 0 0; margin-inline-start: var(--sp-5); color: var(--fg-2); } + +/* A term at its point of definition. */ +dfn { font-style: italic; font-weight: var(--fw-medium); color: var(--fg-1); } + +/* A highlighted run. Amber rather than the action blue: marking is not a link. */ +mark { background: var(--warning-50); color: var(--fg-1); padding: 0 2px; border-radius: var(--r-xs); } + +/* A quotation is set off by a rule and a step down in colour, not by italics — + the quoted text sets its own voice. */ +blockquote { + margin: var(--sp-4) 0; + padding: var(--sp-1) 0 var(--sp-1) var(--sp-4); + border-left: 1px solid var(--border-strong); + color: var(--fg-muted); +} + +/* A thematic break is the element form of .ac-divider wt-muted. */ +hr { + height: 1px; + margin: var(--sp-8) 0; + border: 0; + background: var(--border-default); +} diff --git a/src/main/webapp/static/com/atomgraph/client/css/controls.css b/src/main/webapp/static/com/atomgraph/client/css/controls.css new file mode 100644 index 00000000..4f100b36 --- /dev/null +++ b/src/main/webapp/static/com/atomgraph/client/css/controls.css @@ -0,0 +1,372 @@ +/* ========================================================================= + ui_kits/core/controls.css — form-control primitives + Select · Combobox · IconButton · FileInput · CodeField + + Every control here carries the full core contract: default / hover / active / + focus-visible / disabled, plus loading and invalid where relevant. Metrics + match .ac-field-box exactly (sm 32 / md 40 / lg 48) so a select, a combobox + and a text field line up in the same row. + + ONE FOCUS RING. Everything below uses --focus-ring. No control defines its + own; no app-layer override defines one either. + ========================================================================= */ + +/* ========================================================================= + Select — the design-system replacement for a native - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Literal - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Resource - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Language - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Datatype - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Resource - - - - \ No newline at end of file diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/imports/dh.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/imports/dh.xsl deleted file mode 100644 index 8bf8abc2..00000000 --- a/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/imports/dh.xsl +++ /dev/null @@ -1,36 +0,0 @@ - - - - -]> - - - - - - - - - \ No newline at end of file diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/imports/foaf.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/imports/foaf.xsl deleted file mode 100644 index 0bbf415c..00000000 --- a/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/imports/foaf.xsl +++ /dev/null @@ -1,73 +0,0 @@ - - - - -]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/imports/sp.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/imports/sp.xsl deleted file mode 100644 index 38d8510c..00000000 --- a/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/imports/sp.xsl +++ /dev/null @@ -1,48 +0,0 @@ - - - - -]> - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/resource.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/resource.xsl deleted file mode 100644 index 13ee2e25..00000000 --- a/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/resource.xsl +++ /dev/null @@ -1,339 +0,0 @@ - - - - - - - - - - -]> - - - - - - - - - - - - - - - - - - - - - - - -
  • - - - - / - -
  • -
    - - - - - - - -
    - - - - - - - - - - -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - - - - - - - - - -

    - -

    - -

    - -

    - - -
    -
    - - - - - - -
  • - - - - - -
  • -
    - - - - -
    -
    - -
    -
    - - -
    - - - - - - - - - - - - - - - - - {ac:label($this)} - - - - - - - - - - - - {ac:label($this)} - - - - - - -
      - - - -
    • - -
    • -
      -
    -
    - - - - - - - - -
    - - - - -
    -
    -
    - - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - - - - - - - - - - - Template is not defined for resource '' with types '' - - - - - - -
    -
    - - - - - - - - - - - - - - - -

    - -

    -
    -
    -
    - - - - - - - - - - -
    - - - - - - - -
    -
    - -
    \ No newline at end of file diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/common.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/common.xsl new file mode 100644 index 00000000..88449a2d --- /dev/null +++ b/src/main/webapp/static/com/atomgraph/client/xsl/common.xsl @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/container.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/container.xsl similarity index 50% rename from src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/container.xsl rename to src/main/webapp/static/com/atomgraph/client/xsl/container.xsl index 2db48231..ab441e58 100644 --- a/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/container.xsl +++ b/src/main/webapp/static/com/atomgraph/client/xsl/container.xsl @@ -19,7 +19,6 @@ limitations under the License. - ]> - - + +
      + +
    - + - + -
    +
  • @@ -51,53 +50,44 @@ exclude-result-prefixes="#all"> - - - + + +

    -

    - -

    + +

    + +

    +
    - + - + -
  • +
    - + - - + + - +
      - - - - - -
      -
        - -
      -
      -
      +
    - - + + - - +
  • @@ -107,30 +97,32 @@ exclude-result-prefixes="#all"> -
    - +
    + -
    - +
    +

    -

    - -

    + +

    + +

    +
  • - + - + - + @@ -147,16 +139,19 @@ exclude-result-prefixes="#all"> + + + - + - Resource + - - + + @@ -167,7 +162,7 @@ exclude-result-prefixes="#all"> - + @@ -191,7 +186,7 @@ exclude-result-prefixes="#all"> - + @@ -201,68 +196,62 @@ exclude-result-prefixes="#all"> - + - + + -
    - -
    - - - -
    - - - - +
    \ No newline at end of file diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/document.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/document.xsl similarity index 64% rename from src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/document.xsl rename to src/main/webapp/static/com/atomgraph/client/xsl/document.xsl index 0468e36c..d945b286 100644 --- a/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/document.xsl +++ b/src/main/webapp/static/com/atomgraph/client/xsl/document.xsl @@ -19,9 +19,7 @@ limitations under the License. - - ]> - - - - - - - - - + @@ -66,12 +52,13 @@ exclude-result-prefixes="#all"> - + - + + - - + + @@ -95,20 +82,13 @@ exclude-result-prefixes="#all"> - + - - - - - - - - - - + + + - + @@ -116,17 +96,20 @@ exclude-result-prefixes="#all"> - + - - - + + +
    - +
    diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/external-layout.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/external-layout.xsl similarity index 56% rename from src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/external-layout.xsl rename to src/main/webapp/static/com/atomgraph/client/xsl/external-layout.xsl index 613fd249..9c83e269 100644 --- a/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/external-layout.xsl +++ b/src/main/webapp/static/com/atomgraph/client/xsl/external-layout.xsl @@ -36,32 +36,14 @@ xmlns:sparql="&sparql;" xmlns:xhtml="http://www.w3.org/1999/xhtml" exclude-result-prefixes="#all"> - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + +
    diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/functions.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/functions.xsl index 74e98c96..427bc947 100644 --- a/src/main/webapp/static/com/atomgraph/client/xsl/functions.xsl +++ b/src/main/webapp/static/com/atomgraph/client/xsl/functions.xsl @@ -19,14 +19,6 @@ limitations under the License. - - - - - - - - ]> - - - - + + + + + + + + @@ -69,26 +59,6 @@ exclude-result-prefixes="#all" - - - - - - - - Not implemented -- com.atomgraph.client.writer.function.ConstructForClass needs to be registered as an extension function - - - - - - - - - Not implemented -- com.atomgraph.client.writer.function.Construct needs to be registered as an extension function - - - + + + + @@ -184,85 +160,6 @@ exclude-result-prefixes="#all" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -297,21 +194,4 @@ exclude-result-prefixes="#all" return if ($absolute-path) then $absolute-path || $query-string else $query-string)"/> - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/imports/dbpedia-owl.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/imports/dbpedia-owl.xsl deleted file mode 100644 index de1249ee..00000000 --- a/src/main/webapp/static/com/atomgraph/client/xsl/imports/dbpedia-owl.xsl +++ /dev/null @@ -1,66 +0,0 @@ - - - - - -]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/imports/dc.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/imports/dc.xsl index f3baaa07..9c7c5101 100644 --- a/src/main/webapp/static/com/atomgraph/client/xsl/imports/dc.xsl +++ b/src/main/webapp/static/com/atomgraph/client/xsl/imports/dc.xsl @@ -28,20 +28,12 @@ xmlns:rdf="&rdf;" xmlns:dc="&dc;" exclude-result-prefixes="#all"> - - - - - - - - - + - + \ No newline at end of file diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/imports/dct.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/imports/dct.xsl index 232f7296..9df7dd83 100644 --- a/src/main/webapp/static/com/atomgraph/client/xsl/imports/dct.xsl +++ b/src/main/webapp/static/com/atomgraph/client/xsl/imports/dct.xsl @@ -28,20 +28,32 @@ xmlns:rdf="&rdf;" xmlns:dct="&dct;" exclude-result-prefixes="#all"> - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/imports/default.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/imports/default.xsl index e12e576a..ad49e5f7 100644 --- a/src/main/webapp/static/com/atomgraph/client/xsl/imports/default.xsl +++ b/src/main/webapp/static/com/atomgraph/client/xsl/imports/default.xsl @@ -22,9 +22,7 @@ limitations under the License. - - ]> - + @@ -133,19 +127,24 @@ exclude-result-prefixes="#all"> - - + + + + + + + + - +
    - +
    @@ -180,6 +179,7 @@ exclude-result-prefixes="#all"> + @@ -187,6 +187,9 @@ exclude-result-prefixes="#all"> + + + @@ -422,7 +425,7 @@ exclude-result-prefixes="#all"> - + @@ -443,7 +446,7 @@ exclude-result-prefixes="#all"> - + @@ -462,27 +465,27 @@ exclude-result-prefixes="#all"> - - + + - + - +
    - +
    @@ -491,7 +494,7 @@ exclude-result-prefixes="#all"> - +
    @@ -505,7 +508,7 @@ exclude-result-prefixes="#all"> - +
    @@ -513,10 +516,10 @@ exclude-result-prefixes="#all">
    - + - + @@ -529,11 +532,15 @@ exclude-result-prefixes="#all"> + +
    + +
    - + @@ -541,31 +548,31 @@ exclude-result-prefixes="#all"> - - + + - + - + - + - + @@ -774,4 +781,850 @@ exclude-result-prefixes="#all"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + +
    +
    + + + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + +
    + + + + + + + + +
    + +
    + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + +
    + + + + +
    + + +
    + + +
    + + + +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + +
    + + + + + + + + +
    + + + + + + + + + + + + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + +
    +                    
    +                
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/imports/doap.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/imports/doap.xsl index 294a9962..a0d9bf8e 100644 --- a/src/main/webapp/static/com/atomgraph/client/xsl/imports/doap.xsl +++ b/src/main/webapp/static/com/atomgraph/client/xsl/imports/doap.xsl @@ -28,24 +28,23 @@ xmlns:rdf="&rdf;" xmlns:doap="&doap;" exclude-result-prefixes="#all"> - - - - - - - - - + - + - + + + + + + + + diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/imports/external.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/imports/external.xsl index 6edc44fb..4ab7d6b0 100644 --- a/src/main/webapp/static/com/atomgraph/client/xsl/imports/external.xsl +++ b/src/main/webapp/static/com/atomgraph/client/xsl/imports/external.xsl @@ -38,8 +38,17 @@ xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:svg="http://www.w3.org/2000/svg" exclude-result-prefixes="#all"> + + + + + + + + - + @@ -55,7 +64,7 @@ exclude-result-prefixes="#all"> - + @@ -71,7 +80,7 @@ exclude-result-prefixes="#all"> - + diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/imports/foaf.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/imports/foaf.xsl index 1924d8cb..7d75a7d7 100644 --- a/src/main/webapp/static/com/atomgraph/client/xsl/imports/foaf.xsl +++ b/src/main/webapp/static/com/atomgraph/client/xsl/imports/foaf.xsl @@ -26,10 +26,20 @@ xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:ac="∾" xmlns:rdf="&rdf;" xmlns:foaf="&foaf;" +xmlns:xhtml="http://www.w3.org/1999/xhtml" exclude-result-prefixes="#all"> + - + + + + + + + @@ -56,8 +66,16 @@ exclude-result-prefixes="#all"> + - + + + + + + + + @@ -108,4 +126,71 @@ exclude-result-prefixes="#all"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/imports/ldt.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/imports/ldt.xsl deleted file mode 100644 index 9a7884c1..00000000 --- a/src/main/webapp/static/com/atomgraph/client/xsl/imports/ldt.xsl +++ /dev/null @@ -1,37 +0,0 @@ - - - - - -]> - - - -
    -            
    -        
    -
    - -
    \ No newline at end of file diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/imports/rdf.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/imports/rdf.xsl index 65e96ef6..7558c636 100644 --- a/src/main/webapp/static/com/atomgraph/client/xsl/imports/rdf.xsl +++ b/src/main/webapp/static/com/atomgraph/client/xsl/imports/rdf.xsl @@ -15,30 +15,45 @@ See the License for the specific language governing permissions and limitations under the License. --> ]> + - - - - - - - - - + + + + + + + + + + + + + + + + + + diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/imports/rdfs.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/imports/rdfs.xsl index c6af4bc3..b7ccad02 100644 --- a/src/main/webapp/static/com/atomgraph/client/xsl/imports/rdfs.xsl +++ b/src/main/webapp/static/com/atomgraph/client/xsl/imports/rdfs.xsl @@ -44,4 +44,24 @@ exclude-result-prefixes="#all">
    + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/imports/schema.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/imports/schema.xsl index 230af128..0b6334dc 100644 --- a/src/main/webapp/static/com/atomgraph/client/xsl/imports/schema.xsl +++ b/src/main/webapp/static/com/atomgraph/client/xsl/imports/schema.xsl @@ -30,40 +30,38 @@ xmlns:schema1="&schema1;" xmlns:schema2="&schema2;" exclude-result-prefixes="#all"> - - + + + - - + + - - + + - - + + - - + + - - - + + + + - - - + + + + - - - - - - diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/imports/sioc.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/imports/sioc.xsl index 336ca511..deef78ab 100644 --- a/src/main/webapp/static/com/atomgraph/client/xsl/imports/sioc.xsl +++ b/src/main/webapp/static/com/atomgraph/client/xsl/imports/sioc.xsl @@ -28,12 +28,8 @@ xmlns:rdf="&rdf;" xmlns:sioc="&sioc;" exclude-result-prefixes="#all"> - - - - - + diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/imports/skos.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/imports/skos.xsl index 29d169fc..8fc6aa31 100644 --- a/src/main/webapp/static/com/atomgraph/client/xsl/imports/skos.xsl +++ b/src/main/webapp/static/com/atomgraph/client/xsl/imports/skos.xsl @@ -28,12 +28,8 @@ xmlns:rdf="&rdf;" xmlns:skos="&skos;" exclude-result-prefixes="#all"> - - - - - + \ No newline at end of file diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/imports/sp.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/imports/sp.xsl index 5eebab10..06dbb8ee 100644 --- a/src/main/webapp/static/com/atomgraph/client/xsl/imports/sp.xsl +++ b/src/main/webapp/static/com/atomgraph/client/xsl/imports/sp.xsl @@ -34,4 +34,28 @@ exclude-result-prefixes="#all"> + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/internal-layout.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/internal-layout.xsl similarity index 57% rename from src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/internal-layout.xsl rename to src/main/webapp/static/com/atomgraph/client/xsl/internal-layout.xsl index 5ac28fae..2af59845 100644 --- a/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/internal-layout.xsl +++ b/src/main/webapp/static/com/atomgraph/client/xsl/internal-layout.xsl @@ -36,31 +36,11 @@ xmlns:sparql="&sparql;" xmlns:xhtml="http://www.w3.org/1999/xhtml" exclude-result-prefixes="#all"> - - - - - - - - - - - - - - - - - - - - - - - - + + + + diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/layout.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/layout.xsl similarity index 55% rename from src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/layout.xsl rename to src/main/webapp/static/com/atomgraph/client/xsl/layout.xsl index 4f829737..13c33d06 100644 --- a/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/layout.xsl +++ b/src/main/webapp/static/com/atomgraph/client/xsl/layout.xsl @@ -24,12 +24,10 @@ limitations under the License. - - ]> - - - + - - - - - - - @@ -97,18 +83,6 @@ exclude-result-prefixes="#all"> AtomGraph - - Previous - - - - Next - - - - Delete - - @@ -121,98 +95,92 @@ exclude-result-prefixes="#all"> - + - +
    - + - + - + - + - + + + + + + + + + + - + + + -
    -
    - + - -
    +
    + + +
    - - + + - - -
- + - + - +
  • + + + @@ -463,55 +436,64 @@ exclude-result-prefixes="#all">
  • - + - + + - + -
    - - - - - - + + + + + +

    + + + +

    -

    - - - -

    -
    + + + + +
    - -
    +
    - + - + - + - +
    @@ -525,9 +507,9 @@ exclude-result-prefixes="#all">
    - + - + diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/resource.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/resource.xsl new file mode 100644 index 00000000..96dbe466 --- /dev/null +++ b/src/main/webapp/static/com/atomgraph/client/xsl/resource.xsl @@ -0,0 +1,237 @@ + + + + + + + +]> + + + + + + + + +
    + + + + + + + + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + +

    + +

    + + +

    + +

    +
    + + +
    +
    + + + + + + + + + + + + + + + + + + + + {ac:label($this)} + + + + + + + + + + + + + {ac:label($this)} + + + + + + + +
      + + + +
    • + +
    • +
      +
    +
    + + + + + + + + +
    + + + + +
    +
    +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + +
    \ No newline at end of file diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/sparql.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/sparql.xsl similarity index 80% rename from src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/sparql.xsl rename to src/main/webapp/static/com/atomgraph/client/xsl/sparql.xsl index 8daa5e98..2d03201a 100644 --- a/src/main/webapp/static/com/atomgraph/client/xsl/bootstrap/2.3.2/sparql.xsl +++ b/src/main/webapp/static/com/atomgraph/client/xsl/sparql.xsl @@ -23,7 +23,6 @@ limitations under the License. - @@ -40,10 +39,8 @@ xmlns:rdfs="&rdfs;" xmlns:owl="&owl;" xmlns:srx="&srx;" xmlns:sd="&sd;" -xmlns:ldt="&ldt;" xmlns:spl="&spl;" xmlns:void="&void;" -xmlns:bs2="http://graphity.org/xsl/bootstrap/2.3.2" exclude-result-prefixes="#all"> SELECT DISTINCT * @@ -58,9 +55,9 @@ WHERE } LIMIT 100 - + - +
    @@ -70,7 +67,7 @@ LIMIT 100 - + @@ -84,19 +81,19 @@ LIMIT 100
    - + - + - + - + @@ -120,14 +117,21 @@ LIMIT 100
    - - - - - - - - ",l.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var da=/^key/,ea=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,fa=/^([^.]*)(?:\.(.+)|)/;function ga(){return!0}function ha(){return!1}function ia(){try{return d.activeElement}catch(a){}}function ja(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ja(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=ha;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return"undefined"!=typeof n&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(G)||[""],j=b.length;while(j--)h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.hasData(a)&&N.get(a);if(r&&(i=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&N.remove(a,"handle events")}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(N.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!==this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,la=/\s*$/g;function pa(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function qa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function ra(a){var b=na.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function sa(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(N.hasData(a)&&(f=N.access(a),g=N.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}O.hasData(a)&&(h=O.access(a),i=n.extend({},h),O.set(b,i))}}function ta(a,b){var c=b.nodeName.toLowerCase();"input"===c&&X.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function ua(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&ma.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),ua(f,b,c,d)});if(o&&(e=ca(b,a[0].ownerDocument,!1,a,d),g=e.firstChild,1===e.childNodes.length&&(e=g),g||d)){for(h=n.map(_(e,"script"),qa),i=h.length;o>m;m++)j=e,m!==p&&(j=n.clone(j,!0,!0),i&&n.merge(h,_(j,"script"))),c.call(a[m],j,m);if(i)for(k=h[h.length-1].ownerDocument,n.map(h,ra),m=0;i>m;m++)j=h[m],Z.test(j.type||"")&&!N.access(j,"globalEval")&&n.contains(k,j)&&(j.src?n._evalUrl&&n._evalUrl(j.src):n.globalEval(j.textContent.replace(oa,"")))}return a}function va(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(_(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&aa(_(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(ka,"<$1>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=_(h),f=_(a),d=0,e=f.length;e>d;d++)ta(f[d],g[d]);if(b)if(c)for(f=f||_(a),g=g||_(h),d=0,e=f.length;e>d;d++)sa(f[d],g[d]);else sa(a,h);return g=_(h,"script"),g.length>0&&aa(g,!i&&_(a,"script")),h},cleanData:function(a){for(var b,c,d,e=n.event.special,f=0;void 0!==(c=a[f]);f++)if(L(c)){if(b=c[N.expando]){if(b.events)for(d in b.events)e[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);c[N.expando]=void 0}c[O.expando]&&(c[O.expando]=void 0)}}}),n.fn.extend({domManip:ua,detach:function(a){return va(this,a,!0)},remove:function(a){return va(this,a)},text:function(a){return K(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.appendChild(a)}})},prepend:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(_(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return K(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!la.test(a)&&!$[(Y.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(_(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return ua(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(_(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),f=e.length-1,h=0;f>=h;h++)c=h===f?this:this.clone(!0),n(e[h])[b](c),g.apply(d,c.get());return this.pushStack(d)}});var wa,xa={HTML:"block",BODY:"block"};function ya(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function za(a){var b=d,c=xa[a];return c||(c=ya(a,b),"none"!==c&&c||(wa=(wa||n("