Developer Guide
This guide documents internal design decisions and extension points for developers working on SPARQL Anything itself (as opposed to users writing SPARQL Anything queries). Most sections are still TODO — contributions welcome.
Table of contents
- Reading input resources: the service-provider pattern
- Triplifier architecture — TODO
- Facade-X graph builders — TODO
- The query execution pipeline (
FacadeX.ExecutorFactory) — TODO - Functions and magic properties — TODO
- The plugin loading mechanism — TODO
- Module layout and build — TODO
- Testing conventions — TODO
Reading input resources: the service-provider pattern
Triplifier.getInputStream(Properties) is the single entry point every triplifier uses to obtain an InputStream for the resource it needs to read, regardless of where that resource actually comes from (a local file, an HTTP(S) URL, inline content, the output of a shell command, an entry inside an archive, or an S3 object). Internally, this is implemented as a service-provider (SPI) pattern rather than one large method handling every input source inline.
ResourceService
The core abstraction is the ResourceService interface (io.github.sparqlanything.model.resources.ResourceService, module sparql-anything-model):
public interface ResourceService {
InputStream getInputStream(Properties properties) throws IOException, TriplifierHTTPException;
}
Each input source is implemented as one ResourceService. It receives the full Properties of the current query/triplifier invocation and is responsible for returning a readable InputStream for that source, including any resource cleanup that must happen once the caller closes the stream.
@TargetOption
A ResourceService implementation is annotated with @TargetOption, giving the IRIArgument key (as a raw string) that identifies when this service should be used:
@TargetOption(IRIArgument.LOCATION_NAME)
public class LocationInputService implements ResourceService { ... }
Every IRIArgument constant has a companion ..._NAME string constant (e.g. LOCATION_NAME, COMMAND_NAME, CONTENT_NAME, S3_ENDPOINT_NAME) precisely so that @TargetOption values, and any other code that needs the raw property key, don't need to depend on the full IRIArgument object.
Built-in implementations
sparql-anything-model ships three built-in services:
| Service | @TargetOption |
Handles |
|---|---|---|
LocationInputService |
IRIArgument.LOCATION_NAME |
file://, http(s)://, other URL protocols, and archive-extraction (from-archive) |
CommandInputService |
IRIArgument.COMMAND_NAME |
Running a shell command and returning its stdout |
ContentInputService |
IRIArgument.CONTENT_NAME |
Wrapping an inline content property value as a stream |
Additional services can live in their own modules — see S3 support for sparql-anything-s3-support's S3InputService, which reads objects from an S3-compatible bucket and is discovered exactly the same way, without the engine having any hardcoded reference to S3 or the AWS SDK.
Discovery via java.util.ServiceLoader
Implementations are registered using the standard Java ServiceLoader mechanism: each module contributes a file at
META-INF/services/io.github.sparqlanything.model.resources.ResourceService
listing the fully-qualified class name(s) of its ResourceService implementation(s), one per line. For example, sparql-anything-model's file lists:
io.github.sparqlanything.model.resources.LocationInputService
io.github.sparqlanything.model.resources.CommandInputService
io.github.sparqlanything.model.resources.ContentInputService
Triplifier.loadResourceServicesAll() loads every registered implementation via ServiceLoader.load(ResourceService.class), and builds a Map<String, ResourceService> keyed by each implementation's @TargetOption value. A duplicate key (two implementations registered for the same option) is treated as a configuration error and throws IllegalStateException.
Note for implementers: the registration file must live under
META-INF/services/, not simplyservices/, orServiceLoaderwill silently fail to find it — there is no error, the implementation is just never discovered.
Dispatch
Triplifier.getInputStream(Properties) is a thin dispatcher: it checks a fixed priority list of options —
IRIArgument[] options = {IRIArgument.S3_ENDPOINT, IRIArgument.COMMAND, IRIArgument.CONTENT, IRIArgument.LOCATION};
— and delegates to whichever registered ResourceService matches the first option present in the given Properties. Adding support for a new input source is therefore a matter of implementing ResourceService, annotating it with the right @TargetOption, registering it via ServiceLoader, and adding its IRIArgument to this priority list if it introduces a new option.
Root minting for non-location sources
Triplifier.getRootArgument(Properties) mints the root node of the generated Facade-X graph. For location-based resources, the root is derived from the (normalised) resource URL. Sources that don't have a natural URL need their own root-minting logic in this same method — for example:
contentandcommand: the root isXYZ_NSplus an MD5 hash of the content/command string, since there's no natural identifier to use.- S3 (
s3.endpoint): the root is the concatenation ofs3.endpoint,s3.bucket-name, ands3.key(e.g.http://localhost:9000/my-bucket/people.json), so that two different objects served from the same endpoint are always minted with distinct roots.
Any new non-location ResourceService should extend getRootArgument similarly, ensuring the chosen root is unique for the combination of properties that identifies the resource.