Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 243 additions & 0 deletions src/main/java/life/qbic/compass/Compass.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
package life.qbic.compass;

import life.qbic.compass.model.SignPostingResult;
import life.qbic.compass.parsing.LinkSetInlineParser;
import life.qbic.compass.parsing.LinkSetJsonParser;
import life.qbic.compass.spi.LinkSetParser;
import life.qbic.compass.spi.SignPostingValidator;
import life.qbic.compass.validation.Level1SignPostingValidator;
import life.qbic.compass.validation.Level2ContentResourceValidator;
import life.qbic.compass.validation.Level2LandingPageValidator;
import life.qbic.compass.validation.Level2MetadataResourceValidator;
import life.qbic.compass.validation.Level2RecipeValidator;
import life.qbic.linksmith.model.WebLink;

/**
* Primary entry point and facade for the Compass FAIR Signposting library.
*
* <p>
* {@code Compass} provides a single, discoverable access point to the library’s
* <strong>default parsers</strong> and <strong>default validators</strong>.
* It is intentionally lightweight and stateless, exposing only factory-style
* accessors grouped by concern.
* </p>
*
* <h2>Design goals</h2>
* <ul>
* <li>
* <strong>Discoverability</strong> – clients can find all supported parsing
* and validation capabilities starting from one class.
* </li>
* <li>
* <strong>Separation of concerns</strong> – parsing and validation are exposed
* independently and are not coupled to each other.
* </li>
* <li>
* <strong>Non-opinionated usage</strong> – clients may use parsers without validators,
* validators without parsers, or supply their own implementations via the SPI.
* </li>
* <li>
* <strong>Stable defaults</strong> – this class exposes ready-to-use default
* implementations that follow the FAIR Signposting specification.
* </li>
* </ul>
*
* <h2>Typical usage</h2>
*
* <pre>{@code
* // Parse a Level 2 Link Set
* var parser = Compass.parsers().linksetJson();
* List<WebLink> links = parser.parse(inputStream);
*
* // Validate using Level 2 recipe routing
* var validator = Compass.validators().level2Recipes();
* SignPostingResult result = validator.validate(links);
* }</pre>
*
* <p>
* {@code Compass} itself performs no parsing or validation logic. All stateful
* behavior is delegated to the returned parser and validator instances.
* </p>
*
* <h2>Extensibility</h2>
* <p>
* Advanced users may bypass this facade entirely and work directly with:
* </p>
* <ul>
* <li>{@link LinkSetParser} implementations</li>
* <li>{@link SignPostingValidator} implementations</li>
* </ul>
*
* <p>
* This class exists solely as a convenience and does not restrict extensibility
* or customization.
* </p>
*
* @since 1.0.0
* @author Sven Fillinger
*/
public final class Compass {

private Compass() {}

/**
* Provides access to the default FAIR Signposting parsers.
*
* <p>
* The returned object acts as a logical namespace grouping all supported
* {@link LinkSetParser} implementations shipped with this library.
* </p>
*
* @return a singleton accessor for parser factories
*/
public static Parsers parsers() {
return Parsers.INSTANCE;
}

/**
* Provides access to the default FAIR Signposting validators.
*
* <p>
* The returned object acts as a logical namespace grouping all supported
* {@link SignPostingValidator} implementations shipped with this library.
* </p>
*
* @return a singleton accessor for validator factories
*/
public static Validators validators() {
return Validators.INSTANCE;
}

/**
* Namespace for FAIR Signposting parser factories.
*
* <p>
* Parsers are responsible for transforming serialized representations
* (e.g. HTTP {@code Link} headers, Link Set JSON) into {@link WebLink}
* objects suitable for validation and inspection.
* </p>
*
* <p>
* All parsers returned by this class are stateless and may be reused
* across multiple parsing operations.
* </p>
*/
public static final class Parsers {

private static final Parsers INSTANCE = new Parsers();

private Parsers() {}

/**
* Creates a parser for RFC&nbsp;9264 JSON Link Sets.
*
* <p>
* This parser is intended for FAIR Signposting Level&nbsp;2 discovery,
* where links for multiple resource contexts are provided together
* in a Link Set document.
* </p>
*
* @return a {@link LinkSetParser} for JSON Link Sets
*/
public LinkSetParser linksetJson() {
return LinkSetJsonParser.create();
}

/**
* Creates a parser for inline {@code Link} headers and HTML {@code <link>} elements.
*
* <p>
* This parser is typically used for FAIR Signposting Level&nbsp;1,
* where typed links are provided directly with the resource response.
* </p>
*
* @return a {@link LinkSetParser} for inline link representations
*/
public LinkSetParser linksetInline() {
return LinkSetInlineParser.create();
}
}

/**
* Namespace for FAIR Signposting validator factories.
*
* <p>
* Validators operate on parsed {@link WebLink} collections and produce
* {@link SignPostingResult} instances
* describing validation outcomes.
* </p>
*
* <p>
* Validators returned by this class are independent and stateless;
* clients may freely compose or replace them.
* </p>
*/
public static final class Validators {

private static final Validators INSTANCE = new Validators();

private Validators() {}

/**
* Creates a validator for FAIR Signposting Level&nbsp;1.
*
* <p>
* This validator enforces the <em>mandatory</em> Level&nbsp;1 landing page
* recipe and emits warnings for recommended relations.
* </p>
*
* @return a Level&nbsp;1 {@link SignPostingValidator}
*/
public SignPostingValidator level1() {
return Level1SignPostingValidator.create();
}

/**
* Creates a validator that routes FAIR Signposting Level&nbsp;2 recipes
* automatically per origin (anchor).
*
* <p>
* This validator detects landing page, metadata resource, and content
* resource recipes using heuristics and delegates validation to the
* appropriate specialized validator.
* </p>
*
* @return a Level&nbsp;2 recipe-routing {@link SignPostingValidator}
*/
public SignPostingValidator level2Recipes() {
return Level2RecipeValidator.create();
}

/**
* Creates a validator for the FAIR Signposting Level&nbsp;2 landing page recipe.
*
* <p>
* This validator should be used when the landing page context is known
* a priori and no heuristic routing is required.
* </p>
*
* @return a landing page {@link SignPostingValidator}
*/
public SignPostingValidator level2LandingPage() {
return Level2LandingPageValidator.create();
}

/**
* Creates a validator for the FAIR Signposting Level&nbsp;2 metadata resource recipe.
*
* @return a metadata resource {@link SignPostingValidator}
*/
public SignPostingValidator level2MetadataResource() {
return Level2MetadataResourceValidator.create();
}

/**
* Creates a validator for the FAIR Signposting Level&nbsp;2 content resource recipe.
*
* @return a content resource {@link SignPostingValidator}
*/
public SignPostingValidator level2ContentResource() {
return Level2ContentResourceValidator.create();
}
}
}
99 changes: 98 additions & 1 deletion src/main/java/life/qbic/compass/SignPostingProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,31 +61,128 @@ private SignPostingProcessor(List<SignPostingValidator> validators) {
this.validators = List.copyOf(validators);
}

/**
* Applies all configured {@link SignPostingValidator}s to the provided WebLinks
* and aggregates their reported issues into a single {@link SignPostingResult}.
*
* <p>
* Each validator is executed independently and receives the <em>same</em>
* input list. Validators are not allowed to mutate the input.
* </p>
*
* <h3>Aggregation semantics</h3>
* <ul>
* <li>All validators are executed in the order they were configured.</li>
* <li>All {@link life.qbic.linksmith.spi.WebLinkValidator.Issue}s from all
* validators are collected and merged into a single {@link IssueReport}.</li>
* <li>No short-circuiting occurs: even if one validator reports errors,
* subsequent validators are still executed.</li>
* </ul>
*
* <h3>View semantics</h3>
* <p>
* The processor is <strong>non-destructive with respect to non-null links</strong>.
* It does not reorder or modify WebLinks. However, {@code null} elements are
* <strong>filtered out</strong> before validation and before creating the returned
* {@link SignPostingView}, because {@code null} values cannot be represented safely
* in the view API.
* </p>
*
* <p>
* As a result, the returned {@link SignPostingView} contains all non-null WebLinks
* from the input list, in their original order.
* </p>
* This processor intentionally does <em>not</em> merge or expose any
* {@link life.qbic.compass.model.Level2LinksetView} instances returned by
* individual validators. If Level&nbsp;2 structural views are required,
* clients should invoke the corresponding validator directly
* (e.g. {@code Level2RecipeValidator}).
* </p>
*
* <h3>Error handling</h3>
* <ul>
* <li>{@code webLinks} must not be {@code null}.</li>
* <li>{@code webLinks} may contain {@code null} elements. Null elements are skipped.</li>
* </ul>
*
* @param webLinks the WebLinks to be validated
* @return a {@link SignPostingResult} containing the aggregated issues and
* a {@link SignPostingView} over the input links
* @throws NullPointerException if {@code webLinks} is {@code null}
*/
public SignPostingResult process(List<WebLink> webLinks) throws NullPointerException {
Objects.requireNonNull(webLinks);
var safeLinks = webLinks.stream()
.filter(Objects::nonNull)
.toList();

var recordedIssues = validators.stream()
.map(validator -> validator.validate(webLinks))
.map(validator -> validator.validate(safeLinks))
.map(SignPostingResult::issueReport)
.flatMap(report -> report.issues().stream())
.toList();

return new SignPostingResult(new SignPostingView(webLinks), new IssueReport(recordedIssues), null);
}

/**
* Builder for constructing a {@link SignPostingProcessor} with a configurable
* set of {@link SignPostingValidator}s.
*
* <p>
* Validators are executed in the order they are added to the builder.
* </p>
*
* <p>
* If no validators are explicitly configured, the processor defaults to
* using {@link Level1SignPostingValidator}.
* </p>
*/
public static final class Builder {

private List<SignPostingValidator> validators = new ArrayList<>();

/**
* Adds one or more validators to this processor.
*
* <p>
* Validators are appended in the order provided.
* </p>
*
* @param validators one or more {@link SignPostingValidator}s
* @return this builder for fluent chaining
* @throws NullPointerException if {@code validators} is {@code null}
*/
Builder withValidators(SignPostingValidator... validators) {
return this.withValidators(Arrays.stream(validators).toList());
}

/**
* Adds a list of validators to this processor.
*
* <p>
* The provided list is not defensively copied until {@link #build()} is called.
* </p>
*
* @param validators validators to add
* @return this builder for fluent chaining
* @throws NullPointerException if {@code validators} is {@code null}
*/
Builder withValidators(List<SignPostingValidator> validators) {
this.validators.addAll(validators);
return this;
}

/**
* Builds a {@link SignPostingProcessor} instance.
*
* <p>
* If no validators were added, the processor is created with a single
* {@link Level1SignPostingValidator} as a sensible default.
* </p>
*
* @return a configured {@link SignPostingProcessor}
*/
SignPostingProcessor build() {
if (validators.isEmpty()) {
return new SignPostingProcessor(List.of(Level1SignPostingValidator.create()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import java.io.InputStream;
import java.io.Reader;
import java.nio.charset.Charset;
import java.util.List;
import life.qbic.compass.spi.LinkSetParser;
import life.qbic.linksmith.model.WebLink;
Expand All @@ -12,7 +11,13 @@
*
* @since <version tag>
*/
public class LinkSetInlineParser implements LinkSetParser {
public final class LinkSetInlineParser implements LinkSetParser {

private LinkSetInlineParser() {}

public static LinkSetInlineParser create() {
return new LinkSetInlineParser();
}

@Override
public List<WebLink> parse(String rawLinkSet) throws ParsingException {
Expand Down
Loading
Loading