From 68a462b05a5ceefd61784c7216f1c099f1f3f8bf Mon Sep 17 00:00:00 2001 From: grahamtriggs Date: Fri, 23 Dec 2016 21:55:08 +0000 Subject: [PATCH] [VIVO-1312] Implement Linked Data Fragment Server (#53) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [VIVO-1312] Linked Data Fragments initial implementation * [VIVO-1312] Use known ontologies in the prefixes config * [VIVO-1312] Simplify SPARQL as when restricted to specific values, they don’t need ordering * [VIVO-1312] Freemarker fixes * [VIVO-1312] Remove blank nodes * [VIVO-1312] Add access control header and standard prefixes for TPF * [VIVO-1312] Render literals in form so that they will work on resubmit * [VIVO-1312] Minor template update * [VIVO-1312] Minor template update --- .../config/ConfigReader.java | 119 +++++++ .../datasource/AbstractRequestProcessor.java | 76 +++++ ...ractRequestProcessorForTriplePatterns.java | 158 +++++++++ .../datasource/DataSourceBase.java | 54 ++++ .../datasource/DataSourceFactory.java | 35 ++ .../datasource/DataSourceTypesRegistry.java | 51 +++ .../datasource/IDataSource.java | 38 +++ .../datasource/IDataSourceType.java | 33 ++ .../datasource/IFragmentRequestProcessor.java | 26 ++ .../datasource/index/IndexDataSource.java | 47 +++ .../index/IndexRequestProcessorForTPFs.java | 146 +++++++++ .../JenaTDBBasedRequestProcessorForTPFs.java | 165 ++++++++++ .../datasource/tdb/JenaTDBDataSource.java | 47 +++ .../datasource/tdb/JenaTDBDataSourceType.java | 34 ++ .../DataSourceCreationException.java | 25 ++ .../exceptions/DataSourceException.java | 37 +++ .../DataSourceNotFoundException.java | 16 + .../NoRegisteredMimeTypesException.java | 17 + .../UnknownDataSourceTypeException.java | 16 + .../fragments/FragmentRequestParserBase.java | 141 +++++++++ .../fragments/IFragmentRequestParser.java | 29 ++ .../fragments/ILinkedDataFragment.java | 79 +++++ .../fragments/ILinkedDataFragmentRequest.java | 48 +++ .../fragments/LinkedDataFragmentBase.java | 189 +++++++++++ .../LinkedDataFragmentRequestBase.java | 81 +++++ .../fragments/tpf/ITriplePatternElement.java | 89 ++++++ .../fragments/tpf/ITriplePatternFragment.java | 15 + .../tpf/ITriplePatternFragmentRequest.java | 52 +++ .../fragments/tpf/TPFRequestParser.java | 127 ++++++++ .../tpf/TPFRequestParserForJenaBackends.java | 35 ++ .../tpf/TriplePatternElementFactory.java | 214 +++++++++++++ .../tpf/TriplePatternFragmentBase.java | 164 ++++++++++ .../tpf/TriplePatternFragmentImpl.java | 72 +++++ .../tpf/TriplePatternFragmentRequestImpl.java | 96 ++++++ .../servlet/LinkedDataFragmentServlet.java | 213 +++++++++++++ .../util/CommonResources.java | 139 ++++++++ .../linkeddatafragments/util/MIMEParse.java | 299 ++++++++++++++++++ .../util/RDFTermParser.java | 116 +++++++ .../util/TriplePatternElementParser.java | 80 +++++ .../TriplePatternElementParserForJena.java | 128 ++++++++ .../HtmlTriplePatternFragmentWriterImpl.java | 145 +++++++++ .../views/ILinkedDataFragmentWriter.java | 44 +++ .../views/LinkedDataFragmentWriterBase.java | 42 +++ .../LinkedDataFragmentWriterFactory.java | 35 ++ .../views/RdfWriterImpl.java | 55 ++++ .../TriplePatternFragmentWriterBase.java | 46 +++ ...DFServiceBasedRequestProcessorForTPFs.java | 201 ++++++++++++ .../rdfservice/RDFServiceDataSource.java | 46 +++ .../rdfservice/RDFServiceDataSourceType.java | 31 ++ .../VitroLinkedDataFragmentServlet.java | 270 ++++++++++++++++ .../HtmlTriplePatternFragmentWriterImpl.java | 221 +++++++++++++ .../LinkedDataFragmentWriterFactory.java | 36 +++ .../linkeddatafragments/servlet/favicon.ico | Bin 0 -> 318 bytes .../linkeddatafragments/servlet/logo.svg | 67 ++++ .../linkeddatafragments/servlet/style.css | 247 +++++++++++++++ api/src/main/resources/tpf/base.ftl.html | 30 ++ .../main/resources/tpf/datasource.ftl.html | 7 + api/src/main/resources/tpf/error.ftl.html | 11 + api/src/main/resources/tpf/fragment.ftl.html | 83 +++++ api/src/main/resources/tpf/index.ftl.html | 21 ++ api/src/main/resources/tpf/notfound.ftl.html | 16 + dependencies/pom.xml | 5 + webapp/src/main/webapp/WEB-INF/web.xml | 10 + 63 files changed, 5215 insertions(+) create mode 100644 api/src/main/java/org/linkeddatafragments/config/ConfigReader.java create mode 100644 api/src/main/java/org/linkeddatafragments/datasource/AbstractRequestProcessor.java create mode 100644 api/src/main/java/org/linkeddatafragments/datasource/AbstractRequestProcessorForTriplePatterns.java create mode 100644 api/src/main/java/org/linkeddatafragments/datasource/DataSourceBase.java create mode 100644 api/src/main/java/org/linkeddatafragments/datasource/DataSourceFactory.java create mode 100644 api/src/main/java/org/linkeddatafragments/datasource/DataSourceTypesRegistry.java create mode 100644 api/src/main/java/org/linkeddatafragments/datasource/IDataSource.java create mode 100644 api/src/main/java/org/linkeddatafragments/datasource/IDataSourceType.java create mode 100644 api/src/main/java/org/linkeddatafragments/datasource/IFragmentRequestProcessor.java create mode 100644 api/src/main/java/org/linkeddatafragments/datasource/index/IndexDataSource.java create mode 100644 api/src/main/java/org/linkeddatafragments/datasource/index/IndexRequestProcessorForTPFs.java create mode 100644 api/src/main/java/org/linkeddatafragments/datasource/tdb/JenaTDBBasedRequestProcessorForTPFs.java create mode 100644 api/src/main/java/org/linkeddatafragments/datasource/tdb/JenaTDBDataSource.java create mode 100644 api/src/main/java/org/linkeddatafragments/datasource/tdb/JenaTDBDataSourceType.java create mode 100644 api/src/main/java/org/linkeddatafragments/exceptions/DataSourceCreationException.java create mode 100644 api/src/main/java/org/linkeddatafragments/exceptions/DataSourceException.java create mode 100644 api/src/main/java/org/linkeddatafragments/exceptions/DataSourceNotFoundException.java create mode 100644 api/src/main/java/org/linkeddatafragments/exceptions/NoRegisteredMimeTypesException.java create mode 100644 api/src/main/java/org/linkeddatafragments/exceptions/UnknownDataSourceTypeException.java create mode 100644 api/src/main/java/org/linkeddatafragments/fragments/FragmentRequestParserBase.java create mode 100644 api/src/main/java/org/linkeddatafragments/fragments/IFragmentRequestParser.java create mode 100644 api/src/main/java/org/linkeddatafragments/fragments/ILinkedDataFragment.java create mode 100644 api/src/main/java/org/linkeddatafragments/fragments/ILinkedDataFragmentRequest.java create mode 100644 api/src/main/java/org/linkeddatafragments/fragments/LinkedDataFragmentBase.java create mode 100644 api/src/main/java/org/linkeddatafragments/fragments/LinkedDataFragmentRequestBase.java create mode 100644 api/src/main/java/org/linkeddatafragments/fragments/tpf/ITriplePatternElement.java create mode 100644 api/src/main/java/org/linkeddatafragments/fragments/tpf/ITriplePatternFragment.java create mode 100644 api/src/main/java/org/linkeddatafragments/fragments/tpf/ITriplePatternFragmentRequest.java create mode 100644 api/src/main/java/org/linkeddatafragments/fragments/tpf/TPFRequestParser.java create mode 100644 api/src/main/java/org/linkeddatafragments/fragments/tpf/TPFRequestParserForJenaBackends.java create mode 100644 api/src/main/java/org/linkeddatafragments/fragments/tpf/TriplePatternElementFactory.java create mode 100644 api/src/main/java/org/linkeddatafragments/fragments/tpf/TriplePatternFragmentBase.java create mode 100644 api/src/main/java/org/linkeddatafragments/fragments/tpf/TriplePatternFragmentImpl.java create mode 100644 api/src/main/java/org/linkeddatafragments/fragments/tpf/TriplePatternFragmentRequestImpl.java create mode 100644 api/src/main/java/org/linkeddatafragments/servlet/LinkedDataFragmentServlet.java create mode 100644 api/src/main/java/org/linkeddatafragments/util/CommonResources.java create mode 100644 api/src/main/java/org/linkeddatafragments/util/MIMEParse.java create mode 100644 api/src/main/java/org/linkeddatafragments/util/RDFTermParser.java create mode 100644 api/src/main/java/org/linkeddatafragments/util/TriplePatternElementParser.java create mode 100644 api/src/main/java/org/linkeddatafragments/util/TriplePatternElementParserForJena.java create mode 100644 api/src/main/java/org/linkeddatafragments/views/HtmlTriplePatternFragmentWriterImpl.java create mode 100644 api/src/main/java/org/linkeddatafragments/views/ILinkedDataFragmentWriter.java create mode 100644 api/src/main/java/org/linkeddatafragments/views/LinkedDataFragmentWriterBase.java create mode 100644 api/src/main/java/org/linkeddatafragments/views/LinkedDataFragmentWriterFactory.java create mode 100644 api/src/main/java/org/linkeddatafragments/views/RdfWriterImpl.java create mode 100644 api/src/main/java/org/linkeddatafragments/views/TriplePatternFragmentWriterBase.java create mode 100644 api/src/main/java/org/vivoweb/linkeddatafragments/datasource/rdfservice/RDFServiceBasedRequestProcessorForTPFs.java create mode 100644 api/src/main/java/org/vivoweb/linkeddatafragments/datasource/rdfservice/RDFServiceDataSource.java create mode 100644 api/src/main/java/org/vivoweb/linkeddatafragments/datasource/rdfservice/RDFServiceDataSourceType.java create mode 100644 api/src/main/java/org/vivoweb/linkeddatafragments/servlet/VitroLinkedDataFragmentServlet.java create mode 100644 api/src/main/java/org/vivoweb/linkeddatafragments/views/HtmlTriplePatternFragmentWriterImpl.java create mode 100644 api/src/main/java/org/vivoweb/linkeddatafragments/views/LinkedDataFragmentWriterFactory.java create mode 100644 api/src/main/resources/org/vivoweb/linkeddatafragments/servlet/favicon.ico create mode 100644 api/src/main/resources/org/vivoweb/linkeddatafragments/servlet/logo.svg create mode 100644 api/src/main/resources/org/vivoweb/linkeddatafragments/servlet/style.css create mode 100644 api/src/main/resources/tpf/base.ftl.html create mode 100644 api/src/main/resources/tpf/datasource.ftl.html create mode 100644 api/src/main/resources/tpf/error.ftl.html create mode 100644 api/src/main/resources/tpf/fragment.ftl.html create mode 100644 api/src/main/resources/tpf/index.ftl.html create mode 100644 api/src/main/resources/tpf/notfound.ftl.html diff --git a/api/src/main/java/org/linkeddatafragments/config/ConfigReader.java b/api/src/main/java/org/linkeddatafragments/config/ConfigReader.java new file mode 100644 index 000000000..3db1d638f --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/config/ConfigReader.java @@ -0,0 +1,119 @@ +package org.linkeddatafragments.config; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.linkeddatafragments.datasource.IDataSourceType; + +import java.io.Reader; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Reads the configuration of a Linked Data Fragments server. + * + * @author Ruben Verborgh + * @author Olaf Hartig + */ +public class ConfigReader { + private final Map dataSourceTypes = new HashMap<>(); + private final Map dataSources = new HashMap<>(); + private final Map prefixes = new HashMap<>(); + private final String baseURL; + + /** + * Creates a new configuration reader. + * + * @param configReader the configuration + */ + public ConfigReader(Reader configReader) { + JsonObject root = new JsonParser().parse(configReader).getAsJsonObject(); + this.baseURL = root.has("baseURL") ? root.getAsJsonPrimitive("baseURL").getAsString() : null; + + for (Entry entry : root.getAsJsonObject("datasourcetypes").entrySet()) { + final String className = entry.getValue().getAsString(); + dataSourceTypes.put(entry.getKey(), initDataSouceType(className) ); + } + for (Entry entry : root.getAsJsonObject("datasources").entrySet()) { + JsonObject dataSource = entry.getValue().getAsJsonObject(); + this.dataSources.put(entry.getKey(), dataSource); + } + for (Entry entry : root.getAsJsonObject("prefixes").entrySet()) { + this.prefixes.put(entry.getKey(), entry.getValue().getAsString()); + } + } + + /** + * Gets the data source types. + * + * @return a mapping of names of data source types to these types + */ + public Map getDataSourceTypes() { + return dataSourceTypes; + } + + /** + * Gets the data sources. + * + * @return the data sources + */ + public Map getDataSources() { + return dataSources; + } + + /** + * Gets the prefixes. + * + * @return the prefixes + */ + public Map getPrefixes() { + return prefixes; + } + + /** + * Gets the base URL + * + * @return the base URL + */ + public String getBaseURL() { + return baseURL; + } + + /** + * Loads a certain {@link IDataSourceType} class at runtime + * + * @param className IDataSourceType class + * @return the created IDataSourceType object + */ + protected IDataSourceType initDataSouceType(final String className ) + { + final Class c; + try { + c = Class.forName( className ); + } + catch ( ClassNotFoundException e ) { + throw new IllegalArgumentException( "Class not found: " + className, + e ); + } + + final Object o; + try { + o = c.newInstance(); + } + catch ( Exception e ) { + throw new IllegalArgumentException( + "Creating an instance of class '" + className + "' " + + "caused a " + e.getClass().getSimpleName() + ": " + + e.getMessage(), e ); + } + + if ( ! (o instanceof IDataSourceType) ) + throw new IllegalArgumentException( + "Class '" + className + "' is not an implementation " + + "of IDataSourceType." ); + + return (IDataSourceType) o; + } + +} diff --git a/api/src/main/java/org/linkeddatafragments/datasource/AbstractRequestProcessor.java b/api/src/main/java/org/linkeddatafragments/datasource/AbstractRequestProcessor.java new file mode 100644 index 000000000..6cfa14ef8 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/datasource/AbstractRequestProcessor.java @@ -0,0 +1,76 @@ +package org.linkeddatafragments.datasource; + +import org.linkeddatafragments.fragments.ILinkedDataFragment; +import org.linkeddatafragments.fragments.ILinkedDataFragmentRequest; + +/** + * Base class for implementations of {@link IFragmentRequestProcessor}. + * + * @author Olaf Hartig + */ +abstract public class AbstractRequestProcessor + implements IFragmentRequestProcessor +{ + @Override + public void close() {} + + /** + * Create an {@link ILinkedDataFragment} from {@link ILinkedDataFragmentRequest} + * + * @param request + * @return + * @throws IllegalArgumentException + */ + @Override + final public ILinkedDataFragment createRequestedFragment( + final ILinkedDataFragmentRequest request ) + throws IllegalArgumentException + { + return getWorker( request ).createRequestedFragment(); + } + + /** + * Get the {@link Worker} from {@link ILinkedDataFragmentRequest} + * + * @param request + * @return + * @throws IllegalArgumentException + */ + abstract protected Worker getWorker( + final ILinkedDataFragmentRequest request ) + throws IllegalArgumentException; + + /** + * Processes {@link ILinkedDataFragmentRequest}s + * + */ + abstract static protected class Worker + { + + /** + * The {@link ILinkedDataFragmentRequest} to process + */ + public final ILinkedDataFragmentRequest request; + + /** + * Create a Worker + * + * @param request + */ + public Worker( final ILinkedDataFragmentRequest request ) + { + this.request = request; + } + + /** + * Create the requested {@link ILinkedDataFragment} + * + * @return The ILinkedDataFragment + * @throws IllegalArgumentException + */ + abstract public ILinkedDataFragment createRequestedFragment() + throws IllegalArgumentException; + + } // end of class Worker + +} diff --git a/api/src/main/java/org/linkeddatafragments/datasource/AbstractRequestProcessorForTriplePatterns.java b/api/src/main/java/org/linkeddatafragments/datasource/AbstractRequestProcessorForTriplePatterns.java new file mode 100644 index 000000000..de8070b7d --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/datasource/AbstractRequestProcessorForTriplePatterns.java @@ -0,0 +1,158 @@ +package org.linkeddatafragments.datasource; + +import org.apache.jena.rdf.model.Model; +import org.linkeddatafragments.fragments.ILinkedDataFragment; +import org.linkeddatafragments.fragments.ILinkedDataFragmentRequest; +import org.linkeddatafragments.fragments.tpf.ITriplePatternElement; +import org.linkeddatafragments.fragments.tpf.ITriplePatternFragment; +import org.linkeddatafragments.fragments.tpf.ITriplePatternFragmentRequest; +import org.linkeddatafragments.fragments.tpf.TriplePatternFragmentImpl; + +/** + * Base class for implementations of {@link IFragmentRequestProcessor} that + * process {@link ITriplePatternFragmentRequest}s. + * + * @param + * type for representing constants in triple patterns (i.e., URIs and + * literals) + * @param + * type for representing named variables in triple patterns + * @param + * type for representing anonymous variables in triple patterns (i.e., + * variables denoted by a blank node) + * + * @author Olaf Hartig + */ +public abstract class + AbstractRequestProcessorForTriplePatterns + extends AbstractRequestProcessor +{ + + /** + * + * @param request + * @return + * @throws IllegalArgumentException + */ + @Override + protected final Worker getWorker( + final ILinkedDataFragmentRequest request ) + throws IllegalArgumentException + { + if ( request instanceof ITriplePatternFragmentRequest) { + @SuppressWarnings("unchecked") + final ITriplePatternFragmentRequest tpfRequest = + (ITriplePatternFragmentRequest) request; + return getTPFSpecificWorker( tpfRequest ); + } + else + throw new IllegalArgumentException( request.getClass().getName() ); + } + + /** + * + * @param request + * @return + * @throws IllegalArgumentException + */ + abstract protected Worker getTPFSpecificWorker( + final ITriplePatternFragmentRequest request ) + throws IllegalArgumentException; + + /** + * + * @param + * @param + * @param + */ + abstract static protected class Worker + extends AbstractRequestProcessor.Worker + { + + /** + * + * @param request + */ + public Worker( + final ITriplePatternFragmentRequest request ) + { + super( request ); + } + + /** + * + * @return + * @throws IllegalArgumentException + */ + @Override + public ILinkedDataFragment createRequestedFragment() + throws IllegalArgumentException + { + final long limit = ILinkedDataFragmentRequest.TRIPLESPERPAGE; + final long offset; + if ( request.isPageRequest() ) + offset = limit * ( request.getPageNumber() - 1L ); + else + offset = 0L; + + @SuppressWarnings("unchecked") + final ITriplePatternFragmentRequest tpfRequest = + (ITriplePatternFragmentRequest) request; + + return createFragment( tpfRequest.getSubject(), + tpfRequest.getPredicate(), + tpfRequest.getObject(), + offset, limit ); + } + + /** + * + * @param subj + * @param pred + * @param obj + * @param offset + * @param limit + * @return + * @throws IllegalArgumentException + */ + abstract protected ILinkedDataFragment createFragment( + final ITriplePatternElement subj, + final ITriplePatternElement pred, + final ITriplePatternElement obj, + final long offset, + final long limit ) + throws IllegalArgumentException; + + /** + * + * @return + */ + protected ITriplePatternFragment createEmptyTriplePatternFragment() + { + return new TriplePatternFragmentImpl( request.getFragmentURL(), + request.getDatasetURL() ); + } + + /** + * + * @param triples + * @param totalSize + * @param isLastPage + * @return + */ + protected ITriplePatternFragment createTriplePatternFragment( + final Model triples, + final long totalSize, + final boolean isLastPage ) + { + return new TriplePatternFragmentImpl( triples, + totalSize, + request.getFragmentURL(), + request.getDatasetURL(), + request.getPageNumber(), + isLastPage ); + } + + } // end of class Worker + +} diff --git a/api/src/main/java/org/linkeddatafragments/datasource/DataSourceBase.java b/api/src/main/java/org/linkeddatafragments/datasource/DataSourceBase.java new file mode 100644 index 000000000..45b669f6a --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/datasource/DataSourceBase.java @@ -0,0 +1,54 @@ +package org.linkeddatafragments.datasource; + +/** + * The base class for an {@link IDataSource} + * + * @author Miel Vander Sande + * @author Bart Hanssens + */ +public abstract class DataSourceBase implements IDataSource { + + /** + * Get the datasource title + */ + protected String title; + + /** + * Get the datasource description + */ + protected String description; + + /** + * Create a base for a {@link IDataSource} + * + * @param title the datasource title + * @param description the datasource description + */ + public DataSourceBase(String title, String description) { + this.title = title; + this.description = description; + } + + /** + * Get the datasource description + * + * @return + */ + @Override + public String getDescription() { + return this.description; + }; + + /** + * Get the datasource title + * + * @return + */ + @Override + public String getTitle() { + return this.title; + }; + + @Override + public void close() {} +} diff --git a/api/src/main/java/org/linkeddatafragments/datasource/DataSourceFactory.java b/api/src/main/java/org/linkeddatafragments/datasource/DataSourceFactory.java new file mode 100644 index 000000000..d23741d54 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/datasource/DataSourceFactory.java @@ -0,0 +1,35 @@ +package org.linkeddatafragments.datasource; + +import com.google.gson.JsonObject; +import org.linkeddatafragments.exceptions.DataSourceCreationException; +import org.linkeddatafragments.exceptions.UnknownDataSourceTypeException; + +/** + * + * @author Miel Vander Sande + * @author Bart Hanssens + * @author Olaf Hartig + */ +public class DataSourceFactory { + /** + * Create a datasource using a JSON config + * + * @param config + * @return datasource interface + * @throws DataSourceCreationException + */ + public static IDataSource create(JsonObject config) throws DataSourceCreationException { + String title = config.getAsJsonPrimitive("title").getAsString(); + String description = config.getAsJsonPrimitive("description").getAsString(); + String typeName = config.getAsJsonPrimitive("type").getAsString(); + + JsonObject settings = config.getAsJsonObject("settings"); + + final IDataSourceType type = DataSourceTypesRegistry.getType(typeName); + if ( type == null ) + throw new UnknownDataSourceTypeException(typeName); + + return type.createDataSource( title, description, settings ); + } + +} diff --git a/api/src/main/java/org/linkeddatafragments/datasource/DataSourceTypesRegistry.java b/api/src/main/java/org/linkeddatafragments/datasource/DataSourceTypesRegistry.java new file mode 100644 index 000000000..f502c6f69 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/datasource/DataSourceTypesRegistry.java @@ -0,0 +1,51 @@ +package org.linkeddatafragments.datasource; + +import java.util.HashMap; +import java.util.Map; + +/** + * A registry of {@link IDataSourceType}s. + * + * @author Olaf Hartig + */ +public class DataSourceTypesRegistry +{ + private static Map registry = + new HashMap(); + + /** + * + * @param typeName + * @return + */ + public static synchronized IDataSourceType getType( final String typeName ) + { + return registry.get( typeName ); + } + + /** + * + * @param typeName + * @return + */ + public static synchronized boolean isRegistered( final String typeName ) + { + return registry.containsKey( typeName ); + } + + /** + * + * @param typeName + * @param type + */ + public static synchronized void register( final String typeName, + final IDataSourceType type ) + { + if ( registry.containsKey(typeName) ) { + throw new IllegalArgumentException( "The registry already " + + "contains a type with the name '" + typeName + "'." ); + } + registry.put( typeName, type ); + } + +} diff --git a/api/src/main/java/org/linkeddatafragments/datasource/IDataSource.java b/api/src/main/java/org/linkeddatafragments/datasource/IDataSource.java new file mode 100644 index 000000000..971e18422 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/datasource/IDataSource.java @@ -0,0 +1,38 @@ +package org.linkeddatafragments.datasource; + +import org.linkeddatafragments.fragments.IFragmentRequestParser; + +import java.io.Closeable; + +/** + * A data source of Linked Data Fragments. + * + * @author Ruben Verborgh + * @author Olaf Hartig + */ +public interface IDataSource extends Closeable { + + /** + * + * @return + */ + public String getTitle(); + + /** + * + * @return + */ + public String getDescription(); + + /** + * Returns a data source specific {@link IFragmentRequestParser}. + * @return + */ + IFragmentRequestParser getRequestParser(); + + /** + * Returns a data source specific {@link IFragmentRequestProcessor}. + * @return + */ + IFragmentRequestProcessor getRequestProcessor(); +} diff --git a/api/src/main/java/org/linkeddatafragments/datasource/IDataSourceType.java b/api/src/main/java/org/linkeddatafragments/datasource/IDataSourceType.java new file mode 100644 index 000000000..b9a19f760 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/datasource/IDataSourceType.java @@ -0,0 +1,33 @@ +package org.linkeddatafragments.datasource; + +import com.google.gson.JsonObject; +import org.linkeddatafragments.exceptions.DataSourceCreationException; + +/** + * Represents types of {@link IDataSource}s that can be used to provide some + * Linked Data Fragments interface. + * + * @author Olaf Hartig + */ +public interface IDataSourceType +{ + /** + * Creates a data source of this type. + * + * @param title + * The title of the data source (as given in the config file). + * + * @param description + * The description of the data source (as given in the config file). + * + * @param settings + * The properties of the data source to be created; usually, these + * properties are given in the config file of the LDF server. + * @return + * @throws org.linkeddatafragments.exceptions.DataSourceCreationException + */ + IDataSource createDataSource(final String title, + final String description, + final JsonObject settings) + throws DataSourceCreationException; +} diff --git a/api/src/main/java/org/linkeddatafragments/datasource/IFragmentRequestProcessor.java b/api/src/main/java/org/linkeddatafragments/datasource/IFragmentRequestProcessor.java new file mode 100644 index 000000000..b3f48bef1 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/datasource/IFragmentRequestProcessor.java @@ -0,0 +1,26 @@ +package org.linkeddatafragments.datasource; + +import org.linkeddatafragments.fragments.ILinkedDataFragment; +import org.linkeddatafragments.fragments.ILinkedDataFragmentRequest; + +import java.io.Closeable; + +/** + * Processes {@link ILinkedDataFragmentRequest}s and returns + * the requested {@link ILinkedDataFragment}s. + * + * @author Olaf Hartig + */ +public interface IFragmentRequestProcessor extends Closeable +{ + + /** + * + * @param request + * @return + * @throws IllegalArgumentException + */ + ILinkedDataFragment createRequestedFragment( + final ILinkedDataFragmentRequest request) + throws IllegalArgumentException; +} diff --git a/api/src/main/java/org/linkeddatafragments/datasource/index/IndexDataSource.java b/api/src/main/java/org/linkeddatafragments/datasource/index/IndexDataSource.java new file mode 100644 index 000000000..90536702e --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/datasource/index/IndexDataSource.java @@ -0,0 +1,47 @@ +package org.linkeddatafragments.datasource.index; + +import org.linkeddatafragments.datasource.DataSourceBase; +import org.linkeddatafragments.datasource.IDataSource; +import org.linkeddatafragments.datasource.IFragmentRequestProcessor; +import org.linkeddatafragments.fragments.IFragmentRequestParser; +import org.linkeddatafragments.fragments.tpf.TPFRequestParserForJenaBackends; + +import java.util.HashMap; + +/** + * An Index data source provides an overview of all available datasets. + * + * @author Miel Vander Sande + * @author Olaf Hartig + */ +public class IndexDataSource extends DataSourceBase { + + /** + * The request processor + * + */ + protected final IndexRequestProcessorForTPFs requestProcessor; + + /** + * + * @param baseUrl + * @param datasources + */ + public IndexDataSource(String baseUrl, HashMap datasources) { + super("Index", "List of all datasources"); + requestProcessor = new IndexRequestProcessorForTPFs( baseUrl, datasources ); + } + + @Override + public IFragmentRequestParser getRequestParser() + { + return TPFRequestParserForJenaBackends.getInstance(); + } + + @Override + public IFragmentRequestProcessor getRequestProcessor() + { + return requestProcessor; + } + +} diff --git a/api/src/main/java/org/linkeddatafragments/datasource/index/IndexRequestProcessorForTPFs.java b/api/src/main/java/org/linkeddatafragments/datasource/index/IndexRequestProcessorForTPFs.java new file mode 100644 index 000000000..35e632ab1 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/datasource/index/IndexRequestProcessorForTPFs.java @@ -0,0 +1,146 @@ +package org.linkeddatafragments.datasource.index; + + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; +import org.apache.jena.rdf.model.StmtIterator; +import org.apache.jena.rdf.model.impl.PropertyImpl; +import org.apache.jena.rdf.model.impl.ResourceImpl; +import org.linkeddatafragments.datasource.AbstractRequestProcessorForTriplePatterns; +import org.linkeddatafragments.datasource.IDataSource; +import org.linkeddatafragments.datasource.IFragmentRequestProcessor; +import org.linkeddatafragments.fragments.ILinkedDataFragment; +import org.linkeddatafragments.fragments.tpf.ITriplePatternElement; +import org.linkeddatafragments.fragments.tpf.ITriplePatternFragmentRequest; + +import java.util.HashMap; +import java.util.Map; + +/** + * Implementation of {@link IFragmentRequestProcessor} that processes + * {@link ITriplePatternFragmentRequest}s over an index that provides + * an overview of all available datasets. + * + * @author Miel Vander Sande + * @author Olaf Hartig + */ +public class IndexRequestProcessorForTPFs + extends AbstractRequestProcessorForTriplePatterns +{ + final static String RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; + final static String RDFS = "http://www.w3.org/2000/01/rdf-schema#"; + final static String DC = "http://purl.org/dc/terms/"; + final static String VOID = "http://rdfs.org/ns/void#"; + + private final Model model; + + /** + * + * @param baseUrl + * @param datasources + */ + public IndexRequestProcessorForTPFs( + final String baseUrl, + final HashMap datasources ) + { + this.model = ModelFactory.createDefaultModel(); + + for (Map.Entry entry : datasources.entrySet()) { + String datasourceName = entry.getKey(); + IDataSource datasource = entry.getValue(); + + Resource datasourceUrl = new ResourceImpl(baseUrl + "/" + datasourceName); + + model.add(datasourceUrl, new PropertyImpl(RDF + "type"), VOID + "Dataset"); + model.add(datasourceUrl, new PropertyImpl(RDFS + "label"), datasource.getTitle()); + model.add(datasourceUrl, new PropertyImpl(DC + "title"), datasource.getTitle()); + model.add(datasourceUrl, new PropertyImpl(DC + "description"), datasource.getDescription()); + } + } + + /** + * + * @param request + * @return + * @throws IllegalArgumentException + */ + @Override + protected Worker getTPFSpecificWorker( + final ITriplePatternFragmentRequest request ) + throws IllegalArgumentException + { + return new Worker( request ); + } + + /** + * Worker for the index + */ + protected class Worker + extends AbstractRequestProcessorForTriplePatterns.Worker + { + + /** + * Creates a Worker for the datasource index + * + * @param req + */ + public Worker( + final ITriplePatternFragmentRequest req ) + { + super( req ); + } + + /** + * + * @param s + * @param p + * @param o + * @param offset + * @param limit + * @return + */ + @Override + protected ILinkedDataFragment createFragment( + final ITriplePatternElement s, + final ITriplePatternElement p, + final ITriplePatternElement o, + final long offset, + final long limit ) + { + // FIXME: The following algorithm is incorrect for cases in which + // the requested triple pattern contains a specific variable + // multiple times; + // e.g., (?x foaf:knows ?x ) or (_:bn foaf:knows _:bn) + // see https://github.com/LinkedDataFragments/Server.Java/issues/25 + + final Resource subject = s.isVariable() ? null + : s.asConstantTerm().asResource(); + final Property predicate = p.isVariable() ? null + : ResourceFactory.createProperty(p.asConstantTerm().asResource().getURI()); + final RDFNode object = o.isVariable() ? null + : o.asConstantTerm(); + + StmtIterator listStatements = model.listStatements(subject, predicate, object); + Model result = ModelFactory.createDefaultModel(); + + long index = 0; + while (listStatements.hasNext() && index < offset) { + listStatements.next(); + index++; + } + + while (listStatements.hasNext() && index < (offset + limit)) { + result.add(listStatements.next()); + } + + final boolean isLastPage = ( result.size() < offset + limit ); + return createTriplePatternFragment( result, result.size(), isLastPage ); + } + + } // end of class Worker + +} diff --git a/api/src/main/java/org/linkeddatafragments/datasource/tdb/JenaTDBBasedRequestProcessorForTPFs.java b/api/src/main/java/org/linkeddatafragments/datasource/tdb/JenaTDBBasedRequestProcessorForTPFs.java new file mode 100644 index 000000000..804d9482c --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/datasource/tdb/JenaTDBBasedRequestProcessorForTPFs.java @@ -0,0 +1,165 @@ +package org.linkeddatafragments.datasource.tdb; + +import org.apache.jena.query.Dataset; +import org.apache.jena.query.Query; +import org.apache.jena.query.QueryExecution; +import org.apache.jena.query.QueryExecutionFactory; +import org.apache.jena.query.QueryFactory; +import org.apache.jena.query.QuerySolution; +import org.apache.jena.query.QuerySolutionMap; +import org.apache.jena.query.ResultSet; +import org.apache.jena.query.Syntax; +import org.apache.jena.rdf.model.Literal; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.tdb.TDBFactory; +import org.linkeddatafragments.datasource.AbstractRequestProcessorForTriplePatterns; +import org.linkeddatafragments.datasource.IFragmentRequestProcessor; +import org.linkeddatafragments.fragments.ILinkedDataFragment; +import org.linkeddatafragments.fragments.tpf.ITriplePatternElement; +import org.linkeddatafragments.fragments.tpf.ITriplePatternFragmentRequest; + +import java.io.File; + +/** + * Implementation of {@link IFragmentRequestProcessor} that processes + * {@link ITriplePatternFragmentRequest}s over data stored in Jena TDB. + * + * @author Bart Hanssens + * @author Olaf Hartig + */ +public class JenaTDBBasedRequestProcessorForTPFs + extends AbstractRequestProcessorForTriplePatterns +{ + private final Dataset tdb; + private final String sparql = "CONSTRUCT WHERE { ?s ?p ?o } " + + "ORDER BY ?s ?p ?o"; + + private final String count = "SELECT (COUNT(?s) AS ?count) WHERE { ?s ?p ?o }"; + + private final Query query = QueryFactory.create(sparql, Syntax.syntaxSPARQL_11); + private final Query countQuery = QueryFactory.create(count, Syntax.syntaxSPARQL_11); + + /** + * + * @param request + * @return + * @throws IllegalArgumentException + */ + @Override + protected Worker getTPFSpecificWorker( + final ITriplePatternFragmentRequest request ) + throws IllegalArgumentException + { + return new Worker( request ); + } + + /** + * + */ + protected class Worker + extends AbstractRequestProcessorForTriplePatterns.Worker + { + + /** + * + * @param req + */ + public Worker( + final ITriplePatternFragmentRequest req ) + { + super( req ); + } + + /** + * + * @param subject + * @param predicate + * @param object + * @param offset + * @param limit + * @return + */ + @Override + protected ILinkedDataFragment createFragment( + final ITriplePatternElement subject, + final ITriplePatternElement predicate, + final ITriplePatternElement object, + final long offset, + final long limit ) + { + // FIXME: The following algorithm is incorrect for cases in which + // the requested triple pattern contains a specific variable + // multiple times; + // e.g., (?x foaf:knows ?x ) or (_:bn foaf:knows _:bn) + // see https://github.com/LinkedDataFragments/Server.Java/issues/24 + + Model model = tdb.getDefaultModel(); + QuerySolutionMap map = new QuerySolutionMap(); + if ( ! subject.isVariable() ) { + map.add("s", subject.asConstantTerm()); + } + if ( ! predicate.isVariable() ) { + map.add("p", predicate.asConstantTerm()); + } + if ( ! object.isVariable() ) { + map.add("o", object.asConstantTerm()); + } + + query.setOffset(offset); + query.setLimit(limit); + + Model triples = ModelFactory.createDefaultModel(); + + try (QueryExecution qexec = QueryExecutionFactory.create(query, model, map)) { + qexec.execConstruct(triples); + } + + if (triples.isEmpty()) { + return createEmptyTriplePatternFragment(); + } + + // Try to get an estimate + long size = triples.size(); + long estimate = -1; + + try (QueryExecution qexec = QueryExecutionFactory.create(countQuery, model, map)) { + ResultSet results = qexec.execSelect(); + if (results.hasNext()) { + QuerySolution soln = results.nextSolution() ; + Literal literal = soln.getLiteral("count"); + estimate = literal.getLong(); + } + } + + /*GraphStatisticsHandler stats = model.getGraph().getStatisticsHandler(); + if (stats != null) { + Node s = (subject != null) ? subject.asNode() : null; + Node p = (predicate != null) ? predicate.asNode() : null; + Node o = (object != null) ? object.asNode() : null; + estimate = stats.getStatistic(s, p, o); + }*/ + + // No estimate or incorrect + if (estimate < offset + size) { + estimate = (size == limit) ? offset + size + 1 : offset + size; + } + + // create the fragment + final boolean isLastPage = ( estimate < offset + limit ); + return createTriplePatternFragment( triples, estimate, isLastPage ); + } + + } // end of class Worker + + + /** + * Constructor + * + * @param tdbdir directory used for TDB backing + */ + public JenaTDBBasedRequestProcessorForTPFs(File tdbdir) { + this.tdb = TDBFactory.createDataset(tdbdir.getAbsolutePath()); + } +} diff --git a/api/src/main/java/org/linkeddatafragments/datasource/tdb/JenaTDBDataSource.java b/api/src/main/java/org/linkeddatafragments/datasource/tdb/JenaTDBDataSource.java new file mode 100644 index 000000000..58e35b3ae --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/datasource/tdb/JenaTDBDataSource.java @@ -0,0 +1,47 @@ +package org.linkeddatafragments.datasource.tdb; + +import org.linkeddatafragments.datasource.DataSourceBase; +import org.linkeddatafragments.datasource.IFragmentRequestProcessor; +import org.linkeddatafragments.fragments.IFragmentRequestParser; +import org.linkeddatafragments.fragments.tpf.TPFRequestParserForJenaBackends; + +import java.io.File; + +/** + * Experimental Jena TDB-backed data source of Basic Linked Data Fragments. + * + * @author Bart Hanssens + * @author Olaf Hartig + */ +public class JenaTDBDataSource extends DataSourceBase { + + /** + * The request processor + * + */ + protected final JenaTDBBasedRequestProcessorForTPFs requestProcessor; + + @Override + public IFragmentRequestParser getRequestParser() + { + return TPFRequestParserForJenaBackends.getInstance(); + } + + @Override + public IFragmentRequestProcessor getRequestProcessor() + { + return requestProcessor; + } + + /** + * Constructor + * + * @param title + * @param description + * @param tdbdir directory used for TDB backing + */ + public JenaTDBDataSource(String title, String description, File tdbdir) { + super(title, description); + requestProcessor = new JenaTDBBasedRequestProcessorForTPFs( tdbdir ); + } +} diff --git a/api/src/main/java/org/linkeddatafragments/datasource/tdb/JenaTDBDataSourceType.java b/api/src/main/java/org/linkeddatafragments/datasource/tdb/JenaTDBDataSourceType.java new file mode 100644 index 000000000..bbebca71f --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/datasource/tdb/JenaTDBDataSourceType.java @@ -0,0 +1,34 @@ +package org.linkeddatafragments.datasource.tdb; + +import com.google.gson.JsonObject; +import org.linkeddatafragments.datasource.IDataSource; +import org.linkeddatafragments.datasource.IDataSourceType; +import org.linkeddatafragments.exceptions.DataSourceCreationException; + +import java.io.File; + +/** + * The type of Triple Pattern Fragment data sources that are backed by + * a Jena TDB instance. + * + * @author Olaf Hartig + */ +public class JenaTDBDataSourceType implements IDataSourceType +{ + @Override + public IDataSource createDataSource( final String title, + final String description, + final JsonObject settings ) + throws DataSourceCreationException + { + final String dname = settings.getAsJsonPrimitive("directory").getAsString(); + final File dir = new File( dname ); + + try { + return new JenaTDBDataSource(title, description, dir); + } catch (Exception ex) { + throw new DataSourceCreationException(ex); + } + } + +} diff --git a/api/src/main/java/org/linkeddatafragments/exceptions/DataSourceCreationException.java b/api/src/main/java/org/linkeddatafragments/exceptions/DataSourceCreationException.java new file mode 100644 index 000000000..ea4924ecf --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/exceptions/DataSourceCreationException.java @@ -0,0 +1,25 @@ +package org.linkeddatafragments.exceptions; + +/** + * + * @author Miel Vander Sande + */ +public class DataSourceCreationException extends DataSourceException { + + /** + * + * @param cause + */ + public DataSourceCreationException(Throwable cause) { + super(cause); + } + + /** + * + * @param datasourceName + * @param message + */ + public DataSourceCreationException(String datasourceName, String message) { + super(datasourceName, "Could not create DataSource - " + message); + } +} diff --git a/api/src/main/java/org/linkeddatafragments/exceptions/DataSourceException.java b/api/src/main/java/org/linkeddatafragments/exceptions/DataSourceException.java new file mode 100644 index 000000000..2f5973de7 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/exceptions/DataSourceException.java @@ -0,0 +1,37 @@ +package org.linkeddatafragments.exceptions; + +import org.linkeddatafragments.datasource.IDataSource; + +/** + * + * @author Miel Vander Sande + */ +abstract public class DataSourceException extends Exception { + + /** + * + * @param cause + */ + public DataSourceException(Throwable cause) { + super(cause); + } + + /** + * + * @param datasourceName + * @param message + */ + public DataSourceException(String datasourceName, String message) { + super("Error for datasource '" + datasourceName + "': " + message); + } + + /** + * + * @param datasource + * @param message + */ + public DataSourceException(IDataSource datasource, String message) { + this(datasource.getTitle(), message); + } + +} diff --git a/api/src/main/java/org/linkeddatafragments/exceptions/DataSourceNotFoundException.java b/api/src/main/java/org/linkeddatafragments/exceptions/DataSourceNotFoundException.java new file mode 100644 index 000000000..08dca003c --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/exceptions/DataSourceNotFoundException.java @@ -0,0 +1,16 @@ +package org.linkeddatafragments.exceptions; + +/** + * + * @author Miel Vander Sande + */ +public class DataSourceNotFoundException extends DataSourceException { + + /** + * + * @param dataSourceName + */ + public DataSourceNotFoundException(String dataSourceName) { + super(dataSourceName, "Datasource not found."); + } +} diff --git a/api/src/main/java/org/linkeddatafragments/exceptions/NoRegisteredMimeTypesException.java b/api/src/main/java/org/linkeddatafragments/exceptions/NoRegisteredMimeTypesException.java new file mode 100644 index 000000000..ad278a64c --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/exceptions/NoRegisteredMimeTypesException.java @@ -0,0 +1,17 @@ +package org.linkeddatafragments.exceptions; + +/** + * Exception thrown when no mimeTypes are known to the system + * + * @author Miel Vander Sande + */ +public class NoRegisteredMimeTypesException extends Exception { + + /** + * Constructs the exception + */ + public NoRegisteredMimeTypesException() { + super("List of supported mimeTypes is empty."); + } + +} diff --git a/api/src/main/java/org/linkeddatafragments/exceptions/UnknownDataSourceTypeException.java b/api/src/main/java/org/linkeddatafragments/exceptions/UnknownDataSourceTypeException.java new file mode 100644 index 000000000..1b631a09d --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/exceptions/UnknownDataSourceTypeException.java @@ -0,0 +1,16 @@ +package org.linkeddatafragments.exceptions; + +/** + * + * @author Miel Vander Sande + */ +public class UnknownDataSourceTypeException extends DataSourceCreationException { + + /** + * + * @param type + */ + public UnknownDataSourceTypeException(String type) { + super("", "Type " + type + " does not exist."); + } +} diff --git a/api/src/main/java/org/linkeddatafragments/fragments/FragmentRequestParserBase.java b/api/src/main/java/org/linkeddatafragments/fragments/FragmentRequestParserBase.java new file mode 100644 index 000000000..7d6c8b8c0 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/fragments/FragmentRequestParserBase.java @@ -0,0 +1,141 @@ +package org.linkeddatafragments.fragments; + +import org.linkeddatafragments.config.ConfigReader; + +import javax.servlet.http.HttpServletRequest; + +/** + * Base class for implementations of {@link IFragmentRequestParser}. + * + * @author Olaf Hartig + */ +abstract public class FragmentRequestParserBase implements IFragmentRequestParser +{ + @Override + final public ILinkedDataFragmentRequest parseIntoFragmentRequest( + final HttpServletRequest httpRequest, + final ConfigReader config ) + throws IllegalArgumentException + { + return getWorker( httpRequest, config ).createFragmentRequest(); + } + + /** + * + * @param httpRequest + * @param config + * @return + * @throws IllegalArgumentException + */ + abstract protected Worker getWorker( final HttpServletRequest httpRequest, + final ConfigReader config ) + throws IllegalArgumentException; + + /** + * + */ + abstract static protected class Worker + { + + /** + * + */ + public final HttpServletRequest request; + + /** + * + */ + public final ConfigReader config; + + /** + * + */ + public final boolean pageNumberWasRequested; + + /** + * + */ + public final long pageNumber; + + /** + * + * @param request + * @param config + */ + public Worker( final HttpServletRequest request, + final ConfigReader config ) + { + this.request = request; + this.config = config; + + final String givenPageNumber = request.getParameter( + ILinkedDataFragmentRequest.PARAMETERNAME_PAGE ); + if ( givenPageNumber != null ) { + long pageNumber; + try { + pageNumber = Long.parseLong( givenPageNumber ); + } catch (NumberFormatException ex) { + pageNumber = 1L; + } + this.pageNumber = ( pageNumber > 0 ) ? pageNumber : 1L; + this.pageNumberWasRequested = true; + } + else { + this.pageNumber = 1L; + this.pageNumberWasRequested = false; + } + } + + /** + * + * @return + * @throws IllegalArgumentException + */ + abstract public ILinkedDataFragmentRequest createFragmentRequest() + throws IllegalArgumentException; + + /** + * + * @return + */ + public String getFragmentURL() { + final String datasetURL = getDatasetURL(); + final String query = request.getQueryString(); + return query == null ? datasetURL : (datasetURL + "?" + query); + } + + /** + * + * @return + */ + public String getDatasetURL() { + return extractBaseURL( request, config ) + request.getRequestURI(); + } + + } // end of class Worker + + + // ----- HELPERS --------- + + /** + * + * @param request + * @param config + * @return + */ + + public static String extractBaseURL( final HttpServletRequest request, + final ConfigReader config ) { + if (config.getBaseURL() != null) { + return config.getBaseURL(); + } else if ((request.getServerPort() == 80) + || (request.getServerPort() == 443)) { + return request.getScheme() + "://" + + request.getServerName(); + } else { + return request.getScheme() + "://" + + request.getServerName() + ":" + request.getServerPort(); + } + } + +} diff --git a/api/src/main/java/org/linkeddatafragments/fragments/IFragmentRequestParser.java b/api/src/main/java/org/linkeddatafragments/fragments/IFragmentRequestParser.java new file mode 100644 index 000000000..113621076 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/fragments/IFragmentRequestParser.java @@ -0,0 +1,29 @@ +package org.linkeddatafragments.fragments; + +import org.linkeddatafragments.config.ConfigReader; + +import javax.servlet.http.HttpServletRequest; + +/** + * Parses HTTP requests into specific {@link ILinkedDataFragmentRequest}s. + * + * @author Olaf Hartig + */ +public interface IFragmentRequestParser +{ + /** + * Parses the given HTTP request into a specific + * {@link ILinkedDataFragmentRequest}. + * + * @param httpRequest + * @param config + * @return + * @throws IllegalArgumentException + * If the given HTTP request cannot be interpreted (perhaps due to + * missing request parameters). + */ + ILinkedDataFragmentRequest parseIntoFragmentRequest( + final HttpServletRequest httpRequest, + final ConfigReader config) + throws IllegalArgumentException; +} diff --git a/api/src/main/java/org/linkeddatafragments/fragments/ILinkedDataFragment.java b/api/src/main/java/org/linkeddatafragments/fragments/ILinkedDataFragment.java new file mode 100644 index 000000000..5fa14aa98 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/fragments/ILinkedDataFragment.java @@ -0,0 +1,79 @@ +package org.linkeddatafragments.fragments; + +import org.apache.jena.rdf.model.StmtIterator; + +import java.io.Closeable; + +/** + * Represents any possible Linked Data Fragment. + * + * @author Olaf Hartig + */ +public interface ILinkedDataFragment extends Closeable +{ + /** + * Returns an iterator over the RDF data of this fragment (possibly only + * partial if the data is paged, as indicated by {@link #isPageOnly()}). + * @return + */ + StmtIterator getTriples(); + + /** + * Returns true if {@link #getTriples()} returns a page of data only. + * In this case, {@link #getPageNumber()} can be used to obtain the + * corresponding page number. + * @return + */ + boolean isPageOnly(); + + /** + * Returns the number of the page of data returned by {@link #getTriples()} + * if the data is paged (that is, if {@link #isPageOnly()} returns true). + * + * If the data is not paged, this method throws an exception. + * + * @return + * @throws UnsupportedOperationException + * If the data of this fragment is not paged. + */ + long getPageNumber() throws UnsupportedOperationException; + + /** + * Returns true if {@link #getTriples()} returns a page of data only and + * this is the last page of the fragment. + * + * If the data is not paged (i.e., if {@link #isPageOnly()} returns false), + * this method throws an exception. + * + * @return + * @throws UnsupportedOperationException + * If the data of this fragment is not paged. + */ + boolean isLastPage() throws UnsupportedOperationException; + + /** + * Returns the maximum number of triples per page if {@link #getTriples()} + * returns a page of data only (that is, if {@link #isPageOnly()} returns + * true). + * + * If the data is not paged, this method throws an exception. + * + * @return + * @throws UnsupportedOperationException + * If the data of this fragment is not paged. + */ + long getMaxPageSize() throws UnsupportedOperationException; + + /** + * Returns an iterator over the metadata of this fragment. + * @return + */ + StmtIterator getMetadata(); + + /** + * Returns an iterator over an RDF description of the controls associated + * with this fragment. + * @return + */ + StmtIterator getControls(); +} diff --git a/api/src/main/java/org/linkeddatafragments/fragments/ILinkedDataFragmentRequest.java b/api/src/main/java/org/linkeddatafragments/fragments/ILinkedDataFragmentRequest.java new file mode 100644 index 000000000..ed9aa5df5 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/fragments/ILinkedDataFragmentRequest.java @@ -0,0 +1,48 @@ +package org.linkeddatafragments.fragments; + +/** + * Basis for representing a request of some type of Linked Data Fragment (LDF). + * + * @author Olaf Hartig + */ +public interface ILinkedDataFragmentRequest +{ + + /** + * + */ + public final static long TRIPLESPERPAGE = 100L; + + /** + * + */ + public final static String PARAMETERNAME_PAGE = "page"; + + /** + * Returns the URL of the requested LDF. + * @return + */ + String getFragmentURL(); + + /** + * Returns the URL of the dataset to which the requested LDF belongs. + * @return + */ + String getDatasetURL(); + + /** + * Returns true if the request is for a specific page of the requested + * fragment. In this case, {@link #getPageNumber()} can be used to obtain + * the requested page number. + * @return + */ + boolean isPageRequest(); + + /** + * Returns the number of the page requested for the LDF; if this is not a + * page-based request (that is, if {@link #isPageRequest()} returns true), + * then this method returns 1. + * @return + */ + long getPageNumber(); +} diff --git a/api/src/main/java/org/linkeddatafragments/fragments/LinkedDataFragmentBase.java b/api/src/main/java/org/linkeddatafragments/fragments/LinkedDataFragmentBase.java new file mode 100644 index 000000000..9ac16bd9b --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/fragments/LinkedDataFragmentBase.java @@ -0,0 +1,189 @@ +package org.linkeddatafragments.fragments; + +import org.apache.http.client.utils.URIBuilder; +import org.apache.jena.rdf.model.Literal; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.StmtIterator; +import org.linkeddatafragments.util.CommonResources; + +import java.net.URISyntaxException; + + +/** + * Base class of any implementation of {@link ILinkedDataFragment} that uses + * paging. + * + * @author Olaf Hartig + */ +public abstract class LinkedDataFragmentBase implements ILinkedDataFragment +{ + + /** + * + */ + public final String fragmentURL; + + /** + * + */ + public final String datasetURL; + + /** + * + */ + public final long pageNumber; + + /** + * + */ + public final boolean isLastPage; + + /** + * + * @param fragmentURL + * @param datasetURL + * @param pageNumber + * @param isLastPage + */ + protected LinkedDataFragmentBase( final String fragmentURL, + final String datasetURL, + final long pageNumber, + final boolean isLastPage ) + { + this.fragmentURL = fragmentURL; + this.datasetURL = datasetURL; + this.pageNumber = pageNumber; + this.isLastPage = isLastPage; + } + + /** + * Does nothing. May be overridden by subclasses that hold some objects + * that need to be closed (such as iterators from the underlying data + * source). + */ + @Override + public void close() {} + + @Override + public boolean isPageOnly() { + return true; + } + + @Override + public long getPageNumber() { + return pageNumber; + } + + @Override + public boolean isLastPage() { + return isLastPage; + } + + @Override + public long getMaxPageSize() { + return ILinkedDataFragmentRequest.TRIPLESPERPAGE; + } + + /** + * This implementation uses {@link #addMetadata(Model)}, which should be + * overridden in subclasses (instead of overriding this method). + * @return + */ + @Override + public StmtIterator getMetadata() + { + final Model output = ModelFactory.createDefaultModel(); + addMetadata( output ); + return output.listStatements(); + } + + /** + * This implementation uses {@link #addControls(Model)}, which should be + * overridden in subclasses (instead of overriding this method). + * @return + */ + @Override + public StmtIterator getControls() + { + final Model output = ModelFactory.createDefaultModel(); + addControls( output ); + return output.listStatements(); + } + + /** + * Adds some basic metadata to the given RDF model. + * This method may be overridden in subclasses. + * @param model + */ + public void addMetadata( final Model model ) + { + final Resource datasetId = model.createResource( getDatasetURI() ); + final Resource fragmentId = model.createResource( fragmentURL ); + + datasetId.addProperty( CommonResources.RDF_TYPE, CommonResources.VOID_DATASET ); + datasetId.addProperty( CommonResources.RDF_TYPE, CommonResources.HYDRA_COLLECTION ); + datasetId.addProperty( CommonResources.VOID_SUBSET, fragmentId ); + + Literal itemsPerPage = model.createTypedLiteral(this.getMaxPageSize()); + datasetId.addProperty( CommonResources.HYDRA_ITEMSPERPAGE, itemsPerPage); + + fragmentId.addProperty( CommonResources.RDF_TYPE, CommonResources.HYDRA_COLLECTION ); + fragmentId.addProperty( CommonResources.RDF_TYPE, CommonResources.HYDRA_PAGEDCOLLECTION ); + } + + /** + * Adds an RDF description of page links to the given RDF model. + * This method may be overridden in subclasses. + * @param model + */ + public void addControls( final Model model ) + { + final URIBuilder pagedURL; + try { + pagedURL = new URIBuilder( fragmentURL ); + } + catch ( URISyntaxException e ) { + throw new IllegalArgumentException( e ); + } + + final Resource fragmentId = model.createResource( fragmentURL ); + + final Resource firstPageId = + model.createResource( + pagedURL.setParameter(ILinkedDataFragmentRequest.PARAMETERNAME_PAGE, + "1").toString() ); + + fragmentId.addProperty( CommonResources.HYDRA_FIRSTPAGE, firstPageId ); + + if ( pageNumber > 1) { + final String prevPageNumber = Long.toString( pageNumber - 1 ); + final Resource prevPageId = + model.createResource( + pagedURL.setParameter(ILinkedDataFragmentRequest.PARAMETERNAME_PAGE, + prevPageNumber).toString() ); + + fragmentId.addProperty( CommonResources.HYDRA_PREVIOUSPAGE, prevPageId ); + } + + if ( ! isLastPage ) { + final String nextPageNumber = Long.toString( pageNumber + 1 ); + final Resource nextPageId = + model.createResource( + pagedURL.setParameter(ILinkedDataFragmentRequest.PARAMETERNAME_PAGE, + nextPageNumber).toString() ); + + fragmentId.addProperty( CommonResources.HYDRA_NEXTPAGE, nextPageId ); + } + } + + /** + * + * @return + */ + public String getDatasetURI() { + return datasetURL + "#dataset"; + } + +} diff --git a/api/src/main/java/org/linkeddatafragments/fragments/LinkedDataFragmentRequestBase.java b/api/src/main/java/org/linkeddatafragments/fragments/LinkedDataFragmentRequestBase.java new file mode 100644 index 000000000..c53ae5a45 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/fragments/LinkedDataFragmentRequestBase.java @@ -0,0 +1,81 @@ +package org.linkeddatafragments.fragments; + +/** + * Base class for implementations of {@link ILinkedDataFragmentRequest}. + * + * @author Olaf Hartig + */ +public abstract class LinkedDataFragmentRequestBase + implements ILinkedDataFragmentRequest +{ + + /** + * + */ + public final String fragmentURL; + + /** + * + */ + public final String datasetURL; + + /** + * + */ + public final boolean pageNumberWasRequested; + + /** + * + */ + public final long pageNumber; + + /** + * + * @param fragmentURL + * @param datasetURL + * @param pageNumberWasRequested + * @param pageNumber + */ + public LinkedDataFragmentRequestBase( final String fragmentURL, + final String datasetURL, + final boolean pageNumberWasRequested, + final long pageNumber ) + { + this.fragmentURL = fragmentURL; + this.datasetURL = datasetURL; + this.pageNumberWasRequested = pageNumberWasRequested; + this.pageNumber = (pageNumberWasRequested) ? pageNumber : 1L; + } + + @Override + public String getFragmentURL() { + return fragmentURL; + } + + @Override + public String getDatasetURL() { + return datasetURL; + } + + @Override + public boolean isPageRequest() { + return pageNumberWasRequested; + } + + @Override + public long getPageNumber() { + return pageNumber; + } + + @Override + public String toString() + { + return "LinkedDataFragmentRequest(" + + "class: " + getClass().getName() + + ", fragmentURL: " + fragmentURL + + ", isPageRequest: " + pageNumberWasRequested + + ", pageNumber: " + pageNumber + + ")"; + } + +} diff --git a/api/src/main/java/org/linkeddatafragments/fragments/tpf/ITriplePatternElement.java b/api/src/main/java/org/linkeddatafragments/fragments/tpf/ITriplePatternElement.java new file mode 100644 index 000000000..6c6d928aa --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/fragments/tpf/ITriplePatternElement.java @@ -0,0 +1,89 @@ +package org.linkeddatafragments.fragments.tpf; + +/** + * Represents an element of a triple pattern (i.e., subject, predicate, object). + * + * @param type for representing constants in triple patterns + * (i.e., URIs and literals) + * @param type for representing named variables in triple patterns + * @param type for representing anonymous variables in triple + * patterns (i.e., variables denoted by a blank node) + * + * @author Olaf Hartig + */ +public interface ITriplePatternElement +{ + /** + * Returns true if this element is a variable (specific or unspecified). + * @return + */ + boolean isVariable(); + + /** + * Returns true if this element is a specific variable, and false if either + * it is not a variable (but a URI or literal) or it is some variable that + * is not specified. The latter (unspecified variables) is possible because + * when a client requests a triple pattern fragment, it may omit triple + * pattern related parameters. + * @return + */ + boolean isSpecificVariable(); + + /** + * Returns true if this element is a specific variable that has a name + * (i.e., it is denoted by a string that begins with a question mark), + * and false if either it is not a specific variable or it is a specific + * variable that is denoted by a blank node. + * + * If this element is a specific variable that has a name (that is, this + * method returns true), the named variable can be obtained by the method + * {@link #asNamedVariable()}. + * @return + */ + boolean isNamedVariable(); + + /** + * Returns a representation of this element as a named variable (assuming + * it is a specific variable that has a name). + * + * @return + * @throws UnsupportedOperationException + * If this element is not a specific variable that has a name + * (i.e., if {@link #isNamedVariable()} returns false). + */ + NamedVarType asNamedVariable() throws UnsupportedOperationException; + + /** + * Returns true if this element is a specific variable that does not have + * a name (i.e., it is denoted by a blank node), and false if either it is + * not a specific variable or it is a specific variable that has a name. + * + * If this element is a specific variable denoted by a blank node (that is, + * this method returns true), the blank node can be obtained by the method + * {@link #asAnonymousVariable()}. + * @return + */ + boolean isAnonymousVariable(); + + /** + * Returns a representation of this element as a blank node (assuming + * it is a specific, but non-named variable). + * + * @return + * @throws UnsupportedOperationException + * If this element is not a specific anonymous variable (i.e., + * if {@link #isAnonymousVariable()} returns false). + */ + AnonVarType asAnonymousVariable() throws UnsupportedOperationException; + + /** + * Returns a representation of this element as a constant RDF term (i.e., + * a URI or a literal). + * + * @return + * @throws UnsupportedOperationException + * If this element is not a constant RDF term but a variable + * (i.e., if {@link #isVariable()} returns true). + */ + ConstantTermType asConstantTerm() throws UnsupportedOperationException; +} diff --git a/api/src/main/java/org/linkeddatafragments/fragments/tpf/ITriplePatternFragment.java b/api/src/main/java/org/linkeddatafragments/fragments/tpf/ITriplePatternFragment.java new file mode 100644 index 000000000..9f51b34a7 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/fragments/tpf/ITriplePatternFragment.java @@ -0,0 +1,15 @@ +package org.linkeddatafragments.fragments.tpf; + +import org.linkeddatafragments.fragments.ILinkedDataFragment; + +/** + * A Triple Pattern Fragment. + * @author Ruben Verborgh + */ +public interface ITriplePatternFragment extends ILinkedDataFragment { + /** + * Gets the total number of triples in the fragment (can be an estimate). + * @return the total number of triples + */ + public long getTotalSize(); +} diff --git a/api/src/main/java/org/linkeddatafragments/fragments/tpf/ITriplePatternFragmentRequest.java b/api/src/main/java/org/linkeddatafragments/fragments/tpf/ITriplePatternFragmentRequest.java new file mode 100644 index 000000000..ae6d15107 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/fragments/tpf/ITriplePatternFragmentRequest.java @@ -0,0 +1,52 @@ +package org.linkeddatafragments.fragments.tpf; + +import org.linkeddatafragments.fragments.ILinkedDataFragmentRequest; + +/** + * Represents a request of a Triple Pattern Fragment (TPF). + * + * @param type for representing constants in triple patterns + * (i.e., URIs and literals) + * @param type for representing named variables in triple patterns + * @param type for representing anonymous variables in triple + * patterns (i.e., variables denoted by a blank node) + * + * @author Olaf Hartig + */ +public interface ITriplePatternFragmentRequest + extends ILinkedDataFragmentRequest +{ + + /** + * + */ + public final static String PARAMETERNAME_SUBJ = "subject"; + + /** + * + */ + public final static String PARAMETERNAME_PRED = "predicate"; + + /** + * + */ + public final static String PARAMETERNAME_OBJ = "object"; + + /** + * Returns the subject position of the requested triple pattern. + * @return + */ + ITriplePatternElement getSubject(); + + /** + * Returns the predicate position of the requested triple pattern. + * @return + */ + ITriplePatternElement getPredicate(); + + /** + * Returns the object position of the requested triple pattern. + * @return + */ + ITriplePatternElement getObject(); +} diff --git a/api/src/main/java/org/linkeddatafragments/fragments/tpf/TPFRequestParser.java b/api/src/main/java/org/linkeddatafragments/fragments/tpf/TPFRequestParser.java new file mode 100644 index 000000000..55270f251 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/fragments/tpf/TPFRequestParser.java @@ -0,0 +1,127 @@ +package org.linkeddatafragments.fragments.tpf; + +import org.linkeddatafragments.config.ConfigReader; +import org.linkeddatafragments.fragments.FragmentRequestParserBase; +import org.linkeddatafragments.fragments.IFragmentRequestParser; +import org.linkeddatafragments.fragments.ILinkedDataFragmentRequest; +import org.linkeddatafragments.util.TriplePatternElementParser; + +import javax.servlet.http.HttpServletRequest; + +/** + * An {@link IFragmentRequestParser} for {@link ITriplePatternFragmentRequest}s. + * + * @param + * @param + * + * @author Olaf Hartig + * @param + */ +public class TPFRequestParser + extends FragmentRequestParserBase +{ + public final TriplePatternElementParser elmtParser; + + /** + * + * @param elmtParser + */ + public TPFRequestParser( + final TriplePatternElementParser elmtParser ) + { + this.elmtParser = elmtParser; + } + + /** + * + * @param httpRequest + * @param config + * @return + * @throws IllegalArgumentException + */ + @Override + protected Worker getWorker( final HttpServletRequest httpRequest, + final ConfigReader config ) + throws IllegalArgumentException + { + return new Worker( httpRequest, config ); + } + + /** + * + */ + protected class Worker extends FragmentRequestParserBase.Worker + { + + /** + * + * @param request + * @param config + */ + public Worker( final HttpServletRequest request, + final ConfigReader config ) + { + super( request, config ); + } + + /** + * + * @return + * @throws IllegalArgumentException + */ + @Override + public ILinkedDataFragmentRequest createFragmentRequest() + throws IllegalArgumentException + { + return new TriplePatternFragmentRequestImpl( + getFragmentURL(), + getDatasetURL(), + pageNumberWasRequested, + pageNumber, + getSubject(), + getPredicate(), + getObject() ); + } + + /** + * + * @return + */ + public ITriplePatternElement getSubject() { + return getParameterAsTriplePatternElement( + ITriplePatternFragmentRequest.PARAMETERNAME_SUBJ ); + } + + /** + * + * @return + */ + public ITriplePatternElement getPredicate() { + return getParameterAsTriplePatternElement( + ITriplePatternFragmentRequest.PARAMETERNAME_PRED ); + } + + /** + * + * @return + */ + public ITriplePatternElement getObject() { + return getParameterAsTriplePatternElement( + ITriplePatternFragmentRequest.PARAMETERNAME_OBJ ); + } + + /** + * + * @param paramName + * @return + */ + public ITriplePatternElement + getParameterAsTriplePatternElement( final String paramName ) + { + final String parameter = request.getParameter( paramName ); + return elmtParser.parseIntoTriplePatternElement( parameter ); + } + + } // end of class Worker + +} diff --git a/api/src/main/java/org/linkeddatafragments/fragments/tpf/TPFRequestParserForJenaBackends.java b/api/src/main/java/org/linkeddatafragments/fragments/tpf/TPFRequestParserForJenaBackends.java new file mode 100644 index 000000000..cbd38b9a3 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/fragments/tpf/TPFRequestParserForJenaBackends.java @@ -0,0 +1,35 @@ +package org.linkeddatafragments.fragments.tpf; + +import org.apache.jena.rdf.model.RDFNode; +import org.linkeddatafragments.util.TriplePatternElementParserForJena; + +/** + * An {@link TPFRequestParser} for Jena-based backends. + * + * @author Olaf Hartig + */ +public class TPFRequestParserForJenaBackends + extends TPFRequestParser +{ + private static TPFRequestParserForJenaBackends instance = null; + + /** + * + * @return + */ + public static TPFRequestParserForJenaBackends getInstance() + { + if ( instance == null ) { + instance = new TPFRequestParserForJenaBackends(); + } + return instance; + } + + /** + * + */ + protected TPFRequestParserForJenaBackends() + { + super( TriplePatternElementParserForJena.getInstance() ); + } +} diff --git a/api/src/main/java/org/linkeddatafragments/fragments/tpf/TriplePatternElementFactory.java b/api/src/main/java/org/linkeddatafragments/fragments/tpf/TriplePatternElementFactory.java new file mode 100644 index 000000000..3537bdf59 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/fragments/tpf/TriplePatternElementFactory.java @@ -0,0 +1,214 @@ +package org.linkeddatafragments.fragments.tpf; + +/** + * A factory for {@link ITriplePatternElement}s. + * + * @param + * type for representing constants in triple patterns (i.e., URIs and + * literals) + * @param + * type for representing named variables in triple patterns + * @param + * type for representing anonymous variables in triple patterns (i.e., + * variables denoted by a blank node) + * + * @author Olaf Hartig + */ +public class TriplePatternElementFactory +{ + + /** + * + * @return + */ + public ITriplePatternElement createUnspecifiedVariable() + { + return new UnspecifiedVariable(); + } + + /** + * + * @param v + * @return + */ + public ITriplePatternElement createNamedVariable(final NVT v ) + { + return new NamedVariable( v ); + } + + /** + * + * @param bnode + * @return + */ + public ITriplePatternElement createAnonymousVariable( + final AVT bnode ) + { + return new AnonymousVariable( bnode ); + } + + /** + * + * @param term + * @return + */ + public ITriplePatternElement createConstantRDFTerm( + final CTT term ) + { + return new ConstantRDFTerm( term ); + } + + /** + * + * @param + * @param + * @param + */ + static abstract public class Variable + implements ITriplePatternElement + { + @Override + public boolean isVariable() { return true; } + @Override + public CTT asConstantTerm() { throw new UnsupportedOperationException(); } + } + + /** + * + * @param + * @param + * @param + */ + static public class UnspecifiedVariable + extends Variable + { + @Override + public boolean isSpecificVariable() { return false; } + @Override + public boolean isNamedVariable() { return false; } + @Override + public NVT asNamedVariable() { throw new UnsupportedOperationException(); } + @Override + public boolean isAnonymousVariable() { return false; } + @Override + public AVT asAnonymousVariable() { throw new UnsupportedOperationException(); } + @Override + public String toString() { return "UnspecifiedVariable"; } + } + + /** + * + * @param + * @param + * @param + */ + static abstract public class SpecificVariable + extends Variable + { + @Override + public boolean isSpecificVariable() { return true; } + } + + /** + * + * @param + * @param + * @param + */ + static public class NamedVariable + extends SpecificVariable + { + + /** + * + */ + protected final NVT v; + + /** + * + * @param variable + */ + public NamedVariable( final NVT variable ) { v = variable; } + @Override + public boolean isNamedVariable() { return true; } + @Override + public NVT asNamedVariable() { return v; } + @Override + public boolean isAnonymousVariable() { return false; } + @Override + public AVT asAnonymousVariable() { throw new UnsupportedOperationException(); } + @Override + public String toString() { return "NamedVariable(" + v.toString() + ")"; } + } + + /** + * + * @param + * @param + * @param + */ + static public class AnonymousVariable + extends SpecificVariable + { + + /** + * + */ + protected final AVT bn; + + /** + * + * @param bnode + */ + public AnonymousVariable( final AVT bnode ) { bn = bnode; } + @Override + public boolean isNamedVariable() { return false; } + @Override + public NVT asNamedVariable() { throw new UnsupportedOperationException(); } + @Override + public boolean isAnonymousVariable() { return true; } + @Override + public AVT asAnonymousVariable() { return bn; } + @Override + public String toString() { return "AnonymousVariable(" + bn.toString() + ")"; } + } + + /** + * + * @param + * @param + * @param + */ + static public class ConstantRDFTerm + implements ITriplePatternElement + { + + /** + * + */ + protected final CTT t; + + /** + * + * @param term + */ + public ConstantRDFTerm( final CTT term ) { t = term; } + @Override + public boolean isVariable() { return false; } + @Override + public boolean isSpecificVariable() { return false; } + @Override + public boolean isNamedVariable() { return false; } + @Override + public NVT asNamedVariable() { throw new UnsupportedOperationException(); } + @Override + public boolean isAnonymousVariable() { return false; } + @Override + public AVT asAnonymousVariable() { throw new UnsupportedOperationException(); } + @Override + public CTT asConstantTerm() { return t; } + @Override + public String toString() { return "ConstantRDFTerm(" + t.toString() + ")(type: " + t.getClass().getSimpleName() + ")"; } + } + +} diff --git a/api/src/main/java/org/linkeddatafragments/fragments/tpf/TriplePatternFragmentBase.java b/api/src/main/java/org/linkeddatafragments/fragments/tpf/TriplePatternFragmentBase.java new file mode 100644 index 000000000..132e8ed98 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/fragments/tpf/TriplePatternFragmentBase.java @@ -0,0 +1,164 @@ +package org.linkeddatafragments.fragments.tpf; + +import org.apache.jena.datatypes.xsd.XSDDatatype; +import org.apache.jena.rdf.model.Literal; +import org.apache.jena.rdf.model.Model; +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.util.iterator.NiceIterator; +import org.linkeddatafragments.fragments.LinkedDataFragmentBase; +import org.linkeddatafragments.util.CommonResources; + +import java.util.NoSuchElementException; + + +/** + * Base class for implementations of {@link ITriplePatternFragment}. + * + * @author Ruben Verborgh + * @author Olaf Hartig + */ +abstract public class TriplePatternFragmentBase extends LinkedDataFragmentBase + implements ITriplePatternFragment +{ + private final long totalSize; + + /** + * Creates an empty Triple Pattern Fragment. + * @param fragmentURL + * @param datasetURL + */ + public TriplePatternFragmentBase( final String fragmentURL, + final String datasetURL ) { + this( 0L, fragmentURL, datasetURL, 1, true ); + } + + /** + * Creates an empty Triple Pattern Fragment page. + * @param fragmentURL + * @param isLastPage + * @param datasetURL + * @param pageNumber + */ + public TriplePatternFragmentBase( final String fragmentURL, + final String datasetURL, + final long pageNumber, + final boolean isLastPage ) { + this( 0L, fragmentURL, datasetURL, pageNumber, isLastPage ); + } + + /** + * Creates a new Triple Pattern Fragment. + * @param totalSize the total size + * @param fragmentURL + * @param datasetURL + * @param pageNumber + * @param isLastPage + */ + public TriplePatternFragmentBase( long totalSize, + final String fragmentURL, + final String datasetURL, + final long pageNumber, + final boolean isLastPage ) { + super( fragmentURL, datasetURL, pageNumber, isLastPage ); + this.totalSize = totalSize < 0L ? 0L : totalSize; + } + + @Override + public StmtIterator getTriples() { + if ( totalSize == 0L ) + return emptyStmtIterator; + else + return getNonEmptyStmtIterator(); + } + + /** + * + * @return + */ + abstract protected StmtIterator getNonEmptyStmtIterator(); + + @Override + public long getTotalSize() { + return totalSize; + } + + @Override + public void addMetadata( final Model model ) + { + super.addMetadata( model ); + + final Resource fragmentId = model.createResource( fragmentURL ); + + final Literal totalTyped = model.createTypedLiteral( totalSize, + XSDDatatype.XSDinteger ); + final Literal limitTyped = model.createTypedLiteral( getMaxPageSize(), + XSDDatatype.XSDinteger ); + + fragmentId.addLiteral( CommonResources.VOID_TRIPLES, totalTyped ); + fragmentId.addLiteral( CommonResources.HYDRA_TOTALITEMS, totalTyped ); + fragmentId.addLiteral( CommonResources.HYDRA_ITEMSPERPAGE, limitTyped ); + } + + @Override + public void addControls( final Model model ) + { + super.addControls( model ); + + final Resource datasetId = model.createResource( getDatasetURI() ); + + final Resource triplePattern = model.createResource(); + final Resource subjectMapping = model.createResource(); + final Resource predicateMapping = model.createResource(); + final Resource objectMapping = model.createResource(); + + datasetId.addProperty( CommonResources.HYDRA_SEARCH, triplePattern ); + + triplePattern.addProperty( CommonResources.HYDRA_TEMPLATE, getTemplate() ); + triplePattern.addProperty( CommonResources.HYDRA_MAPPING, subjectMapping ); + triplePattern.addProperty( CommonResources.HYDRA_MAPPING, predicateMapping ); + triplePattern.addProperty( CommonResources.HYDRA_MAPPING, objectMapping ); + + subjectMapping.addProperty( CommonResources.HYDRA_VARIABLE, ITriplePatternFragmentRequest.PARAMETERNAME_SUBJ ); + subjectMapping.addProperty( CommonResources.HYDRA_PROPERTY, CommonResources.RDF_SUBJECT ); + + predicateMapping.addProperty( CommonResources.HYDRA_VARIABLE, ITriplePatternFragmentRequest.PARAMETERNAME_PRED ); + predicateMapping.addProperty( CommonResources.HYDRA_PROPERTY, CommonResources.RDF_PREDICATE ); + + objectMapping.addProperty( CommonResources.HYDRA_VARIABLE, ITriplePatternFragmentRequest.PARAMETERNAME_OBJ ); + objectMapping.addProperty( CommonResources.HYDRA_PROPERTY, CommonResources.RDF_OBJECT ); + } + + /** + * + * @return + */ + public String getTemplate() { + return datasetURL + "{?" + + ITriplePatternFragmentRequest.PARAMETERNAME_SUBJ + "," + + ITriplePatternFragmentRequest.PARAMETERNAME_PRED + "," + + ITriplePatternFragmentRequest.PARAMETERNAME_OBJ + "}"; + } + + /** + * + */ + public static final StmtIterator emptyStmtIterator = new EmptyStmtIterator(); + + /** + * + */ + public static class EmptyStmtIterator + extends NiceIterator + implements StmtIterator + { + + /** + * + * @return + */ + public Statement nextStatement() { throw new NoSuchElementException(); } + } + +} diff --git a/api/src/main/java/org/linkeddatafragments/fragments/tpf/TriplePatternFragmentImpl.java b/api/src/main/java/org/linkeddatafragments/fragments/tpf/TriplePatternFragmentImpl.java new file mode 100644 index 000000000..3c78d4b52 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/fragments/tpf/TriplePatternFragmentImpl.java @@ -0,0 +1,72 @@ +package org.linkeddatafragments.fragments.tpf; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.StmtIterator; + + +/** + * Implementation of {@link ITriplePatternFragment}. + * + * @author Olaf Hartig + */ +public class TriplePatternFragmentImpl extends TriplePatternFragmentBase +{ + + /** + * + */ + protected final Model triples; + + /** + * Creates an empty Triple Pattern Fragment. + * @param fragmentURL + * @param datasetURL + */ + public TriplePatternFragmentImpl( final String fragmentURL, + final String datasetURL ) { + this( null, 0L, fragmentURL, datasetURL, 1, true ); + } + + /** + * Creates an empty Triple Pattern Fragment page. + * @param fragmentURL + * @param datasetURL + * @param isLastPage + * @param pageNumber + */ + public TriplePatternFragmentImpl( final String fragmentURL, + final String datasetURL, + final long pageNumber, + final boolean isLastPage ) { + this( null, 0L, fragmentURL, datasetURL, pageNumber, isLastPage ); + } + + /** + * Creates a new Triple Pattern Fragment. + * @param triples the triples (possibly partial) + * @param totalSize the total size + * @param fragmentURL + * @param datasetURL + * @param isLastPage + * @param pageNumber + */ + public TriplePatternFragmentImpl( final Model triples, + long totalSize, + final String fragmentURL, + final String datasetURL, + final long pageNumber, + final boolean isLastPage ) { + super( totalSize, fragmentURL, datasetURL, pageNumber, isLastPage ); + this.triples = triples; + } + + /** + * + * @return + */ + @Override + protected StmtIterator getNonEmptyStmtIterator() { + return triples.listStatements(); + } + +} diff --git a/api/src/main/java/org/linkeddatafragments/fragments/tpf/TriplePatternFragmentRequestImpl.java b/api/src/main/java/org/linkeddatafragments/fragments/tpf/TriplePatternFragmentRequestImpl.java new file mode 100644 index 000000000..59424630b --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/fragments/tpf/TriplePatternFragmentRequestImpl.java @@ -0,0 +1,96 @@ +package org.linkeddatafragments.fragments.tpf; + +import org.linkeddatafragments.fragments.LinkedDataFragmentRequestBase; + +/** + * An implementation of {@link ITriplePatternFragmentRequest}. + * + * @author Olaf Hartig + * @param + * @param + * @param + */ +public class TriplePatternFragmentRequestImpl + extends LinkedDataFragmentRequestBase + implements ITriplePatternFragmentRequest +{ + + /** + * + */ + public final ITriplePatternElement subject; + + /** + * + */ + public final ITriplePatternElement predicate; + + /** + * + */ + public final ITriplePatternElement object; + + /** + * + * @param fragmentURL + * @param datasetURL + * @param pageNumberWasRequested + * @param pageNumber + * @param subject + * @param predicate + * @param object + */ + public TriplePatternFragmentRequestImpl( final String fragmentURL, + final String datasetURL, + final boolean pageNumberWasRequested, + final long pageNumber, + final ITriplePatternElement subject, + final ITriplePatternElement predicate, + final ITriplePatternElement object ) + { + super( fragmentURL, datasetURL, pageNumberWasRequested, pageNumber ); + + if ( subject == null ) + throw new IllegalArgumentException(); + + if ( predicate == null ) + throw new IllegalArgumentException(); + + if ( object == null ) + throw new IllegalArgumentException(); + + this.subject = subject; + this.predicate = predicate; + this.object = object; + } + + @Override + public ITriplePatternElement getSubject() { + return subject; + } + + @Override + public ITriplePatternElement getPredicate() { + return predicate; + } + + @Override + public ITriplePatternElement getObject() { + return object; + } + + @Override + public String toString() + { + return "TriplePatternFragmentRequest(" + + "class: " + getClass().getName() + + ", subject: " + subject.toString() + + ", predicate: " + predicate.toString() + + ", object: " + object.toString() + + ", fragmentURL: " + fragmentURL + + ", isPageRequest: " + pageNumberWasRequested + + ", pageNumber: " + pageNumber + + ")"; + } + +} diff --git a/api/src/main/java/org/linkeddatafragments/servlet/LinkedDataFragmentServlet.java b/api/src/main/java/org/linkeddatafragments/servlet/LinkedDataFragmentServlet.java new file mode 100644 index 000000000..881dd4fd5 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/servlet/LinkedDataFragmentServlet.java @@ -0,0 +1,213 @@ +package org.linkeddatafragments.servlet; + +import com.google.gson.JsonObject; +import org.apache.jena.riot.Lang; +import org.linkeddatafragments.config.ConfigReader; +import org.linkeddatafragments.datasource.DataSourceFactory; +import org.linkeddatafragments.datasource.DataSourceTypesRegistry; +import org.linkeddatafragments.datasource.IDataSource; +import org.linkeddatafragments.datasource.IDataSourceType; +import org.linkeddatafragments.datasource.index.IndexDataSource; +import org.linkeddatafragments.exceptions.DataSourceNotFoundException; +import org.linkeddatafragments.fragments.FragmentRequestParserBase; +import org.linkeddatafragments.fragments.ILinkedDataFragment; +import org.linkeddatafragments.fragments.ILinkedDataFragmentRequest; +import org.linkeddatafragments.util.MIMEParse; +import org.linkeddatafragments.views.ILinkedDataFragmentWriter; +import org.linkeddatafragments.views.LinkedDataFragmentWriterFactory; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map.Entry; + +/** + * Servlet that responds with a Linked Data Fragment. + * + * @author Ruben Verborgh + * @author Bart Hanssens + * @author Olaf Hartig + */ +public class LinkedDataFragmentServlet extends HttpServlet { + + private final static long serialVersionUID = 1L; + + // Parameters + + /** + * + */ + public final static String CFGFILE = "configFile"; + + private ConfigReader config; + private final HashMap dataSources = new HashMap<>(); + private final Collection mimeTypes = new ArrayList<>(); + + private File getConfigFile(ServletConfig config) throws IOException { + String path = config.getServletContext().getRealPath("/"); + if (path == null) { + // this can happen when running standalone + path = System.getProperty("user.dir"); + } + File cfg = new File(path, "config-example.json"); + if (config.getInitParameter(CFGFILE) != null) { + cfg = new File(config.getInitParameter(CFGFILE)); + } + if (!cfg.exists()) { + throw new IOException("Configuration file " + cfg + " not found."); + } + if (!cfg.isFile()) { + throw new IOException("Configuration file " + cfg + " is not a file."); + } + return cfg; + } + + /** + * + * @param servletConfig + * @throws ServletException + */ + @Override + public void init(ServletConfig servletConfig) throws ServletException { + try { + // load the configuration + File configFile = getConfigFile(servletConfig); + config = new ConfigReader(new FileReader(configFile)); + + // register data source types + for ( Entry typeEntry : config.getDataSourceTypes().entrySet() ) { + DataSourceTypesRegistry.register( typeEntry.getKey(), + typeEntry.getValue() ); + } + + // register data sources + for (Entry dataSource : config.getDataSources().entrySet()) { + dataSources.put(dataSource.getKey(), DataSourceFactory.create(dataSource.getValue())); + } + + // register content types + MIMEParse.register("text/html"); + MIMEParse.register(Lang.TTL.getHeaderString()); + MIMEParse.register(Lang.JSONLD.getHeaderString()); + MIMEParse.register(Lang.NTRIPLES.getHeaderString()); + MIMEParse.register(Lang.RDFXML.getHeaderString()); + } catch (Exception e) { + throw new ServletException(e); + } + } + + /** + * + */ + @Override + public void destroy() + { + for ( IDataSource dataSource : dataSources.values() ) { + try { + dataSource.close(); + } + catch( Exception e ) { + // ignore + } + } + } + + /** + * Get the datasource + * + * @param request + * @return + * @throws IOException + */ + private IDataSource getDataSource(HttpServletRequest request) throws DataSourceNotFoundException { + String contextPath = request.getContextPath(); + String requestURI = request.getRequestURI(); + + String path = contextPath == null + ? requestURI + : requestURI.substring(contextPath.length()); + + if (path.equals("/") || path.isEmpty()) { + final String baseURL = FragmentRequestParserBase.extractBaseURL(request, config); + return new IndexDataSource(baseURL, dataSources); + } + + String dataSourceName = path.substring(1); + IDataSource dataSource = dataSources.get(dataSourceName); + if (dataSource == null) { + throw new DataSourceNotFoundException(dataSourceName); + } + return dataSource; + } + + /** + * + * @param request + * @param response + * @throws ServletException + */ + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException { + ILinkedDataFragment fragment = null; + try { + // do conneg + String bestMatch = MIMEParse.bestMatch(request.getHeader("Accept")); + + // set additional response headers + response.setHeader("Server", "Linked Data Fragments Server"); + response.setContentType(bestMatch); + response.setCharacterEncoding("utf-8"); + + // create a writer depending on the best matching mimeType + ILinkedDataFragmentWriter writer = LinkedDataFragmentWriterFactory.create(config.getPrefixes(), dataSources, bestMatch); + + try { + + final IDataSource dataSource = getDataSource( request ); + + final ILinkedDataFragmentRequest ldfRequest = + dataSource.getRequestParser() + .parseIntoFragmentRequest( request, config ); + + fragment = dataSource.getRequestProcessor() + .createRequestedFragment( ldfRequest ); + + writer.writeFragment(response.getOutputStream(), dataSource, fragment, ldfRequest); + + } catch (DataSourceNotFoundException ex) { + try { + response.setStatus(404); + writer.writeNotFound(response.getOutputStream(), request); + } catch (Exception ex1) { + throw new ServletException(ex1); + } + } catch (Exception e) { + response.setStatus(500); + writer.writeError(response.getOutputStream(), e); + } + + } catch (Exception e) { + throw new ServletException(e); + } + finally { + // close the fragment + if ( fragment != null ) { + try { + fragment.close(); + } + catch ( Exception e ) { + // ignore + } + } + } + } + +} diff --git a/api/src/main/java/org/linkeddatafragments/util/CommonResources.java b/api/src/main/java/org/linkeddatafragments/util/CommonResources.java new file mode 100644 index 000000000..d5ca95f76 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/util/CommonResources.java @@ -0,0 +1,139 @@ +package org.linkeddatafragments.util; + +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.ResourceFactory; + +/** + * + * @author mielvandersande + */ +@SuppressWarnings("javadoc") +/** + * All common URIs needed for parsing and serializations + */ +public class CommonResources { + + /** + * + */ + public final static String RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; + + /** + * + */ + public final static Property RDF_TYPE = createProperty(RDF + "type"); + + /** + * + */ + public final static Property RDF_SUBJECT = createProperty(RDF + "subject"); + + /** + * + */ + public final static Property RDF_PREDICATE = createProperty(RDF + "predicate"); + + /** + * + */ + public final static Property RDF_OBJECT = createProperty(RDF + "object"); + + /** + * + */ + public final static String VOID = "http://rdfs.org/ns/void#"; + + /** + * + */ + public final static Property VOID_TRIPLES = createProperty(VOID + "triples"); + + /** + * + */ + public final static Property VOID_SUBSET = createProperty(VOID + "subset"); + + /** + * + */ + public final static Property VOID_DATASET = createProperty(VOID + "Dataset"); + + /** + * + */ + public final static String HYDRA = "http://www.w3.org/ns/hydra/core#"; + + /** + * + */ + public final static Property HYDRA_TOTALITEMS = createProperty(HYDRA + "totalItems"); + + /** + * + */ + public final static Property HYDRA_ITEMSPERPAGE = createProperty(HYDRA + "itemsPerPage"); + + /** + * + */ + public final static Property HYDRA_SEARCH = createProperty(HYDRA + "search"); + + /** + * + */ + public final static Property HYDRA_TEMPLATE = createProperty(HYDRA + "template"); + + /** + * + */ + public final static Property HYDRA_MAPPING = createProperty(HYDRA + "mapping"); + + /** + * + */ + public final static Property HYDRA_VARIABLE = createProperty(HYDRA + "variable"); + + /** + * + */ + public final static Property HYDRA_PROPERTY = createProperty(HYDRA + "property"); + + /** + * + */ + public final static Property HYDRA_COLLECTION = createProperty(HYDRA + "Collection"); + + /** + * + */ + public final static Property HYDRA_PAGEDCOLLECTION = createProperty(HYDRA + "PagedCollection"); + + /** + * + */ + public final static Property HYDRA_FIRSTPAGE = createProperty(HYDRA + "firstPage"); + + /** + * + */ + public final static Property HYDRA_LASTPAGE = createProperty(HYDRA + "lastPage"); + + /** + * + */ + public final static Property HYDRA_NEXTPAGE = createProperty(HYDRA + "nextPage"); + + /** + * + */ + public final static Property HYDRA_PREVIOUSPAGE = createProperty(HYDRA + "previousPage"); + + /** + * + */ + public final static Property INVALID_URI = createProperty("urn:invalid"); + + private static Property createProperty(String uri) { + return ResourceFactory.createProperty(uri); + } +} diff --git a/api/src/main/java/org/linkeddatafragments/util/MIMEParse.java b/api/src/main/java/org/linkeddatafragments/util/MIMEParse.java new file mode 100644 index 000000000..c6c43f80b --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/util/MIMEParse.java @@ -0,0 +1,299 @@ +package org.linkeddatafragments.util; + +import org.apache.commons.lang.math.NumberUtils; +import org.apache.commons.lang3.StringUtils; +import org.linkeddatafragments.exceptions.NoRegisteredMimeTypesException; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * MIME-Type Parser + * + * This class provides basic functions for handling mime-types. It can handle + * matching mime-types against a list of media-ranges. See section 14.1 of the + * HTTP specification [RFC 2616] for a complete explanation. + * + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 + * + * A port to Java of Joe Gregorio's MIME-Type Parser: + * + * http://code.google.com/p/mimeparse/ + * + * Ported by Tom Zellman. + * Extended by Miel Vander Sande + * + */ +public final class MIMEParse +{ + private final static List mimeTypes = new ArrayList<>(); + + /** + * Register mimeType in collection + * @param mimeType + */ + public static void register(String mimeType) { + mimeTypes.add(mimeType); + } + + + /** + * Parse results container + */ + protected static class ParseResults + { + String type; + + String subType; + + // !a dictionary of all the parameters for the media range + Map params; + + @Override + public String toString() + { + StringBuffer s = new StringBuffer("('" + type + "', '" + subType + + "', {"); + for (String k : params.keySet()) + s.append("'" + k + "':'" + params.get(k) + "',"); + return s.append("})").toString(); + } + } + + /** + * Carves up a mime-type and returns a ParseResults object + * + * For example, the media range 'application/xhtml;q=0.5' would get parsed + * into: + * + * ('application', 'xhtml', {'q', '0.5'}) + * @param mimeType + * @return + */ + protected static ParseResults parseMimeType(String mimeType) + { + String[] parts = StringUtils.split(mimeType, ";"); + ParseResults results = new ParseResults(); + results.params = new HashMap(); + + for (int i = 1; i < parts.length; ++i) + { + String p = parts[i]; + String[] subParts = StringUtils.split(p, '='); + if (subParts.length == 2) + results.params.put(subParts[0].trim(), subParts[1].trim()); + } + String fullType = parts[0].trim(); + + // Java URLConnection class sends an Accept header that includes a + // single "*" - Turn it into a legal wildcard. + if (fullType.equals("*")) + fullType = "*/*"; + String[] types = StringUtils.split(fullType, "/"); + results.type = types[0].trim(); + results.subType = types[1].trim(); + return results; + } + + /** + * Carves up a media range and returns a ParseResults. + * + * For example, the media range 'application/*;q=0.5' would get parsed into: + * + * ('application', '*', {'q', '0.5'}) + * + * In addition this function also guarantees that there is a value for 'q' + * in the params dictionary, filling it in with a proper default if + * necessary. + * + * @param range + * @return + */ + protected static ParseResults parseMediaRange(String range) + { + ParseResults results = parseMimeType(range); + String q = results.params.get("q"); + float f = NumberUtils.toFloat(q, 1); + if (StringUtils.isBlank(q) || f < 0 || f > 1) + results.params.put("q", "1"); + return results; + } + + /** + * Structure for holding a fitness/quality combo + */ + protected static class FitnessAndQuality implements + Comparable + { + int fitness; + + float quality; + + String mimeType; // optionally used + + /** + * + * @param fitness + * @param quality + */ + public FitnessAndQuality(int fitness, float quality) + { + this.fitness = fitness; + this.quality = quality; + } + + public int compareTo(FitnessAndQuality o) + { + if (fitness == o.fitness) + { + if (quality == o.quality) + return 0; + else + return quality < o.quality ? -1 : 1; + } + else + return fitness < o.fitness ? -1 : 1; + } + } + + /** + * Find the best match for a given mimeType against a list of media_ranges + * that have already been parsed by MimeParse.parseMediaRange(). Returns a + * tuple of the fitness value and the value of the 'q' quality parameter of + * the best match, or (-1, 0) if no match was found. Just as for + * quality_parsed(), 'parsed_ranges' must be a list of parsed media ranges. + * + * @param mimeType + * @param parsedRanges + * @return + */ + protected static FitnessAndQuality fitnessAndQualityParsed(String mimeType, + Collection parsedRanges) + { + int bestFitness = -1; + float bestFitQ = 0; + ParseResults target = parseMediaRange(mimeType); + + for (ParseResults range : parsedRanges) + { + if ((target.type.equals(range.type) || range.type.equals("*") || target.type + .equals("*")) + && (target.subType.equals(range.subType) + || range.subType.equals("*") || target.subType + .equals("*"))) + { + for (String k : target.params.keySet()) + { + int paramMatches = 0; + if (!k.equals("q") && range.params.containsKey(k) + && target.params.get(k).equals(range.params.get(k))) + { + paramMatches++; + } + int fitness = (range.type.equals(target.type)) ? 100 : 0; + fitness += (range.subType.equals(target.subType)) ? 10 : 0; + fitness += paramMatches; + if (fitness > bestFitness) + { + bestFitness = fitness; + bestFitQ = NumberUtils + .toFloat(range.params.get("q"), 0); + } + } + } + } + return new FitnessAndQuality(bestFitness, bestFitQ); + } + + /** + * Find the best match for a given mime-type against a list of ranges that + * have already been parsed by parseMediaRange(). Returns the 'q' quality + * parameter of the best match, 0 if no match was found. This function + * bahaves the same as quality() except that 'parsed_ranges' must be a list + * of parsed media ranges. + * + * @param mimeType + * @param parsedRanges + * @return + */ + protected static float qualityParsed(String mimeType, + Collection parsedRanges) + { + return fitnessAndQualityParsed(mimeType, parsedRanges).quality; + } + + /** + * Returns the quality 'q' of a mime-type when compared against the + * mediaRanges in ranges. For example: + * + * @param mimeType + * @param ranges + * @return + */ + public static float quality(String mimeType, String ranges) + { + List results = new LinkedList(); + for (String r : StringUtils.split(ranges, ',')) + results.add(parseMediaRange(r)); + return qualityParsed(mimeType, results); + } + + /** + * Takes a list of supported mime-types and finds the best match for all the + * media-ranges listed in header. The value of header must be a string that + * conforms to the format of the HTTP Accept: header. The value of + * 'supported' is a list of mime-types. + * + * MimeParse.bestMatch(Arrays.asList(new String[]{"application/xbel+xml", + * "text/xml"}), "text/*;q=0.5,*; q=0.1") 'text/xml' + * + * @param supported + * @param header + * @return + * @throws org.linkeddatafragments.exceptions.NoRegisteredMimeTypesException + */ + public static String bestMatch(List supported, String header) throws NoRegisteredMimeTypesException + { + if (supported.isEmpty()) + throw new NoRegisteredMimeTypesException(); + + List parseResults = new LinkedList(); + List weightedMatches = new LinkedList(); + for (String r : StringUtils.split(header, ',')) + parseResults.add(parseMediaRange(r)); + + for (String s : supported) + { + FitnessAndQuality fitnessAndQuality = fitnessAndQualityParsed(s, + parseResults); + fitnessAndQuality.mimeType = s; + weightedMatches.add(fitnessAndQuality); + } + Collections.sort(weightedMatches); + + FitnessAndQuality lastOne = weightedMatches + .get(weightedMatches.size() - 1); + return NumberUtils.compare(lastOne.quality, 0) != 0 ? lastOne.mimeType : supported.get(0); + } + + /** + * + * @param header + * @return + * @throws NoRegisteredMimeTypesException + */ + public static String bestMatch(String header) throws NoRegisteredMimeTypesException + { + return bestMatch(mimeTypes, header); + } + + // hidden + private MIMEParse() + { + } +} diff --git a/api/src/main/java/org/linkeddatafragments/util/RDFTermParser.java b/api/src/main/java/org/linkeddatafragments/util/RDFTermParser.java new file mode 100644 index 000000000..eafa401b5 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/util/RDFTermParser.java @@ -0,0 +1,116 @@ +package org.linkeddatafragments.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parses strings (as obtained from HTTP request parameters) into RDF terms. + * + * @param type for representing RDF terms + * + * @author Olaf Hartig + */ +abstract public class RDFTermParser +{ + + /** + * + */ + public static final Pattern STRINGPATTERN + = Pattern.compile("^\"(.*)\"(?:@(.*)|\\^\\^]*)>?)?$"); + + /** + * + * @param param + * @return + */ + public TermType parseIntoRDFNode( final String param ) + { + if ( param == null || param.isEmpty() ) + return handleUnparsableParameter( param ); + + // identify the kind of RDF term based on the first character + char firstChar = param.charAt(0); + switch ( firstChar ) + { + // blank node + case '_': + return createBlankNode( param ); + + // angular brackets indicate a URI + case '<': + return createURI( param.substring(1, param.length()-1) ); + + // quotes indicate a string + case '"': + Matcher matcher = STRINGPATTERN.matcher( param ); + if ( matcher.matches() ) { + String label = matcher.group(1); + String langTag = matcher.group(2); + String typeURI = matcher.group(3); + + if ( langTag != null ) + return createLanguageLiteral( label, langTag ); + + else if ( typeURI != null ) + return createTypedLiteral( label, typeURI ); + + else + return createPlainLiteral( label ); + } + else + return handleUnparsableParameter( param ); + + // assume it is a URI without angular brackets + default: + return createURI( param ); + } + } + + /** + * + * @param label + * @return + */ + abstract public TermType createBlankNode( final String label ); + + /** + * + * @param uri + * @return + */ + abstract public TermType createURI( final String uri ); + + /** + * + * @param label + * @param typeURI + * @return + */ + abstract public TermType createTypedLiteral( final String label, + final String typeURI ); + + /** + * + * @param label + * @param langTag + * @return + */ + abstract public TermType createLanguageLiteral( final String label, + final String langTag ); + + /** + * + * @param label + * @return + */ + abstract public TermType createPlainLiteral( final String label ); + + /** + * + * @param param + * @return + */ + abstract public TermType handleUnparsableParameter( final String param ); + +} diff --git a/api/src/main/java/org/linkeddatafragments/util/TriplePatternElementParser.java b/api/src/main/java/org/linkeddatafragments/util/TriplePatternElementParser.java new file mode 100644 index 000000000..9621bab09 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/util/TriplePatternElementParser.java @@ -0,0 +1,80 @@ +package org.linkeddatafragments.util; + +import org.linkeddatafragments.fragments.tpf.ITriplePatternElement; +import org.linkeddatafragments.fragments.tpf.TriplePatternElementFactory; + +/** + * Parses strings (as obtained from HTTP request parameters) into + * {@link ITriplePatternElement}s. + * + * @param type for representing constants in triple patterns + * (i.e., URIs and literals) + * @param type for representing named variables in triple patterns + * @param type for representing anonymous variables in triple + * patterns (i.e., variables denoted by a blank node) + * + * @author Olaf Hartig + * @author Ruben Verborgh + */ +abstract public + class TriplePatternElementParser + extends RDFTermParser +{ + + /** + * + */ + public final TriplePatternElementFactory + factory = new TriplePatternElementFactory(); + + /** + * + * @param param + * @return + */ + public ITriplePatternElement + parseIntoTriplePatternElement( final String param ) + { + // nothing or empty indicates an unspecified variable + if ( param == null || param.isEmpty() ) + return factory.createUnspecifiedVariable(); + + // identify the kind of RDF term based on the first character + char firstChar = param.charAt(0); + switch ( firstChar ) + { + // specific variable that has a name + case '?': + { + final String varName = param.substring(1); + final NamedVarType var = createNamedVariable( varName ); + return factory.createNamedVariable( var ); + } + + // specific variable that is denoted by a blank node + case '_': + { + final AnonVarType var = createAnonymousVariable( param ); + return factory.createAnonymousVariable( var ); + } + + // assume it is an RDF term + default: + return factory.createConstantRDFTerm( parseIntoRDFNode(param) ); + } + } + + /** + * + * @param varName + * @return + */ + abstract public NamedVarType createNamedVariable( final String varName ); + + /** + * + * @param label + * @return + */ + abstract public AnonVarType createAnonymousVariable( final String label ); +} diff --git a/api/src/main/java/org/linkeddatafragments/util/TriplePatternElementParserForJena.java b/api/src/main/java/org/linkeddatafragments/util/TriplePatternElementParserForJena.java new file mode 100644 index 000000000..a54114d9b --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/util/TriplePatternElementParserForJena.java @@ -0,0 +1,128 @@ +package org.linkeddatafragments.util; + +import org.apache.jena.datatypes.RDFDatatype; +import org.apache.jena.datatypes.TypeMapper; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.ResourceFactory; + +/** + * A {@link TriplePatternElementParser} for Jena-based backends. + * + * @author Olaf Hartig + */ +public class TriplePatternElementParserForJena + extends TriplePatternElementParser +{ + private static TriplePatternElementParserForJena instance = null; + + /** + * + * @return + */ + public static TriplePatternElementParserForJena getInstance() + { + if ( instance == null ) { + instance = new TriplePatternElementParserForJena(); + } + return instance; + } + + /** + * + */ + protected TriplePatternElementParserForJena() {} + + /** + * + * @param varName + * @return + */ + @Override + public String createNamedVariable( final String varName ) + { + return varName; + } + + /** + * + * @param label + * @return + */ + @Override + public String createAnonymousVariable( final String label ) + { + return label; + } + + /** + * + * @param label + * @return + */ + @Override + public RDFNode createBlankNode(final String label ) + { + return ResourceFactory.createResource(); + } + + /** + * + * @param uri + * @return + */ + @Override + public RDFNode createURI(final String uri ) + { + return ResourceFactory.createResource( uri ); + } + + /** + * + * @param label + * @param typeURI + * @return + */ + @Override + public RDFNode createTypedLiteral(final String label, + final String typeURI ) + { + final RDFDatatype dt = TypeMapper.getInstance() + .getSafeTypeByName( typeURI ); + return ResourceFactory.createTypedLiteral( label, dt ); + } + + /** + * + * @param label + * @param languageTag + * @return + */ + @Override + public RDFNode createLanguageLiteral(final String label, + final String languageTag ) + { + return ResourceFactory.createLangLiteral( label, languageTag ); + } + + /** + * + * @param label + * @return + */ + @Override + public RDFNode createPlainLiteral(final String label ) + { + return ResourceFactory.createPlainLiteral( label ); + } + + /** + * + * @param parameter + * @return + */ + @Override + public RDFNode handleUnparsableParameter(final String parameter ) + { + return CommonResources.INVALID_URI; + } +} diff --git a/api/src/main/java/org/linkeddatafragments/views/HtmlTriplePatternFragmentWriterImpl.java b/api/src/main/java/org/linkeddatafragments/views/HtmlTriplePatternFragmentWriterImpl.java new file mode 100644 index 000000000..179c9744e --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/views/HtmlTriplePatternFragmentWriterImpl.java @@ -0,0 +1,145 @@ +package org.linkeddatafragments.views; + +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import freemarker.template.TemplateExceptionHandler; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Statement; +import org.apache.jena.rdf.model.StmtIterator; +import org.linkeddatafragments.datasource.IDataSource; +import org.linkeddatafragments.datasource.index.IndexDataSource; +import org.linkeddatafragments.fragments.ILinkedDataFragment; +import org.linkeddatafragments.fragments.tpf.ITriplePatternFragment; +import org.linkeddatafragments.fragments.tpf.ITriplePatternFragmentRequest; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +//TODO: Refactor to a composable & flexible architecture using DataSource types, fragments types and request types + +/** + * Serializes an {@link ILinkedDataFragment} to the HTML format + * + * @author Miel Vander Sande + */ +public class HtmlTriplePatternFragmentWriterImpl extends TriplePatternFragmentWriterBase implements ILinkedDataFragmentWriter { + private final Configuration cfg; + + private final Template indexTemplate; + private final Template datasourceTemplate; + private final Template notfoundTemplate; + private final Template errorTemplate; + + private final String HYDRA = "http://www.w3.org/ns/hydra/core#"; + + /** + * + * @param prefixes + * @param datasources + * @throws IOException + */ + public HtmlTriplePatternFragmentWriterImpl(Map prefixes, HashMap datasources) throws IOException { + super(prefixes, datasources); + + cfg = new Configuration(Configuration.VERSION_2_3_22); + cfg.setClassForTemplateLoading(getClass(), "/views"); + cfg.setDefaultEncoding("UTF-8"); + cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + + indexTemplate = cfg.getTemplate("index.ftl.html"); + datasourceTemplate = cfg.getTemplate("datasource.ftl.html"); + notfoundTemplate = cfg.getTemplate("notfound.ftl.html"); + errorTemplate = cfg.getTemplate("error.ftl.html"); + } + + /** + * + * @param outputStream + * @param datasource + * @param fragment + * @param tpfRequest + * @throws IOException + * @throws TemplateException + */ + @Override + public void writeFragment(ServletOutputStream outputStream, IDataSource datasource, ITriplePatternFragment fragment, ITriplePatternFragmentRequest tpfRequest) throws IOException, TemplateException{ + Map data = new HashMap(); + + // base.ftl.html + data.put("assetsPath", "assets/"); + data.put("header", datasource.getTitle()); + data.put("date", new Date()); + + // fragment.ftl.html + data.put("datasourceUrl", tpfRequest.getDatasetURL()); + data.put("datasource", datasource); + + // Parse controls to template variables + StmtIterator controls = fragment.getControls(); + while (controls.hasNext()) { + Statement control = controls.next(); + + String predicate = control.getPredicate().getURI(); + RDFNode object = control.getObject(); + if (!object.isAnon()) { + String value = object.isURIResource() ? object.asResource().getURI() : object.asLiteral().getLexicalForm(); + data.put(predicate.replaceFirst(HYDRA, ""), value); + } + } + + // Add metadata + data.put("totalEstimate", fragment.getTotalSize()); + data.put("itemsPerPage", fragment.getMaxPageSize()); + + // Add triples and datasources + List triples = fragment.getTriples().toList(); + data.put("triples", triples); + data.put("datasources", getDatasources()); + + // Calculate start and end triple number + Long start = ((tpfRequest.getPageNumber() - 1) * fragment.getMaxPageSize()) + 1; + data.put("start", start); + data.put("end", start + (triples.size() < fragment.getMaxPageSize() ? triples.size() : fragment.getMaxPageSize())); + + // Compose query object + Map query = new HashMap(); + query.put("subject", !tpfRequest.getSubject().isVariable() ? tpfRequest.getSubject().asConstantTerm() : ""); + query.put("predicate", !tpfRequest.getPredicate().isVariable() ? tpfRequest.getPredicate().asConstantTerm() : ""); + query.put("object", !tpfRequest.getObject().isVariable() ? tpfRequest.getObject().asConstantTerm() : ""); + data.put("query", query); + + // Get the template (uses cache internally) + Template temp = datasource instanceof IndexDataSource ? indexTemplate : datasourceTemplate; + + // Merge data-model with template + temp.process(data, new OutputStreamWriter(outputStream)); + } + + @Override + public void writeNotFound(ServletOutputStream outputStream, HttpServletRequest request) throws Exception { + Map data = new HashMap(); + data.put("assetsPath", "assets/"); + data.put("datasources", getDatasources()); + data.put("date", new Date()); + data.put("url", request.getRequestURL().toString()); + + notfoundTemplate.process(data, new OutputStreamWriter(outputStream)); + } + + @Override + public void writeError(ServletOutputStream outputStream, Exception ex) throws Exception { + Map data = new HashMap(); + data.put("assetsPath", "assets/"); + data.put("date", new Date()); + data.put("error", ex); + + errorTemplate.process(data, new OutputStreamWriter(outputStream)); + } +} diff --git a/api/src/main/java/org/linkeddatafragments/views/ILinkedDataFragmentWriter.java b/api/src/main/java/org/linkeddatafragments/views/ILinkedDataFragmentWriter.java new file mode 100644 index 000000000..adaf9cbda --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/views/ILinkedDataFragmentWriter.java @@ -0,0 +1,44 @@ +package org.linkeddatafragments.views; + +import org.linkeddatafragments.datasource.IDataSource; +import org.linkeddatafragments.fragments.ILinkedDataFragment; +import org.linkeddatafragments.fragments.ILinkedDataFragmentRequest; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; + +/** + * Represents a possible writer to serialize an {@link ILinkedDataFragment} object + * + * @author Miel Vander Sande + */ +public interface ILinkedDataFragmentWriter { + /** + * Writes a 404 Not Found error + * + * @param outputStream The response stream to write to + * @param request Request that is unable to answer + * @throws Exception Error that occurs while serializing + */ + public void writeNotFound(ServletOutputStream outputStream, HttpServletRequest request) throws Exception; + + /** + * Writes a 5XX error + * + * @param outputStream The response stream to write to + * @param ex Exception that occurred + * @throws Exception Error that occurs while serializing + */ + public void writeError(ServletOutputStream outputStream, Exception ex) throws Exception; + + /** + * Serializes and writes a {@link ILinkedDataFragment} + * + * @param outputStream The response stream to write to + * @param datasource + * @param fragment + * @param ldfRequest Parsed request for fragment + * @throws Exception Error that occurs while serializing + */ + public void writeFragment(ServletOutputStream outputStream, IDataSource datasource, ILinkedDataFragment fragment, ILinkedDataFragmentRequest ldfRequest) throws Exception; +} diff --git a/api/src/main/java/org/linkeddatafragments/views/LinkedDataFragmentWriterBase.java b/api/src/main/java/org/linkeddatafragments/views/LinkedDataFragmentWriterBase.java new file mode 100644 index 000000000..0e7aed847 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/views/LinkedDataFragmentWriterBase.java @@ -0,0 +1,42 @@ +package org.linkeddatafragments.views; + +import org.linkeddatafragments.datasource.IDataSource; + +import java.util.HashMap; +import java.util.Map; + +/** + * Base class of any implementation of {@link ILinkedDataFragmentWriter}. + * + * @author Miel Vander Sande + */ +public abstract class LinkedDataFragmentWriterBase implements ILinkedDataFragmentWriter { + private final Map prefixes; + private final HashMap datasources; + + /** + * + * @param prefixes + * @param datasources + */ + public LinkedDataFragmentWriterBase(Map prefixes, HashMap datasources) { + this.prefixes = prefixes; + this.datasources = datasources; + } + + /** + * + * @return + */ + public Map getPrefixes() { + return prefixes; + } + + /** + * + * @return + */ + public HashMap getDatasources() { + return datasources; + } +} diff --git a/api/src/main/java/org/linkeddatafragments/views/LinkedDataFragmentWriterFactory.java b/api/src/main/java/org/linkeddatafragments/views/LinkedDataFragmentWriterFactory.java new file mode 100644 index 000000000..efc868cc5 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/views/LinkedDataFragmentWriterFactory.java @@ -0,0 +1,35 @@ +package org.linkeddatafragments.views; + +import org.linkeddatafragments.datasource.IDataSource; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * A factory for {@link ILinkedDataFragmentWriter}s. + * + * @author Miel Vander Sande + */ +public class LinkedDataFragmentWriterFactory { + + private final static String HTML = "text/html"; + + /** + * Creates {@link ILinkedDataFragmentWriter} for a given mimeType + * + * @param prefixes Configured prefixes to be used in serialization + * @param datasources Configured datasources + * @param mimeType mimeType to create writer for + * @return created writer + * @throws IOException + */ + public static ILinkedDataFragmentWriter create(Map prefixes, HashMap datasources, String mimeType) throws IOException { + switch (mimeType) { + case HTML: + return new HtmlTriplePatternFragmentWriterImpl(prefixes, datasources); + default: + return new RdfWriterImpl(prefixes, datasources, mimeType); + } + } +} diff --git a/api/src/main/java/org/linkeddatafragments/views/RdfWriterImpl.java b/api/src/main/java/org/linkeddatafragments/views/RdfWriterImpl.java new file mode 100644 index 000000000..f355fdb75 --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/views/RdfWriterImpl.java @@ -0,0 +1,55 @@ +package org.linkeddatafragments.views; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.riot.Lang; +import org.apache.jena.riot.RDFDataMgr; +import org.apache.jena.riot.RDFLanguages; +import org.linkeddatafragments.datasource.IDataSource; +import org.linkeddatafragments.fragments.ILinkedDataFragment; +import org.linkeddatafragments.fragments.ILinkedDataFragmentRequest; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * Serializes an {@link ILinkedDataFragment} to an RDF format + * + * @author Miel Vander Sande + */ +public class RdfWriterImpl extends LinkedDataFragmentWriterBase implements ILinkedDataFragmentWriter { + + private final Lang contentType; + + public RdfWriterImpl(Map prefixes, HashMap datasources, String mimeType) { + super(prefixes, datasources); + this.contentType = RDFLanguages.contentTypeToLang(mimeType); + } + + @Override + public void writeNotFound(ServletOutputStream outputStream, HttpServletRequest request) throws IOException { + outputStream.println(request.getRequestURL().toString() + " not found!"); + outputStream.close(); + } + + @Override + public void writeError(ServletOutputStream outputStream, Exception ex) throws IOException { + outputStream.println(ex.getMessage()); + outputStream.close(); + } + + @Override + public void writeFragment(ServletOutputStream outputStream, IDataSource datasource, ILinkedDataFragment fragment, ILinkedDataFragmentRequest ldfRequest) throws Exception { + final Model output = ModelFactory.createDefaultModel(); + output.setNsPrefixes(getPrefixes()); + output.add(fragment.getMetadata()); + output.add(fragment.getTriples()); + output.add(fragment.getControls()); + + RDFDataMgr.write(outputStream, output, contentType); + } + +} diff --git a/api/src/main/java/org/linkeddatafragments/views/TriplePatternFragmentWriterBase.java b/api/src/main/java/org/linkeddatafragments/views/TriplePatternFragmentWriterBase.java new file mode 100644 index 000000000..43ebc3e3b --- /dev/null +++ b/api/src/main/java/org/linkeddatafragments/views/TriplePatternFragmentWriterBase.java @@ -0,0 +1,46 @@ +package org.linkeddatafragments.views; + +import freemarker.template.TemplateException; +import org.linkeddatafragments.datasource.IDataSource; +import org.linkeddatafragments.fragments.ILinkedDataFragment; +import org.linkeddatafragments.fragments.ILinkedDataFragmentRequest; +import org.linkeddatafragments.fragments.tpf.ITriplePatternFragment; +import org.linkeddatafragments.fragments.tpf.ITriplePatternFragmentRequest; + +import javax.servlet.ServletOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * Base class of any implementation for ITriplePatternFragment. + * + * @author Miel Vander Sande + */ +public abstract class TriplePatternFragmentWriterBase extends LinkedDataFragmentWriterBase implements ILinkedDataFragmentWriter { + + /** + * + * @param prefixes + * @param datasources + */ + public TriplePatternFragmentWriterBase(Map prefixes, HashMap datasources) { + super(prefixes, datasources); + } + + @Override + public void writeFragment(ServletOutputStream outputStream, IDataSource datasource, ILinkedDataFragment fragment, ILinkedDataFragmentRequest ldfRequest) throws Exception { + writeFragment(outputStream, datasource, (ITriplePatternFragment) fragment, (ITriplePatternFragmentRequest) ldfRequest); + } + + /** + * + * @param outputStream + * @param datasource + * @param fragment + * @param tpfRequest + * @throws IOException + * @throws TemplateException + */ + abstract public void writeFragment(ServletOutputStream outputStream, IDataSource datasource, ITriplePatternFragment fragment, ITriplePatternFragmentRequest tpfRequest) throws IOException, TemplateException; +} diff --git a/api/src/main/java/org/vivoweb/linkeddatafragments/datasource/rdfservice/RDFServiceBasedRequestProcessorForTPFs.java b/api/src/main/java/org/vivoweb/linkeddatafragments/datasource/rdfservice/RDFServiceBasedRequestProcessorForTPFs.java new file mode 100644 index 000000000..ef17a21d8 --- /dev/null +++ b/api/src/main/java/org/vivoweb/linkeddatafragments/datasource/rdfservice/RDFServiceBasedRequestProcessorForTPFs.java @@ -0,0 +1,201 @@ +package org.vivoweb.linkeddatafragments.datasource.rdfservice; + +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.dao.jena.QueryUtils; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException; +import edu.cornell.mannlib.vitro.webapp.rdfservice.ResultSetConsumer; +import org.apache.jena.atlas.io.StringWriterI; +import org.apache.jena.query.Dataset; +import org.apache.jena.query.Query; +import org.apache.jena.query.QueryExecution; +import org.apache.jena.query.QueryExecutionFactory; +import org.apache.jena.query.QueryFactory; +import org.apache.jena.query.QuerySolution; +import org.apache.jena.query.QuerySolutionMap; +import org.apache.jena.query.ResultSet; +import org.apache.jena.query.Syntax; +import org.apache.jena.rdf.model.Literal; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.riot.out.NodeFormatter; +import org.apache.jena.riot.out.NodeFormatterTTL; +import org.apache.jena.tdb.TDBFactory; +import org.linkeddatafragments.datasource.AbstractRequestProcessorForTriplePatterns; +import org.linkeddatafragments.datasource.IFragmentRequestProcessor; +import org.linkeddatafragments.fragments.ILinkedDataFragment; +import org.linkeddatafragments.fragments.tpf.ITriplePatternElement; +import org.linkeddatafragments.fragments.tpf.ITriplePatternFragmentRequest; + +import java.io.File; + +public class RDFServiceBasedRequestProcessorForTPFs + extends AbstractRequestProcessorForTriplePatterns +{ + private static RDFService rdfService; + + public static void setRDFService(RDFService pRDFService) { + rdfService = pRDFService; + } + + @Override + protected Worker getTPFSpecificWorker( + final ITriplePatternFragmentRequest request ) + throws IllegalArgumentException + { + return new Worker( request ); + } + + /** + * + */ + protected class Worker + extends AbstractRequestProcessorForTriplePatterns.Worker + { + public Worker( + final ITriplePatternFragmentRequest req ) + { + super( req ); + } + + private void appendNode(StringBuilder builder, RDFNode node) { + if (node.isLiteral()) { + builder.append(literalToString(node.asLiteral())); + } else if (node.isURIResource()) { + builder.append('<' + node.asResource().getURI() + '>'); + } + } + + private String literalToString(Literal l) { + StringWriterI sw = new StringWriterI(); + NodeFormatter fmt = new NodeFormatterTTL(null, null); + fmt.formatLiteral(sw, l.asNode()); + return sw.toString(); + } + + @Override + protected ILinkedDataFragment createFragment( + final ITriplePatternElement subject, + final ITriplePatternElement predicate, + final ITriplePatternElement object, + final long offset, + final long limit ) + { + StringBuilder whereClause = new StringBuilder(); + StringBuilder filter = new StringBuilder(); + StringBuilder orderBy = new StringBuilder(); + + if ( ! subject.isVariable() ) { + appendNode(whereClause.append(' '), subject.asConstantTerm()); + } else { + whereClause.append(" ?s"); + if (filter.length() > 0) { filter.append(" && "); } + filter.append("!isBlank(?s)"); + orderBy.append(" ?s"); + } + + if ( ! predicate.isVariable() ) { + appendNode(whereClause.append(' '), predicate.asConstantTerm()); + } else { + whereClause.append(" ?p"); + if (filter.length() > 0) { filter.append(" && "); } + filter.append("!isBlank(?p)"); + orderBy.append(" ?p"); + } + + if ( ! object.isVariable() ) { + appendNode(whereClause.append(' '), object.asConstantTerm()); + } else { + whereClause.append(" ?o"); + if (filter.length() > 0) { filter.append(" && "); } + filter.append("!isBlank(?o)"); + orderBy.append(" ?o"); + } + + StringBuilder constructQuery = new StringBuilder(); + + constructQuery.append("CONSTRUCT { "); + constructQuery.append(whereClause.toString()); + constructQuery.append(" } WHERE { "); + constructQuery.append(whereClause.toString()).append(" . "); + if (filter.length() > 0) { + constructQuery.append(" FILTER(").append(filter.toString()).append(")"); + } + constructQuery.append(" }"); + + if (orderBy.length() > 0) { + constructQuery.append(" ORDER BY").append(orderBy.toString()); + } + + if (limit > 0) { + constructQuery.append(" LIMIT ").append(limit); + } + + if (offset > 0) { + constructQuery.append(" OFFSET ").append(offset); + } + + Model triples = ModelFactory.createDefaultModel(); + + try { + rdfService.sparqlConstructQuery(constructQuery.toString(), triples); + } catch (RDFServiceException e) { + return createEmptyTriplePatternFragment(); + } + + if (triples.isEmpty()) { + return createEmptyTriplePatternFragment(); + } + + // Try to get an estimate + long size = triples.size(); + long estimate = -1; + + StringBuilder count = new StringBuilder(); + count.append("SELECT (COUNT(*) AS ?count) WHERE { "); + count.append(whereClause.toString()); + count.append(" . "); + if (filter.length() > 0) { + count.append(" FILTER(").append(filter.toString()).append(") "); + } + count.append(" }"); + try { + CountConsumer countConsumer = new CountConsumer(); + rdfService.sparqlSelectQuery(count.toString(), countConsumer); + estimate = countConsumer.estimate; + } catch (RDFServiceException e) { + return createEmptyTriplePatternFragment(); + } + + // No estimate or incorrect + if (estimate < offset + size) { + estimate = (size == limit) ? offset + size + 1 : offset + size; + } + + // create the fragment + final boolean isLastPage = ( estimate < offset + limit ); + return createTriplePatternFragment( triples, estimate, isLastPage ); + } + + } // end of class Worker + + + /** + * Constructor + */ + public RDFServiceBasedRequestProcessorForTPFs() { + } + + class CountConsumer extends ResultSetConsumer { + public long estimate = -1; + + @Override + protected void processQuerySolution(QuerySolution qs) { + if (estimate == -1) { + Literal literal = qs.getLiteral("count"); + estimate = literal.getLong(); + } + } + } +} diff --git a/api/src/main/java/org/vivoweb/linkeddatafragments/datasource/rdfservice/RDFServiceDataSource.java b/api/src/main/java/org/vivoweb/linkeddatafragments/datasource/rdfservice/RDFServiceDataSource.java new file mode 100644 index 000000000..7511aad96 --- /dev/null +++ b/api/src/main/java/org/vivoweb/linkeddatafragments/datasource/rdfservice/RDFServiceDataSource.java @@ -0,0 +1,46 @@ +package org.vivoweb.linkeddatafragments.datasource.rdfservice; + +import org.linkeddatafragments.datasource.DataSourceBase; +import org.linkeddatafragments.datasource.IFragmentRequestProcessor; +import org.linkeddatafragments.fragments.IFragmentRequestParser; +import org.linkeddatafragments.fragments.tpf.TPFRequestParserForJenaBackends; + +import java.io.File; + +/** + * Experimental Jena TDB-backed data source of Basic Linked Data Fragments. + * + * @author Bart Hanssens + * @author Olaf Hartig + */ +public class RDFServiceDataSource extends DataSourceBase { + + /** + * The request processor + * + */ + protected final RDFServiceBasedRequestProcessorForTPFs requestProcessor; + + @Override + public IFragmentRequestParser getRequestParser() + { + return TPFRequestParserForJenaBackends.getInstance(); + } + + @Override + public IFragmentRequestProcessor getRequestProcessor() + { + return requestProcessor; + } + + /** + * Constructor + * + * @param title + * @param description + */ + public RDFServiceDataSource(String title, String description) { + super(title, description); + requestProcessor = new RDFServiceBasedRequestProcessorForTPFs(); + } +} diff --git a/api/src/main/java/org/vivoweb/linkeddatafragments/datasource/rdfservice/RDFServiceDataSourceType.java b/api/src/main/java/org/vivoweb/linkeddatafragments/datasource/rdfservice/RDFServiceDataSourceType.java new file mode 100644 index 000000000..417466dba --- /dev/null +++ b/api/src/main/java/org/vivoweb/linkeddatafragments/datasource/rdfservice/RDFServiceDataSourceType.java @@ -0,0 +1,31 @@ +package org.vivoweb.linkeddatafragments.datasource.rdfservice; + +import com.google.gson.JsonObject; +import org.linkeddatafragments.datasource.IDataSource; +import org.linkeddatafragments.datasource.IDataSourceType; +import org.linkeddatafragments.exceptions.DataSourceCreationException; + +import java.io.File; + +/** + * The type of Triple Pattern Fragment data sources that are backed by + * a Jena TDB instance. + * + * @author Olaf Hartig + */ +public class RDFServiceDataSourceType implements IDataSourceType +{ + @Override + public IDataSource createDataSource( final String title, + final String description, + final JsonObject settings ) + throws DataSourceCreationException + { + try { + return new RDFServiceDataSource(title, description); + } catch (Exception ex) { + throw new DataSourceCreationException(ex); + } + } + +} diff --git a/api/src/main/java/org/vivoweb/linkeddatafragments/servlet/VitroLinkedDataFragmentServlet.java b/api/src/main/java/org/vivoweb/linkeddatafragments/servlet/VitroLinkedDataFragmentServlet.java new file mode 100644 index 000000000..b43f80aab --- /dev/null +++ b/api/src/main/java/org/vivoweb/linkeddatafragments/servlet/VitroLinkedDataFragmentServlet.java @@ -0,0 +1,270 @@ +package org.vivoweb.linkeddatafragments.servlet; + +import com.google.gson.JsonObject; +import edu.cornell.mannlib.vitro.webapp.beans.Ontology; +import edu.cornell.mannlib.vitro.webapp.controller.VitroHttpServlet; +import edu.cornell.mannlib.vitro.webapp.dao.OntologyDao; +import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService; +import org.apache.commons.io.IOUtils; +import org.apache.jena.riot.Lang; +import org.linkeddatafragments.config.ConfigReader; +import org.linkeddatafragments.datasource.DataSourceFactory; +import org.linkeddatafragments.datasource.DataSourceTypesRegistry; +import org.linkeddatafragments.datasource.IDataSource; +import org.linkeddatafragments.datasource.IDataSourceType; +import org.linkeddatafragments.datasource.index.IndexDataSource; +import org.linkeddatafragments.exceptions.DataSourceNotFoundException; +import org.linkeddatafragments.fragments.FragmentRequestParserBase; +import org.linkeddatafragments.fragments.ILinkedDataFragment; +import org.linkeddatafragments.fragments.ILinkedDataFragmentRequest; +import org.linkeddatafragments.util.MIMEParse; +import org.linkeddatafragments.views.ILinkedDataFragmentWriter; +import org.vivoweb.linkeddatafragments.views.HtmlTriplePatternFragmentWriterImpl; +import org.vivoweb.linkeddatafragments.views.LinkedDataFragmentWriterFactory; +import org.vivoweb.linkeddatafragments.datasource.rdfservice.RDFServiceBasedRequestProcessorForTPFs; +import org.vivoweb.linkeddatafragments.datasource.rdfservice.RDFServiceDataSource; +import org.vivoweb.linkeddatafragments.datasource.rdfservice.RDFServiceDataSourceType; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map.Entry; + +/** + * Servlet that responds with a Linked Data Fragment. + */ +public class VitroLinkedDataFragmentServlet extends VitroHttpServlet { + + private final static long serialVersionUID = 1L; + + private ConfigReader config; + private final HashMap dataSources = new HashMap<>(); + private final Collection mimeTypes = new ArrayList<>(); + + private File getConfigFile(ServletConfig config) throws IOException { + String path = config.getServletContext().getRealPath("/"); + if (path == null) { + // this can happen when running standalone + path = System.getProperty("user.dir"); + } + File cfg = new File(path, "config-example.json"); + if (!cfg.exists()) { + throw new IOException("Configuration file " + cfg + " not found."); + } + if (!cfg.isFile()) { + throw new IOException("Configuration file " + cfg + " is not a file."); + } + return cfg; + } + + @Override + public void init(ServletConfig servletConfig) throws ServletException { + try { + ServletContext ctx = servletConfig.getServletContext(); + RDFService rdfService = ModelAccess.on(ctx).getRDFService(); + RDFServiceBasedRequestProcessorForTPFs.setRDFService(rdfService); + + OntologyDao dao = ModelAccess.on(ctx).getWebappDaoFactory().getOntologyDao(); + + // load the configuration + config = new ConfigReader(new StringReader(getConfigJson(dao))); + + // register data source types + for ( Entry typeEntry : config.getDataSourceTypes().entrySet() ) { + if (!DataSourceTypesRegistry.isRegistered(typeEntry.getKey())) { + DataSourceTypesRegistry.register( typeEntry.getKey(), + typeEntry.getValue() ); + } + } + + // register data sources + for (Entry dataSource : config.getDataSources().entrySet()) { + dataSources.put(dataSource.getKey(), DataSourceFactory.create(dataSource.getValue())); + } + + // register content types + MIMEParse.register("text/html"); + MIMEParse.register(Lang.TTL.getHeaderString()); + MIMEParse.register(Lang.JSONLD.getHeaderString()); + MIMEParse.register(Lang.NTRIPLES.getHeaderString()); + MIMEParse.register(Lang.RDFXML.getHeaderString()); + + HtmlTriplePatternFragmentWriterImpl.setContextPath(servletConfig.getServletContext().getContextPath()); + } catch (Exception e) { + throw new ServletException(e); + } + } + + @Override + public void destroy() + { + for ( IDataSource dataSource : dataSources.values() ) { + try { + dataSource.close(); + } + catch( Exception e ) { + // ignore + } + } + } + + private IDataSource getDataSource(HttpServletRequest request) throws DataSourceNotFoundException { + String contextPath = request.getContextPath(); + String requestURI = request.getRequestURI(); + + String path = contextPath == null + ? requestURI + : requestURI.substring(contextPath.length()); + + if (path.startsWith("/tpf")) { + path = path.substring(4); + } + + if (path.equals("/") || path.isEmpty()) { + final String baseURL = FragmentRequestParserBase.extractBaseURL(request, config); + return new IndexDataSource(baseURL, dataSources); + } + + String dataSourceName = path.substring(1); + IDataSource dataSource = dataSources.get(dataSourceName); + if (dataSource == null) { + throw new DataSourceNotFoundException(dataSourceName); + } + + return dataSource; + } + + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException { + int fileNamePos = request.getRequestURI().toLowerCase().lastIndexOf("/tpf/assets/"); + if (fileNamePos > 0) { + try { + String fileName = request.getRequestURI().substring(fileNamePos + 12); + InputStream in = VitroLinkedDataFragmentServlet.class.getResourceAsStream(fileName); + if (in != null) { + IOUtils.copy(in, response.getOutputStream()); + } + return; + } catch (IOException ioe) { + } + } + + ILinkedDataFragment fragment = null; + try { + // do conneg + String bestMatch = MIMEParse.bestMatch(request.getHeader("Accept")); + + // set additional response headers + response.setHeader("Server", "Linked Data Fragments Server"); + response.setContentType(bestMatch); + response.setCharacterEncoding("utf-8"); + + // create a writer depending on the best matching mimeType + ILinkedDataFragmentWriter writer = LinkedDataFragmentWriterFactory.create(config.getPrefixes(), dataSources, bestMatch); + + try { + + final IDataSource dataSource = getDataSource( request ); + + final ILinkedDataFragmentRequest ldfRequest = + dataSource.getRequestParser() + .parseIntoFragmentRequest( request, config ); + + fragment = dataSource.getRequestProcessor() + .createRequestedFragment( ldfRequest ); + + response.setHeader("Access-Control-Allow-Origin", "*"); + writer.writeFragment(response.getOutputStream(), dataSource, fragment, ldfRequest); + + } catch (DataSourceNotFoundException ex) { + try { + response.setStatus(404); + writer.writeNotFound(response.getOutputStream(), request); + } catch (Exception ex1) { + throw new ServletException(ex1); + } + } catch (Exception e) { + response.setStatus(500); + writer.writeError(response.getOutputStream(), e); + } + + } catch (Exception e) { + throw new ServletException(e); + } + finally { + // close the fragment + if ( fragment != null ) { + try { + fragment.close(); + } + catch ( Exception e ) { + // ignore + } + } + } + } + + private String getConfigJson(OntologyDao dao) { + StringBuilder configJson = new StringBuilder(); + configJson.append("{\n"); + configJson.append(" \"title\": \"Linked Data Fragments server\",\n"); + configJson.append("\n"); + configJson.append(" \"datasourcetypes\": {\n"); + configJson.append(" \"RDFServiceDatasource\": \"" + RDFServiceDataSourceType.class.getCanonicalName() + "\"\n"); + configJson.append(" },\n"); + configJson.append("\n"); + configJson.append(" \"datasources\": {\n"); + configJson.append(" \"core\": {\n"); + configJson.append(" \"title\": \"core\",\n"); + configJson.append(" \"type\": \"RDFServiceDatasource\",\n"); + configJson.append(" \"description\": \"All data\"\n"); + configJson.append(" }\n"); + configJson.append(" },\n"); + configJson.append("\n"); + configJson.append(" \"prefixes\": {\n"); + configJson.append(" \"rdf\": \"http://www.w3.org/1999/02/22-rdf-syntax-ns#\",\n"); + configJson.append(" \"rdfs\": \"http://www.w3.org/2000/01/rdf-schema#\",\n"); + configJson.append(" \"hydra\": \"http://www.w3.org/ns/hydra/core#\",\n"); + configJson.append(" \"void\": \"http://rdfs.org/ns/void#\""); + + List onts = dao.getAllOntologies(); + if (onts != null) { + for (Ontology ont : onts) { + switch (ont.getPrefix()) { + case "rdf": + case "rdfs": + case "hydra": + case "void": + break; + + default: + configJson.append(",\n"); + configJson.append(" \""); + configJson.append(ont.getPrefix()); + configJson.append("\": \""); + configJson.append(ont.getURI()); + configJson.append("\""); + break; + } + } + } + + configJson.append(" }\n"); + configJson.append("}\n"); + + return configJson.toString(); + } +} diff --git a/api/src/main/java/org/vivoweb/linkeddatafragments/views/HtmlTriplePatternFragmentWriterImpl.java b/api/src/main/java/org/vivoweb/linkeddatafragments/views/HtmlTriplePatternFragmentWriterImpl.java new file mode 100644 index 000000000..c58a0743d --- /dev/null +++ b/api/src/main/java/org/vivoweb/linkeddatafragments/views/HtmlTriplePatternFragmentWriterImpl.java @@ -0,0 +1,221 @@ +package org.vivoweb.linkeddatafragments.views; + +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import freemarker.template.TemplateExceptionHandler; +import org.apache.jena.atlas.io.StringWriterI; +import org.apache.jena.rdf.model.Literal; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Statement; +import org.apache.jena.rdf.model.StmtIterator; +import org.apache.jena.rdf.model.impl.LiteralImpl; +import org.apache.jena.riot.out.NodeFormatter; +import org.apache.jena.riot.out.NodeFormatterTTL; +import org.linkeddatafragments.datasource.IDataSource; +import org.linkeddatafragments.datasource.index.IndexDataSource; +import org.linkeddatafragments.fragments.ILinkedDataFragment; +import org.linkeddatafragments.fragments.tpf.ITriplePatternElement; +import org.linkeddatafragments.fragments.tpf.ITriplePatternFragment; +import org.linkeddatafragments.fragments.tpf.ITriplePatternFragmentRequest; +import org.linkeddatafragments.views.ILinkedDataFragmentWriter; +import org.linkeddatafragments.views.TriplePatternFragmentWriterBase; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +//TODO: Refactor to a composable & flexible architecture using DataSource types, fragments types and request types + +/** + * Serializes an {@link ILinkedDataFragment} to the HTML format + * + * @author Miel Vander Sande + */ +public class HtmlTriplePatternFragmentWriterImpl extends TriplePatternFragmentWriterBase implements ILinkedDataFragmentWriter { + private final Configuration cfg; + + private final Template indexTemplate; + private final Template datasourceTemplate; + private final Template notfoundTemplate; + private final Template errorTemplate; + + private final String HYDRA = "http://www.w3.org/ns/hydra/core#"; + + private static String contextPath; + + public static void setContextPath(String path) { + contextPath = path; + if (!contextPath.endsWith("/")) { + contextPath += "/"; + } + } + + /** + * + * @param prefixes + * @param datasources + * @throws IOException + */ + public HtmlTriplePatternFragmentWriterImpl(Map prefixes, HashMap datasources) throws IOException { + super(prefixes, datasources); + + cfg = new Configuration(Configuration.VERSION_2_3_23); + cfg.setClassForTemplateLoading(getClass(), "/tpf"); + cfg.setDefaultEncoding("UTF-8"); + cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + + indexTemplate = cfg.getTemplate("index.ftl.html"); + datasourceTemplate = cfg.getTemplate("datasource.ftl.html"); + notfoundTemplate = cfg.getTemplate("notfound.ftl.html"); + errorTemplate = cfg.getTemplate("error.ftl.html"); + } + + /** + * + * @param outputStream + * @param datasource + * @param fragment + * @param tpfRequest + * @throws IOException + * @throws TemplateException + */ + @Override + public void writeFragment(ServletOutputStream outputStream, IDataSource datasource, ITriplePatternFragment fragment, ITriplePatternFragmentRequest tpfRequest) throws IOException, TemplateException{ + Map data = new HashMap(); + + // base.ftl.html + data.put("homePath", (contextPath != null ? contextPath : "") + "tpf"); + data.put("assetsPath", (contextPath != null ? contextPath : "") + "tpf/assets/"); + data.put("header", datasource.getTitle()); + data.put("date", new Date()); + + // fragment.ftl.html + data.put("datasourceUrl", tpfRequest.getDatasetURL()); + data.put("datasource", datasource); + + // Parse controls to template variables + StmtIterator controls = fragment.getControls(); + while (controls.hasNext()) { + Statement control = controls.next(); + + String predicate = control.getPredicate().getURI(); + RDFNode object = control.getObject(); + if (!object.isAnon()) { + String value = object.isURIResource() ? object.asResource().getURI() : object.asLiteral().getLexicalForm(); + data.put(predicate.replaceFirst(HYDRA, ""), value); + } + } + + // Add metadata + data.put("totalEstimate", fragment.getTotalSize()); + data.put("itemsPerPage", fragment.getMaxPageSize()); + + // Add triples and datasources + List triples = fragment.getTriples().toList(); + data.put("triples", triples); + data.put("datasources", getDatasources()); + + // Calculate start and end triple number + Long start = ((tpfRequest.getPageNumber() - 1) * fragment.getMaxPageSize()) + 1; + data.put("start", start); + data.put("end", start + (triples.size() < fragment.getMaxPageSize() ? triples.size() : fragment.getMaxPageSize())); + + // Compose query object + Map query = new HashMap(); + query.put("subject", !tpfRequest.getSubject().isVariable() ? handleCT(tpfRequest.getSubject().asConstantTerm()) : ""); + query.put("predicate", !tpfRequest.getPredicate().isVariable() ? handleCT(tpfRequest.getPredicate().asConstantTerm()) : ""); + query.put("object", !tpfRequest.getObject().isVariable() ? handleCT(tpfRequest.getObject().asConstantTerm()) : ""); + query.put("pattern", makeQueryPattern(tpfRequest)); + data.put("query", query); + + // Get the template (uses cache internally) + Template temp = datasource instanceof IndexDataSource ? indexTemplate : datasourceTemplate; + + // Merge data-model with template + temp.process(data, new OutputStreamWriter(outputStream)); + } + + private String makeQueryPattern(ITriplePatternFragmentRequest tpfRequest) { + StringBuilder pattern = new StringBuilder(); + + ITriplePatternElement subject = tpfRequest.getSubject(); + ITriplePatternElement predicate = tpfRequest.getPredicate(); + ITriplePatternElement object = tpfRequest.getObject(); + + pattern.append("{"); + + if ( ! subject.isVariable() ) { + appendNode(pattern.append(' '), subject.asConstantTerm()); + } else { + pattern.append(" ?s"); + } + + + if ( ! predicate.isVariable() ) { + appendNode(pattern.append(' '), predicate.asConstantTerm()); + } else { + pattern.append(" ?p"); + } + + if ( ! object.isVariable() ) { + appendNode(pattern.append(' '), object.asConstantTerm()); + } else { + pattern.append(" ?o"); + } + + pattern.append(" }"); + return pattern.toString(); + } + + private void appendNode(StringBuilder builder, RDFNode node) { + if (node.isLiteral()) { + builder.append(literalToString(node.asLiteral())); + } else if (node.isURIResource()) { + builder.append('<' + node.asResource().getURI() + '>'); + } + } + + private String literalToString(Literal l) { + StringWriterI sw = new StringWriterI(); + NodeFormatter fmt = new NodeFormatterTTL(null, null); + fmt.formatLiteral(sw, l.asNode()); + return sw.toString(); + } + + private Object handleCT(Object obj) { + if (obj instanceof LiteralImpl) { + return ((LiteralImpl)obj).asNode().toString(); + } + + return obj; + } + + @Override + public void writeNotFound(ServletOutputStream outputStream, HttpServletRequest request) throws Exception { + Map data = new HashMap(); + data.put("homePath", (contextPath != null ? contextPath : "") + "tpf"); + data.put("assetsPath", (contextPath != null ? contextPath : "") + "tpf/assets/"); + data.put("datasources", getDatasources()); + data.put("date", new Date()); + data.put("url", request.getRequestURL().toString()); + + notfoundTemplate.process(data, new OutputStreamWriter(outputStream)); + } + + @Override + public void writeError(ServletOutputStream outputStream, Exception ex) throws Exception { + Map data = new HashMap(); + data.put("homePath", (contextPath != null ? contextPath : "") + "tpf"); + data.put("assetsPath", (contextPath != null ? contextPath : "") + "tpf/assets/"); + data.put("date", new Date()); + data.put("error", ex); + + errorTemplate.process(data, new OutputStreamWriter(outputStream)); + } +} diff --git a/api/src/main/java/org/vivoweb/linkeddatafragments/views/LinkedDataFragmentWriterFactory.java b/api/src/main/java/org/vivoweb/linkeddatafragments/views/LinkedDataFragmentWriterFactory.java new file mode 100644 index 000000000..ebc933f9f --- /dev/null +++ b/api/src/main/java/org/vivoweb/linkeddatafragments/views/LinkedDataFragmentWriterFactory.java @@ -0,0 +1,36 @@ +package org.vivoweb.linkeddatafragments.views; + +import org.linkeddatafragments.datasource.IDataSource; +import org.linkeddatafragments.views.ILinkedDataFragmentWriter; +import org.linkeddatafragments.views.RdfWriterImpl; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * A factory for {@link ILinkedDataFragmentWriter}s. + * + * @author Miel Vander Sande + */ +public class LinkedDataFragmentWriterFactory { + + private final static String HTML = "text/html"; + + /** + * Creates {@link ILinkedDataFragmentWriter} for a given mimeType + * + * @param prefixes Configured prefixes to be used in serialization + * @param datasources Configured datasources + * @param mimeType mimeType to create writer for + * @return created writer + */ + public static ILinkedDataFragmentWriter create(Map prefixes, HashMap datasources, String mimeType) throws IOException { + switch (mimeType) { + case HTML: + return new HtmlTriplePatternFragmentWriterImpl(prefixes, datasources); + default: + return new RdfWriterImpl(prefixes, datasources, mimeType); + } + } +} diff --git a/api/src/main/resources/org/vivoweb/linkeddatafragments/servlet/favicon.ico b/api/src/main/resources/org/vivoweb/linkeddatafragments/servlet/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..65418193a24cd6f59b00198636351f7270b6dd41 GIT binary patch literal 318 zcmZQzU<5(|0RbS%!l1#(z#zuJz@P!d0zj+)#2|4HXaJKC0wf0l(z3D)k^bixltlM2 z{Qv(SB1i&h3~db%3#_M#y2to@)=;tsYa9FRIEw?&DS39 literal 0 HcmV?d00001 diff --git a/api/src/main/resources/org/vivoweb/linkeddatafragments/servlet/logo.svg b/api/src/main/resources/org/vivoweb/linkeddatafragments/servlet/logo.svg new file mode 100644 index 000000000..dce588f7e --- /dev/null +++ b/api/src/main/resources/org/vivoweb/linkeddatafragments/servlet/logo.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/api/src/main/resources/org/vivoweb/linkeddatafragments/servlet/style.css b/api/src/main/resources/org/vivoweb/linkeddatafragments/servlet/style.css new file mode 100644 index 000000000..4728a61f4 --- /dev/null +++ b/api/src/main/resources/org/vivoweb/linkeddatafragments/servlet/style.css @@ -0,0 +1,247 @@ +/*! @license ©2013 Ruben Verborgh - Multimedia Lab / iMinds / Ghent University */ + +html, input, th, td { + font-family: "Open Sans", Verdana, Arial, sans-serif; + font-size: 11pt; +} + +html { + background: #f6f6f6; +} + +body { + max-width: 800px; + margin: 0 auto; + line-height: 1.3; + color: #333333; + background-color: white; + padding: 10px 40px; + box-shadow: 2px 2px 15px 0px rgba(50, 50, 50, 0.75); +} + +h1, h2, h3, legend { + margin: .4em 0 .2em; + overflow: hidden; +} +h1 { + margin-right: 180px; +} +h1 a { + color: black; +} +h2 { + color: #be1622; +} +h3 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +p { + margin: 0; +} + +a { + color: #be1622; + text-decoration: none; + border-bottom: none 1px; +} +a:hover { + color: #be1622 !important; + border-bottom-style: solid; +} + +ul { + padding: 0; + margin: 0 0 .5em 1.5em; + list-style: none; +} + +pre { + margin: 0; +} + +form { + margin: 0 0 1.5em; +} + +fieldset { + border: none; + padding: .5em 0 0 20px; +} +fieldset ul { + margin-left: 0; +} +fieldset li { + line-height: 2em; +} + +legend { + font-size: 1.17em; + font-weight: bold; + padding: 0; + margin-left: -20px; +} + +label { + width: 100px; + display: block; + float: left; + clear: both; + font-weight: bold; +} +label:after { + content: ":"; +} + +input { + outline: none; + font-size: .95em; +} +fieldset input { + width: 500px; + color: #be1622; + background-color: transparent; + border: none; + border-bottom: 1px solid #bbbbbb; + cursor: pointer; +} +input[type=submit] { + font-weight: bold; + color: #be1622; + background-color: #f6f6f6; + border-radius: 3px; + padding: 5px 8px; + border: 1px solid #999999; + cursor: pointer; +} +input[type=submit]:hover { + border-color: #666666; +} +input[type=submit]:active { + padding: 6px 7px 4px 9px; +} +.uri { + font-family: "Droid Sans Mono", monospace; +} + +header .logo { + text-align: right; +} +header .logo a { + position: absolute; + top: 20px; + margin-left: -100px; + border-bottom-width: 0px; +} +header .logo img { + width: 160px; +} + +footer { + clear: both; + margin: 1.5em 0 .5em; + font-size: small; +} +footer * { + color: gray; + margin-right: 5px; +} + +.counts { + color: gray; +} +ul.links { + margin: 0; + padding: 0; + display: inline; +} +ul.links li { + display: inline; + padding-left: 20px; + font-weight: bold; +} + +ul.triples { + margin: .3em 0 1em 20px; + font-size: .95em; + line-height: 1.5; + font-family: "Droid Sans Mono", monospace; + overflow-x: hidden; +} +ul.triples li { + text-indent: -20px; + padding-left: 20px; + max-width: 100%; + max-height: 1.5em; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +ul.triples li:hover { + max-height: 100em; + white-space: normal; + transition: max-height .5s ease-in; + transition-delay: .5s; +} +ul.triples li:not(:hover) a { + color: inherit; +} +ul.triples a:nth-child(2) { + margin: 0 1em; +} +abbr { + border: none; +} + +.index { + margin-bottom: 2em; +} +.datasets { + margin: .5em 20px; +} +dt { + font-weight: bold; + display: block; + float: left; + clear: left; +} +dd { + color: gray; + margin: .1em 0 0 12em; + font-size: .95em; +} + +#about { + margin-top: 1.5em; + font-size: .9em; +} + +@media screen and (max-width: 700px) { + html, input, th, td { + font-size: 10pt; + } + body { + padding: 15px; + } + header figure { + display: none; + } + h1, legend { + margin: 0; + } + fieldset, ul.triples { + padding: .5em 0; + margin: 0; + } + fieldset input { + width: 70%; + } + label { + width: 80px; + } + ul.triples li { + margin: 1em 0; + } +} diff --git a/api/src/main/resources/tpf/base.ftl.html b/api/src/main/resources/tpf/base.ftl.html new file mode 100644 index 000000000..8952ccf1c --- /dev/null +++ b/api/src/main/resources/tpf/base.ftl.html @@ -0,0 +1,30 @@ +<#-- @license ©2015 Miel Vander Sande - Multimedia Lab / iMinds / Ghent University --> +<#macro display_page> + + + + + Linked Data Fragments Server ${ (title!header)?ensure_starts_with("(")?ensure_ends_with(")") } + + + + + +
+

Linked Data Fragments Server

+ +
+
+ <@contents/> +
+ + + + \ No newline at end of file diff --git a/api/src/main/resources/tpf/datasource.ftl.html b/api/src/main/resources/tpf/datasource.ftl.html new file mode 100644 index 000000000..97cd09985 --- /dev/null +++ b/api/src/main/resources/tpf/datasource.ftl.html @@ -0,0 +1,7 @@ +<#-- @license ©2015 Miel Vander Sande - Multimedia Lab / iMinds / Ghent University --> +<#assign title = datasource.getTitle() + ' | ' + title!""> +<#include "base.ftl.html"> +<#macro contents> +<#include "fragment.ftl.html"> + +<@display_page/> diff --git a/api/src/main/resources/tpf/error.ftl.html b/api/src/main/resources/tpf/error.ftl.html new file mode 100644 index 000000000..84ce65876 --- /dev/null +++ b/api/src/main/resources/tpf/error.ftl.html @@ -0,0 +1,11 @@ +<#-- @license ©2015 Miel Vander Sande - Multimedia Lab / iMinds / Ghent University --> +<#include "base.ftl.html"> +<#macro contents> +

Error executing your request

+

Your request could not be executed due to an internal server error.

+

Please try reloading the page or return to the index page.

+ +

Error details

+

<#if error??>${(error.getMessage())!error!""}

+ +<@display_page/> \ No newline at end of file diff --git a/api/src/main/resources/tpf/fragment.ftl.html b/api/src/main/resources/tpf/fragment.ftl.html new file mode 100644 index 000000000..32ce716bf --- /dev/null +++ b/api/src/main/resources/tpf/fragment.ftl.html @@ -0,0 +1,83 @@ +<#-- @license ©2015 Miel Vander Sande - Multimedia Lab / iMinds / Ghent University --> +<#setting url_escaping_charset='UTF-8'> +

+

${datasource.getTitle()?cap_first}

+ +
+
+ Query ${datasource.getTitle()} by triple pattern +
    +<#list ['subject', 'predicate', 'object'] as component> +
  • + + +
  • + +
+
+

+ +

+
+
+ +

Matches in ${datasource.getTitle()} for ${ (query["pattern"]?html)!"" }

+ +
+<#if (triples?size > 0)> + Showing triples ${ start } to ${ end } of + <#if totalEstimate != end>± + ${ totalEstimate } + with ${ + itemsPerPage + } triples per page. + <@pageLinks/> +<#else> +

+ ${datasource.getTitle()} contains + + no <#if (totalEstimate > 0) >more + + triples that match this pattern. +

+ +
+ + + +<@pageLinks/> + +<#macro pageLinks> + + \ No newline at end of file diff --git a/api/src/main/resources/tpf/index.ftl.html b/api/src/main/resources/tpf/index.ftl.html new file mode 100644 index 000000000..568db4ab5 --- /dev/null +++ b/api/src/main/resources/tpf/index.ftl.html @@ -0,0 +1,21 @@ +<#-- @license ©2015 Miel Vander Sande - Multimedia Lab / iMinds / Ghent University --> +<#include "base.ftl.html"> +<#macro contents> +
+

Available datasets

+

Browse the following datasets as Triple Pattern Fragments:

+
+ <#if datasources??> + <#list datasources?keys as datasourceName> +
${datasources[datasourceName].getTitle() }
+
${ datasources[datasourceName].getDescription()!"" }
+ + +
+

The current dataset index contains metadata about these datasets.

+
+ +<#include "fragment.ftl.html"> + + +<@display_page/> \ No newline at end of file diff --git a/api/src/main/resources/tpf/notfound.ftl.html b/api/src/main/resources/tpf/notfound.ftl.html new file mode 100644 index 000000000..363b8abd9 --- /dev/null +++ b/api/src/main/resources/tpf/notfound.ftl.html @@ -0,0 +1,16 @@ +<#-- @license ©2015 Miel Vander Sande - Multimedia Lab / iMinds / Ghent University --> +<#include "base.ftl.html"> +<#macro contents> +

Resource not found

+

+ No resource with URL ${ url!"" } was found. +

+ +

Available datasets

+ + +<@display_page/> \ No newline at end of file diff --git a/dependencies/pom.xml b/dependencies/pom.xml index df30c3970..856f37d30 100644 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -284,6 +284,11 @@ antisamy 1.5.3 + + com.google.code.gson + gson + 2.5 +