Skip to content
Draft
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
32 changes: 32 additions & 0 deletions fair-signposting/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>life.qbic.datamanager</groupId>
<artifactId>datamanager</artifactId>
<version>1.11.0</version>
</parent>

<artifactId>fair-signposting</artifactId>

<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package life.qbic.datamanager.signposting.http;

/**
* <b><class short description - 1 Line!></b>
*
* <p><More detailed description - When to use, what it solves, etc.></p>
*
* @since <version tag>
*/
public final class FormatException extends RuntimeException {
public FormatException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package life.qbic.datamanager.signposting.http;

import java.net.URI;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import life.qbic.datamanager.signposting.http.validation.RfcLinkParameter;

/**
* A Java record representing a web link object following the
* <a href="https://datatracker.ietf.org/doc/html/rfc8288">RFC 8288</a> model specification.
*/
public record WebLink(URI reference, List<WebLinkParameter> params) {

/**
* Creates an <a href="https://datatracker.ietf.org/doc/html/rfc8288">RFC 8288</a> compliant web
* link object.
* <p>
* Following RFC8288, the ABNF for a link parameter is:
* <p>
* {@code link-param = token BWS [ "=" BWS ( token / quoted-string ) ]}
* <p>
* The parameter key must not be withoutValue, so during construction the {@code params} keys are checked
* for an withoutValue key. The values can be withoutValue though.
*
* @param reference a {@link URI} pointing to the actual resource
* @param params a {@link Map} of parameters as keys and a list of their values
* @return the new Weblink
* @throws NullPointerException if any method argument is {@code null}
*/
public static WebLink create(URI reference, List<WebLinkParameter> params)
throws NullPointerException {
Objects.requireNonNull(reference);
Objects.requireNonNull(params);
return new WebLink(reference, params);
}

/**
* Web link constructor that can be used if a web link has no parameters.
* <p>
*
* @param reference a {@link URI} pointing to the actual resource
* @return the new Weblink
* @throws NullPointerException if any method argument is {@code null}
*/
public static WebLink create(URI reference) throws NullPointerException {
return create(reference, List.of());
}


public Optional<String> anchor() {
return Optional.empty();
}

public List<String> hreflang() {
return List.of();
}

public Optional<String> media() {
return Optional.empty();
}

/**
* Returns all "rel" parameter values of the link.
* <p>
* RFC 8288 section 3.3 states, that the relation parameter MUST NOT appear more than once in a
* given link-value, but one "rel" parameter value can contain multiple relation-types when
* separated by one or more space characters (SP = ASCII 0x20):
* <p>
* {@code relation-type *( 1*SP relation-type ) }.
* <p>
* The method returns space-separated values as individual values of the "rel" parameter.
*
* @return a list of relation parameter values
*/
public List<String> rel() {
return this.params.stream()
.filter(param -> param.name().equals("rel"))
.map(WebLinkParameter::value)
.map(value -> value.split("\\s+"))
.flatMap(Arrays::stream)
.toList();
}

/**
* Returns all "rev" parameter values of the link.
* <p>
* RFC 8288 section 3.3 does not specify the multiplicity of occurrence. But given the close
* relation to the "rel" parameter and its definition in the same section, web link will treat the
* "rev" parameter equally.
* <p>
* As with the "rel" parameter, multiple regular relation types are allowed when they are
* separated by one or more space characters (SP = ASCII 0x20):
* <p>
* {@code relation-type *( 1*SP relation-type ) }.
* <p>
* The method returns space-separated values as individual values of the "rel" parameter.
*
* @return a list of relation parameter values
*/
public List<String> rev() {
return this.params.stream()
.filter(param -> param.name().equals("rev"))
.map(WebLinkParameter::value)
.map(value -> value.split("\\s+"))
.flatMap(Arrays::stream)
.toList();
}

public Optional<String> title() {
return Optional.empty();
}

public Optional<String> titleMultiple() {
return Optional.empty();
}

public Optional<String> type() {
return Optional.empty();
}

public Map<String, List<String>> extensionAttributes() {
Set<String> rfcParameterNames = Arrays.stream(RfcLinkParameter.values())
.map(RfcLinkParameter::rfcValue)
.collect(Collectors.toSet());
return this.params.stream()
.filter(param -> !rfcParameterNames.contains(param.name()))
.collect(Collectors.groupingBy(WebLinkParameter::name,
Collectors.mapping(WebLinkParameter::value, Collectors.toList())));
}

public List<String> extensionAttribute(String name) {
return extensionAttributes().getOrDefault(name, List.of());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package life.qbic.datamanager.signposting.http;

import java.util.List;
import life.qbic.datamanager.signposting.http.lexing.WebLinkToken;

/**
* Lexes a single Web Link (RFC 8288) serialisation string into a list of tokens.
* <p>
* Implementations should be stateless or thread-confined.
*/
public interface WebLinkLexer {

/**
* Lex the given input string into a sequence of tokens.
*
* @param input the raw Link header field-value or link-value
* @return list of tokens ending with an EOF token
* @throws WebLinkLexingException if the input is not lexically well-formed
*/
List<WebLinkToken> lex(String input) throws WebLinkLexingException;

/**
* Thrown when the input cannot be tokenised according to the Web Link lexical rules.
*/
class WebLinkLexingException extends RuntimeException {

public WebLinkLexingException(String message) {
super(message);
}

public WebLinkLexingException(String message, Throwable cause) {
super(message, cause);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package life.qbic.datamanager.signposting.http;

/**
* A parameter for the HTTP Link header attribute.
* <p>
* Based on RFC 8288, a parameter with only a name is valid.
* <p>
* <pre>
* {@code
* // ABNF notation for web links
* Link = #link-value
* link-value = "<" URI-Reference ">" *( OWS ";" OWS link-param )
* link-param = token BWS [ "=" BWS ( token / quoted-string ) ]
*
* // valid parameter examples
* "Link: <https://example.org>; rel; param1;"
* "Link: <https://example.org>; rel="self"; param1="";"
* }
* </pre>
* <p>
* It is important that different parameter serialisation cases are handled correctly.
* <p>
* The following example shows three distinct cases that must be preserved during de-serialisation:
*
* <pre>
* {@code
* x="" // empty double-quoted string
* x="y" // double-quoted with content
* x=y // token value
* x // parameter name only
* }
* </pre>
* <p>
* These are all valid parameter serialisations.
*
*
*/
public record WebLinkParameter(String name, String value) {

/**
* Creates a new web link parameter with the provided name and value.
*
* @param name the name of the web link parameter
* @param value the value of the web link parameter
*/
public static WebLinkParameter create(String name, String value) {
return new WebLinkParameter(name, value);
}

/**
* Creates a new web link parameter without a value.
*
* @param name the name of the parameter
*/
public static WebLinkParameter withoutValue(String name) {
return new WebLinkParameter(name, null);
}

/**
* Checks if the web link parameter has a value.
* <p>
* The method will return {@code true} only when a value (including an empty one) has been
* provided.
*
* @return {@code true}, if the parameter has a value (including an empty one). Returns
* {@code false}, if no value has been provided
*/
public boolean hasValue() {
return value != null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package life.qbic.datamanager.signposting.http;

import java.util.List;
import life.qbic.datamanager.signposting.http.lexing.WebLinkToken;
import life.qbic.datamanager.signposting.http.parsing.RawLinkHeader;

/**
* A parser that checks structural integrity of an HTTP Link header entry in compliance with <a
* href="https://datatracker.ietf.org/doc/html/rfc8288">RFC 8288</a>.
* <p>
* A web link parser is able to process tokens from web link lexing and convert the tokens to raw
* link headers after structural validation, which can be seen as an AST (abstract syntax tree).
* <p>
* Note: Implementations <strong>must not</strong> perform semantic validation, this is concern of
* {@link WebLinkValidator} implementations.
* <p>
* In case of structural violations, implementations of the {@link WebLinkParser} interface must
* throw a {@link StructureException}.
* <p>
* RFC 8288 section 3 describes the serialization of the Link HTTP header attribute:
*
* <pre>
* {@code
* Link = #link-value
* link-value = "<" URI-Reference ">" *( OWS ";" OWS link-param )
* link-param = token BWS [ "=" BWS ( token / quoted-string ) ]
* }
* </pre>
* <p>
* The {@link WebLinkParser} interface can process {@link WebLinkToken}, which are the output of
* lexing raw character values into known token values. See {@link WebLinkLexer} for details to
* lexers.
*/
public interface WebLinkParser {

/**
* Parses a list of {@link WebLinkToken} and performs structural validation based on the RFC 8288
* serialisation requirement.
* <p>
* The returned value is an AST of a raw link header with a list of raw web link items that can be
* used for semantic validation.
*
* @param tokens a list of web link tokens to process
* @return a raw link header parsed from the web link tokens
* @throws NullPointerException if the token list is {@code null}
* @throws StructureException if any structural violation occurred
*/
RawLinkHeader parse(List<WebLinkToken> tokens) throws NullPointerException, StructureException;

/**
* Indicates a structural violation of the RFC 8288 web link serialisation requirement.
*/
class StructureException extends RuntimeException {

public StructureException(String message) {
super(message);
}
}
}
Loading
Loading