diff --git a/src/main/java/life/qbic/compass/Compass.java b/src/main/java/life/qbic/compass/Compass.java new file mode 100644 index 0000000..8279f09 --- /dev/null +++ b/src/main/java/life/qbic/compass/Compass.java @@ -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. + * + *

+ * {@code Compass} provides a single, discoverable access point to the library’s + * default parsers and default validators. + * It is intentionally lightweight and stateless, exposing only factory-style + * accessors grouped by concern. + *

+ * + *

Design goals

+ * + * + *

Typical usage

+ * + *
{@code
+ * // Parse a Level 2 Link Set
+ * var parser = Compass.parsers().linksetJson();
+ * List links = parser.parse(inputStream);
+ *
+ * // Validate using Level 2 recipe routing
+ * var validator = Compass.validators().level2Recipes();
+ * SignPostingResult result = validator.validate(links);
+ * }
+ * + *

+ * {@code Compass} itself performs no parsing or validation logic. All stateful + * behavior is delegated to the returned parser and validator instances. + *

+ * + *

Extensibility

+ *

+ * Advanced users may bypass this facade entirely and work directly with: + *

+ * + * + *

+ * This class exists solely as a convenience and does not restrict extensibility + * or customization. + *

+ * + * @since 1.0.0 + * @author Sven Fillinger + */ +public final class Compass { + + private Compass() {} + + /** + * Provides access to the default FAIR Signposting parsers. + * + *

+ * The returned object acts as a logical namespace grouping all supported + * {@link LinkSetParser} implementations shipped with this library. + *

+ * + * @return a singleton accessor for parser factories + */ + public static Parsers parsers() { + return Parsers.INSTANCE; + } + + /** + * Provides access to the default FAIR Signposting validators. + * + *

+ * The returned object acts as a logical namespace grouping all supported + * {@link SignPostingValidator} implementations shipped with this library. + *

+ * + * @return a singleton accessor for validator factories + */ + public static Validators validators() { + return Validators.INSTANCE; + } + + /** + * Namespace for FAIR Signposting parser factories. + * + *

+ * 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. + *

+ * + *

+ * All parsers returned by this class are stateless and may be reused + * across multiple parsing operations. + *

+ */ + public static final class Parsers { + + private static final Parsers INSTANCE = new Parsers(); + + private Parsers() {} + + /** + * Creates a parser for RFC 9264 JSON Link Sets. + * + *

+ * This parser is intended for FAIR Signposting Level 2 discovery, + * where links for multiple resource contexts are provided together + * in a Link Set document. + *

+ * + * @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 } elements. + * + *

+ * This parser is typically used for FAIR Signposting Level 1, + * where typed links are provided directly with the resource response. + *

+ * + * @return a {@link LinkSetParser} for inline link representations + */ + public LinkSetParser linksetInline() { + return LinkSetInlineParser.create(); + } + } + + /** + * Namespace for FAIR Signposting validator factories. + * + *

+ * Validators operate on parsed {@link WebLink} collections and produce + * {@link SignPostingResult} instances + * describing validation outcomes. + *

+ * + *

+ * Validators returned by this class are independent and stateless; + * clients may freely compose or replace them. + *

+ */ + public static final class Validators { + + private static final Validators INSTANCE = new Validators(); + + private Validators() {} + + /** + * Creates a validator for FAIR Signposting Level 1. + * + *

+ * This validator enforces the mandatory Level 1 landing page + * recipe and emits warnings for recommended relations. + *

+ * + * @return a Level 1 {@link SignPostingValidator} + */ + public SignPostingValidator level1() { + return Level1SignPostingValidator.create(); + } + + /** + * Creates a validator that routes FAIR Signposting Level 2 recipes + * automatically per origin (anchor). + * + *

+ * This validator detects landing page, metadata resource, and content + * resource recipes using heuristics and delegates validation to the + * appropriate specialized validator. + *

+ * + * @return a Level 2 recipe-routing {@link SignPostingValidator} + */ + public SignPostingValidator level2Recipes() { + return Level2RecipeValidator.create(); + } + + /** + * Creates a validator for the FAIR Signposting Level 2 landing page recipe. + * + *

+ * This validator should be used when the landing page context is known + * a priori and no heuristic routing is required. + *

+ * + * @return a landing page {@link SignPostingValidator} + */ + public SignPostingValidator level2LandingPage() { + return Level2LandingPageValidator.create(); + } + + /** + * Creates a validator for the FAIR Signposting Level 2 metadata resource recipe. + * + * @return a metadata resource {@link SignPostingValidator} + */ + public SignPostingValidator level2MetadataResource() { + return Level2MetadataResourceValidator.create(); + } + + /** + * Creates a validator for the FAIR Signposting Level 2 content resource recipe. + * + * @return a content resource {@link SignPostingValidator} + */ + public SignPostingValidator level2ContentResource() { + return Level2ContentResourceValidator.create(); + } + } +} diff --git a/src/main/java/life/qbic/compass/SignPostingProcessor.java b/src/main/java/life/qbic/compass/SignPostingProcessor.java index 9595818..cebe657 100644 --- a/src/main/java/life/qbic/compass/SignPostingProcessor.java +++ b/src/main/java/life/qbic/compass/SignPostingProcessor.java @@ -61,11 +61,63 @@ private SignPostingProcessor(List 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}. + * + *

+ * Each validator is executed independently and receives the same + * input list. Validators are not allowed to mutate the input. + *

+ * + *

Aggregation semantics

+ * + * + *

View semantics

+ *

+ * The processor is non-destructive with respect to non-null links. + * It does not reorder or modify WebLinks. However, {@code null} elements are + * filtered out before validation and before creating the returned + * {@link SignPostingView}, because {@code null} values cannot be represented safely + * in the view API. + *

+ * + *

+ * As a result, the returned {@link SignPostingView} contains all non-null WebLinks + * from the input list, in their original order. + *

+ * This processor intentionally does not merge or expose any + * {@link life.qbic.compass.model.Level2LinksetView} instances returned by + * individual validators. If Level 2 structural views are required, + * clients should invoke the corresponding validator directly + * (e.g. {@code Level2RecipeValidator}). + *

+ * + *

Error handling

+ * + * + * @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 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(); @@ -73,19 +125,64 @@ public SignPostingResult process(List webLinks) throws NullPointerExcep return new SignPostingResult(new SignPostingView(webLinks), new IssueReport(recordedIssues), null); } + /** + * Builder for constructing a {@link SignPostingProcessor} with a configurable + * set of {@link SignPostingValidator}s. + * + *

+ * Validators are executed in the order they are added to the builder. + *

+ * + *

+ * If no validators are explicitly configured, the processor defaults to + * using {@link Level1SignPostingValidator}. + *

+ */ public static final class Builder { private List validators = new ArrayList<>(); + /** + * Adds one or more validators to this processor. + * + *

+ * Validators are appended in the order provided. + *

+ * + * @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. + * + *

+ * The provided list is not defensively copied until {@link #build()} is called. + *

+ * + * @param validators validators to add + * @return this builder for fluent chaining + * @throws NullPointerException if {@code validators} is {@code null} + */ Builder withValidators(List validators) { this.validators.addAll(validators); return this; } + /** + * Builds a {@link SignPostingProcessor} instance. + * + *

+ * If no validators were added, the processor is created with a single + * {@link Level1SignPostingValidator} as a sensible default. + *

+ * + * @return a configured {@link SignPostingProcessor} + */ SignPostingProcessor build() { if (validators.isEmpty()) { return new SignPostingProcessor(List.of(Level1SignPostingValidator.create())); diff --git a/src/main/java/life/qbic/compass/parsing/LinkSetInlineParser.java b/src/main/java/life/qbic/compass/parsing/LinkSetInlineParser.java index 20684f7..e6bfad9 100644 --- a/src/main/java/life/qbic/compass/parsing/LinkSetInlineParser.java +++ b/src/main/java/life/qbic/compass/parsing/LinkSetInlineParser.java @@ -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; @@ -12,7 +11,13 @@ * * @since */ -public class LinkSetInlineParser implements LinkSetParser { +public final class LinkSetInlineParser implements LinkSetParser { + + private LinkSetInlineParser() {} + + public static LinkSetInlineParser create() { + return new LinkSetInlineParser(); + } @Override public List parse(String rawLinkSet) throws ParsingException { diff --git a/src/main/java/life/qbic/compass/parsing/LinkSetJsonParser.java b/src/main/java/life/qbic/compass/parsing/LinkSetJsonParser.java index 9294a7a..156e275 100644 --- a/src/main/java/life/qbic/compass/parsing/LinkSetJsonParser.java +++ b/src/main/java/life/qbic/compass/parsing/LinkSetJsonParser.java @@ -48,7 +48,7 @@ * * @author Sven Fillinger */ -public class LinkSetJsonParser implements LinkSetParser { +public final class LinkSetJsonParser implements LinkSetParser { /** * Creates a new {@link LinkSetJsonParser}. diff --git a/src/main/java/life/qbic/compass/spi/SignPostingValidator.java b/src/main/java/life/qbic/compass/spi/SignPostingValidator.java index fc0ce10..546adfd 100644 --- a/src/main/java/life/qbic/compass/spi/SignPostingValidator.java +++ b/src/main/java/life/qbic/compass/spi/SignPostingValidator.java @@ -5,32 +5,66 @@ import life.qbic.linksmith.model.WebLink; /** - * Validates a collection of {@link WebLink}s against a specific Signposting profile - * or profile level. + * Validates a collection of {@link WebLink}s against a specific FAIR Signposting profile or profile + * level. + * *

* Implementations of this interface perform semantic checks on already parsed and - * RFC 8288–compliant WebLinks. They do not perform HTTP requests, content - * negotiation, or dereferencing of link targets. + * RFC 8288–compliant WebLinks. Validators do not perform HTTP requests, + * content negotiation, dereferencing of link targets, or parsing of {@code Link} headers / Link + * Sets. *

* - *

- * A {@code SignPostingValidator}: - *

+ *

Responsibilities

+ *
    + *
  • Evaluate the presence, cardinality, and relationships of Signposting link relations.
  • + *
  • Collect detected violations and recommendations as validation issues.
  • + *
  • Return a {@link SignPostingResult} that contains a {@link life.qbic.compass.model.SignPostingView} + * and an {@link life.qbic.linksmith.spi.WebLinkValidator.IssueReport}.
  • + *
+ * + *

Contract

+ *

Purity / side effects

+ *
    + *
  • Validators must be side-effect free.
  • + *
  • Validators must not mutate the supplied {@code webLinks} list or any contained {@link WebLink}.
  • + *
+ * + *

Null handling

+ *
    + *
  • The {@code webLinks} argument itself must not be {@code null}. + * Implementations should throw {@link NullPointerException} if it is {@code null}.
  • + *
  • The {@code webLinks} list may contain {@code null} elements. + * Implementations must not throw due to null elements. + * Instead, they must: + *
      + *
    • ignore {@code null} entries for validation logic, and
    • + *
    • record at least one {@code ERROR} issue describing the presence of {@code null} elements + * (best practice: include the index).
    • + *
    + *
  • + *
+ * + *

Return value requirements

*
    - *
  • evaluates the presence, cardinality, and relationships of link relations,
  • - *
  • collects all detected violations and recommendations as validation issues, and
  • - *
  • returns a {@link SignPostingResult} containing both a semantic view and an issue report.
  • + *
  • {@link #validate(List)} must return a non-null {@link SignPostingResult}.
  • + *
  • The returned {@link SignPostingResult#issueReport()} must be non-null.
  • + *
  • The returned {@link SignPostingResult#signPostingView()} must be non-null and must not contain + * {@code null} WebLinks.
  • *
* *

- * Validators must be side effect free and should not modify the - * supplied list of WebLinks. + * Different validators may target different Signposting levels or recipes + * (e.g. Level 1, Level 2 Landing Page / Metadata Resource / Content Resource, + * or Level 2 discovery) and can be applied independently or in sequence by client code. *

* *

- * Different validators may target different Signposting levels or profiles - * (e.g. Level 1, Level 2 discovery, or domain-specific extensions) and - * can be applied independently or in sequence by client code. + * Validators that operate on Level 2 Link Sets (i.e. collections of links describing + * multiple resource origins) may populate the optional + * {@link SignPostingResult#level2LinksetView()}. + * If present, it provides a structured, recipe-aware representation of the validated + * Link Set as a {@link life.qbic.compass.model.Level2LinksetView}. *

*/ public interface SignPostingValidator { diff --git a/src/main/java/life/qbic/compass/validation/Level1SignPostingValidator.java b/src/main/java/life/qbic/compass/validation/Level1SignPostingValidator.java index 632e59d..ecef617 100644 --- a/src/main/java/life/qbic/compass/validation/Level1SignPostingValidator.java +++ b/src/main/java/life/qbic/compass/validation/Level1SignPostingValidator.java @@ -68,7 +68,7 @@ * * @author Sven Fillinger */ -public class Level1SignPostingValidator implements SignPostingValidator { +public final class Level1SignPostingValidator implements SignPostingValidator { private Level1SignPostingValidator() { } diff --git a/src/main/java/life/qbic/compass/validation/Level2ContentResourceValidator.java b/src/main/java/life/qbic/compass/validation/Level2ContentResourceValidator.java index 0012c33..56bb9f7 100644 --- a/src/main/java/life/qbic/compass/validation/Level2ContentResourceValidator.java +++ b/src/main/java/life/qbic/compass/validation/Level2ContentResourceValidator.java @@ -66,7 +66,7 @@ * @since 1.0.0 * @author Sven Fillinger */ -public class Level2ContentResourceValidator implements SignPostingValidator { +public final class Level2ContentResourceValidator implements SignPostingValidator { /** Relation type {@code cite-as} used by FAIR Signposting. */ public static final String CITE_AS = "cite-as"; diff --git a/src/main/java/life/qbic/compass/validation/Level2DiscoveryValidator.java b/src/main/java/life/qbic/compass/validation/Level2DiscoveryValidator.java index ef6fa8c..b39d045 100644 --- a/src/main/java/life/qbic/compass/validation/Level2DiscoveryValidator.java +++ b/src/main/java/life/qbic/compass/validation/Level2DiscoveryValidator.java @@ -62,7 +62,7 @@ * * @author Sven Fillinger */ -public class Level2DiscoveryValidator implements SignPostingValidator { +public final class Level2DiscoveryValidator implements SignPostingValidator { private Level2DiscoveryValidator() { } diff --git a/src/main/java/life/qbic/compass/validation/Level2LandingPageValidator.java b/src/main/java/life/qbic/compass/validation/Level2LandingPageValidator.java index 30bfc19..a4b38ad 100644 --- a/src/main/java/life/qbic/compass/validation/Level2LandingPageValidator.java +++ b/src/main/java/life/qbic/compass/validation/Level2LandingPageValidator.java @@ -62,7 +62,7 @@ * @author Sven Fillinger * @since 1.0.0 */ -public class Level2LandingPageValidator implements SignPostingValidator { +public final class Level2LandingPageValidator implements SignPostingValidator { /** * Relation type {@code cite-as}. diff --git a/src/main/java/life/qbic/compass/validation/Level2MetadataResourceValidator.java b/src/main/java/life/qbic/compass/validation/Level2MetadataResourceValidator.java index 724418e..0a51537 100644 --- a/src/main/java/life/qbic/compass/validation/Level2MetadataResourceValidator.java +++ b/src/main/java/life/qbic/compass/validation/Level2MetadataResourceValidator.java @@ -75,7 +75,7 @@ * @author Sven Fillinger * @since 1.0.0 */ -public class Level2MetadataResourceValidator implements SignPostingValidator { +public final class Level2MetadataResourceValidator implements SignPostingValidator { /** * The relation type required by the Level 2 metadata resource recipe. diff --git a/src/main/java/life/qbic/compass/validation/Level2RecipeValidator.java b/src/main/java/life/qbic/compass/validation/Level2RecipeValidator.java index 98c51b8..3e9c1dd 100644 --- a/src/main/java/life/qbic/compass/validation/Level2RecipeValidator.java +++ b/src/main/java/life/qbic/compass/validation/Level2RecipeValidator.java @@ -14,8 +14,8 @@ import life.qbic.compass.model.Level2LinksetView; import life.qbic.compass.model.MetadataResourceView; import life.qbic.compass.model.MissingOriginLink; -import life.qbic.compass.model.SignPostingView; import life.qbic.compass.model.SignPostingResult; +import life.qbic.compass.model.SignPostingView; import life.qbic.compass.spi.SignPostingValidator; import life.qbic.linksmith.model.WebLink; import life.qbic.linksmith.spi.WebLinkValidator.Issue; @@ -111,6 +111,27 @@ * * * + *

What can change in the future

+ *

+ * The following aspects are not considered part of the stable API contract and may + * change between releases without notice: + *

+ *
    + *
  • + * The heuristic used to determine which recipe validator to apply for a given origin context. + *
  • + *
  • + * The exact wording of {@link Issue#message()} values. + * Clients must rely on issue {@code type} and documented validation rules, not message text. + *
  • + *
  • + * The ordering of reported issues. + *
  • + *
  • + * The severity classification (ERROR vs WARNING) for non-normative profile recommendations. + *
  • + *
+ * * @author Sven Fillinger * @since 1.0.0 */ @@ -262,7 +283,8 @@ public SignPostingResult validate(List webLinks) { * recorded for the given anchor. *

* - * @param anchor the anchor value (RFC 8288) that defines the link origin (FAIR Signposting) or link context (RFC 9264) + * @param anchor the anchor value (RFC 8288) that defines the link origin (FAIR Signposting) or + * link context (RFC 9264) * @param links the links belonging to the anchor context * @param issues the shared issue sink collecting all validation findings */