Skip to content

Commit

Permalink
(#26): Redesign Semantic Identifier (Strict Format Rules)
Browse files Browse the repository at this point in the history
  • Loading branch information
andi-huber committed Aug 30, 2024
1 parent 85d1af0 commit f53b8ab
Show file tree
Hide file tree
Showing 12 changed files with 564 additions and 73 deletions.
4 changes: 2 additions & 2 deletions commons/src/main/java/dita/commons/food/composition/Dtos.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ public record FoodComponentDatapointProxy(
@NonNull BigDecimal datapointValue) {
FoodComponentDatapointDto pack(){
return new FoodComponentDatapointDto(
_Strings.nullToEmpty(componentId.systemId())
+ "," + _Strings.nullToEmpty(componentId.objectId())
componentId.systemId().toString()
+ "," + componentId.objectId().toString()
+ "," + (datapointSemantic == DatapointSemantic.UPPER_BOUND
? "<"
: "")
Expand Down
41 changes: 2 additions & 39 deletions commons/src/main/java/dita/commons/jaxb/JaxbAdapters.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,8 @@

import jakarta.xml.bind.annotation.adapters.XmlAdapter;

import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;

import org.apache.causeway.commons.internal.assertions._Assert;
import org.apache.causeway.commons.internal.base._NullSafe;
import org.apache.causeway.commons.internal.base._Strings;
import org.apache.causeway.commons.internal.exceptions._Exceptions;

import lombok.experimental.UtilityClass;

import dita.commons.sid.SemanticIdentifier;
Expand All @@ -45,49 +39,18 @@ public class JaxbAdapters {
public static final class SemanticIdentifierAdapter extends XmlAdapter<String, SemanticIdentifier>{
@Override public SemanticIdentifier unmarshal(final String v) throws Exception {
try {
return destringify(v);
return SemanticIdentifier.parse(v);
} catch (Exception e) {
e.printStackTrace(); // might be swallowed otherwise
throw e;
}
}
@Override public String marshal(final SemanticIdentifier v) throws Exception {
return v!=null
? stringify(v)
? v.toString()
: null;
}

// -- HELPER

private static SemanticIdentifier destringify(final String string) {
if(_NullSafe.size(string)<7) {
throw _Exceptions.illegalArgument("cannot parse '%s' as SemanticIdentifier (too short)", string);
}
try {
var commaSeparatedValues = _Strings.substring(string, 4, -1);
var parts = _Strings.splitThenStream(commaSeparatedValues, ",")
.map(SemanticIdentifierAdapter::argumentIn)
.toList();
_Assert.assertEquals(2, parts.size(), ()->
String.format("invalid token count in '%s'", commaSeparatedValues));
return new SemanticIdentifier(parts.get(0), parts.get(1));
} catch (Exception e) {
throw _Exceptions.illegalArgument(e, "cannot parse '%s' as SemanticIdentifier", string);
}
}
private static String stringify(final SemanticIdentifier sid) {
return "SID[" + argumentOut(sid.systemId())
+ "," + argumentOut(sid.objectId()) + "]";
}
private static String argumentIn(@Nullable final String in) {
var arg = _Strings.emptyToNull(_Strings.trim(in));
if(arg!=null) _Assert.assertTrue(arg.indexOf(',') + arg.indexOf(' ') == -2,
()->"Semantic Identifiers may not contain characters ',' or whitespace");
return arg;
}
private static String argumentOut(@Nullable final String arg) {
return _Strings.nullToEmpty(arg);
}
}

public static final class SexAdapter extends XmlAdapter<String, Sex>{
Expand Down
8 changes: 4 additions & 4 deletions commons/src/main/java/dita/commons/qmap/QualifiedMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ public int compareTo(final @Nullable QualifiedMapKey o) {
*/
public String shortFormat(final String primaryDelimiter, final String secondaryDelimiter) {
return qualifier().elements().isEmpty()
? source().objectId()
: source().objectId()
? source().objectId().toString()
: source().objectId().toString()
+ primaryDelimiter
+ qualifier().shortFormat(secondaryDelimiter);
}
Expand All @@ -101,8 +101,8 @@ public String shortFormat(final String primaryDelimiter, final String secondaryD
*/
public String fullFormat(final String primaryDelimiter, final String secondaryDelimiter) {
return qualifier().elements().isEmpty()
? source().fullFormat(":")
: source().fullFormat(":")
? source().toStringNoBox()
: source().toStringNoBox()
+ primaryDelimiter
+ qualifier().fullFormat(secondaryDelimiter);
}
Expand Down
236 changes: 220 additions & 16 deletions commons/src/main/java/dita/commons/sid/SemanticIdentifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,38 +24,234 @@

import org.apache.causeway.commons.internal.base._Strings;

import lombok.Builder;
import lombok.NonNull;

/**
* A Semantic Identifier references data objects across system boundaries.
* <p>
* <ul>
* <li>system: at.gd</li>
* <li>version: 2.0</li>
* <li>context: food | recipe | food.desc | recipe.desc | food.group etc.</li>
* <li>object: 00123</li>
* </ul>
* <p>
* formats to -> SID[at.gd/2.0, food/00123]
*/
@Builder
public record SemanticIdentifier (
/**
* Identifies the system (and optionally version) of the referenced data object.
* Identifies the system of the referenced data object.
* (optionally includes version information)
*/
String systemId,
@NonNull SystemId systemId,
/**
* Uniquely identifies the data object within the system.
* (optionally includes context information)
*/
String objectId) implements Comparable<SemanticIdentifier> {
@NonNull ObjectId objectId) implements Comparable<SemanticIdentifier> {

// -- SYSTEM ID

public record SystemId(
/**
* Identifies the system of the referenced data object.
*/
String system,
/**
* Identifies the system's version of the referenced data object.
*/
String version) implements Comparable<SystemId> {

public static SystemId empty() {
return new SystemId(null, null);
}

public static SystemId parse(final @Nullable String systemIdStringified) {
return _Utils.parseSystemId(systemIdStringified);
}

public SystemId(final String system, final String version) {
this.system = _Utils.validate(system);
this.version = _Utils.validate(version);
}

/**
* SystemId constructor with empty version part.
*/
public SystemId(final String system) {
this(system, null);
}

// -- WITHER

public SystemId withSystem(final String system) {
return new SystemId(system, version());
}
public SystemId withVersion(final String version) {
return new SystemId(system(), version);
}

// -- MAPPER

public SystemId mapSystem(final UnaryOperator<String> systemMapper) {
return withSystem(systemMapper.apply(system()));
}
public SystemId mapVersion(final UnaryOperator<String> versionMapper) {
return withVersion(versionMapper.apply(version()));
}

// -- CONTRACT

@Override
public int compareTo(final @Nullable SystemId o) {
return compare(this, o);
}

/**
* Identifies the system of the referenced data object.
* (optionally suffixed with a version)
*/
@Override
public final String toString() {
return _Utils.format(this);
}

public static int compare(
final @Nullable SystemId a,
final @Nullable SystemId b) {
if(a==null) return b==null
? 0
: -1;
if(b==null) return 1;
final int c = _Strings.compareNullsFirst(a.system(), b.system());
return c!=0
? c
// TODO should use DEWEY compare here
: _Strings.compareNullsFirst(a.version(), b.version());
}

}

// -- OBJECT ID

public record ObjectId(
/**
* Uniquely identifies the data object's context within the system.
*/
String context,
/**
* Uniquely identifies the data object within the system's context.
*/
String object) implements Comparable<ObjectId> {

public static ObjectId empty() {
return new ObjectId(null, null);
}

public static ObjectId parse(final @Nullable String objectIdStringified) {
return _Utils.parseObjectId(objectIdStringified);
}

public ObjectId(final String context, final String object) {
this.context = _Utils.validate(context);
this.object = _Utils.validateObject(object);
}

/**
* ObjectId constructor with empty context part.
*/
public ObjectId(final String object) {
this(null, object);
}

// -- WITHER

public ObjectId withContext(final String context) {
return new ObjectId(context, object());
}
public ObjectId withObject(final String object) {
return new ObjectId(context(), object);
}

// -- MAPPER

public ObjectId mapContext(final UnaryOperator<String> contextMapper) {
return withContext(contextMapper.apply(context()));
}
public ObjectId mapObject(final UnaryOperator<String> objectMapper) {
return withObject(objectMapper.apply(object()));
}

// -- CONTRACT

@Override
public int compareTo(final @Nullable ObjectId o) {
return compare(this, o);
}

/**
* Uniquely identifies the data object within the system.
* (optionally prefixed with a context)
*/
@Override
public final String toString() {
return _Utils.format(this);
}

public static int compare(
final @Nullable ObjectId a,
final @Nullable ObjectId b) {
if(a==null) return b==null
? 0
: -1;
if(b==null) return 1;
final int c = _Strings.compareNullsFirst(a.context(), b.context());
return c!=0
? c
// TODO should use DEWEY compare here
: _Strings.compareNullsFirst(a.object(), b.object());
}

}

// -- SID IMPL

public static SemanticIdentifier empty() {
return new SemanticIdentifier(SystemId.empty(), ObjectId.empty());
}

public SemanticIdentifier(final String systemId, final String objectId) {
this(SystemId.parse(systemId), ObjectId.parse(objectId));
}

// -- PARSER

/**
* Inverse of {@link SemanticIdentifier#toString()}.
* @see SemanticIdentifier#toString()
*/
public static SemanticIdentifier parse(final @Nullable String stringified) {
return _Utils.parseSid(stringified);
}

// -- WITHER

public SemanticIdentifier withSystemId(final String systemId) {
return new SemanticIdentifier(systemId, objectId);
public SemanticIdentifier withSystemId(final SystemId systemId) {
return new SemanticIdentifier(systemId, objectId());
}
public SemanticIdentifier withObjectId(final String objectId) {
return new SemanticIdentifier(systemId, objectId);
public SemanticIdentifier withObjectId(final ObjectId objectId) {
return new SemanticIdentifier(systemId(), objectId);
}

// -- MAPPER

public SemanticIdentifier mapSystemId(final UnaryOperator<String> systemIdMapper) {
return withSystemId(systemIdMapper.apply(systemId));
public SemanticIdentifier mapSystemId(final UnaryOperator<SystemId> systemIdMapper) {
return withSystemId(systemIdMapper.apply(systemId()));
}
public SemanticIdentifier mapObjectId(final UnaryOperator<String> objectIdMapper) {
return withObjectId(objectIdMapper.apply(objectId));
public SemanticIdentifier mapObjectId(final UnaryOperator<ObjectId> objectIdMapper) {
return withObjectId(objectIdMapper.apply(objectId()));
}


Expand All @@ -65,10 +261,18 @@ public int compareTo(final @Nullable SemanticIdentifier o) {
}

/**
* @param delimiter that separates systemId and objectId
* e.g. {@code at.gd/2.0:food/00123}
*/
public String toStringNoBox() {
return _Utils.formatUnboxed(this);
}

/**
* e.g. {@code SID[at.gd/2.0:food/00123]}
*/
public String fullFormat(final @NonNull String delimiter) {
return _Strings.nullToEmpty(systemId) + delimiter + _Strings.nullToEmpty(objectId);
@Override
public final String toString() {
return _Utils.formatBoxed(this);
}

// -- UTILITY
Expand All @@ -80,10 +284,10 @@ public static int compare(
? 0
: -1;
if(b==null) return 1;
final int c = _Strings.compareNullsFirst(a.systemId(), b.systemId());
final int c = a.systemId().compareTo(b.systemId());
return c!=0
? c
: _Strings.compareNullsFirst(a.objectId(), b.objectId());
: a.objectId().compareTo(b.objectId());
}

}
Loading

0 comments on commit f53b8ab

Please sign in to comment.