-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #147 from BIBSYSDEV/NP-1979-sortable-identifier
NP-1979: SortableIdentifier in commons
- Loading branch information
Showing
6 changed files
with
264 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
dependencies{ | ||
compileOnly project (":core") | ||
testImplementation project (":core") | ||
} |
72 changes: 72 additions & 0 deletions
72
identifiers/src/main/java/no/unit/nva/identifiers/SortableIdentifier.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package no.unit.nva.identifiers; | ||
|
||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; | ||
import com.fasterxml.jackson.databind.annotation.JsonSerialize; | ||
import java.util.Objects; | ||
import java.util.UUID; | ||
|
||
/** | ||
* Generates ids of the form "0176f264a5ad-446893d8-3c02-4f64-936b-2997fec34e98". where the first part is an Instant | ||
* encoded in 12 hex digits (enough until the year 10889) and the rest is a UUID. The timestamp is should be used only | ||
* for sorting and not for identifying creation date | ||
*/ | ||
@JsonSerialize(using = SortableIdentifierSerializer.class) | ||
@JsonDeserialize(using = SortableIdentifierDeserializer.class) | ||
public final class SortableIdentifier implements Comparable<SortableIdentifier> { | ||
|
||
public static final int UUID_LENGTH = 36; | ||
public static final int TIMESTAMP_LENGTH = 12; | ||
public static final int EXTRA_DASH = 1; | ||
public static final int SORTABLE_ID_LENGTH = UUID_LENGTH + TIMESTAMP_LENGTH + EXTRA_DASH; | ||
private static final String IDENTIFIER_FORMATTING = "%0" + TIMESTAMP_LENGTH + "x-%s"; | ||
|
||
private final String identifier; | ||
|
||
public SortableIdentifier(String identifier) { | ||
validate(identifier); | ||
this.identifier = identifier; | ||
} | ||
|
||
public static SortableIdentifier next() { | ||
return new SortableIdentifier(newIdentifierString()); | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
return Objects.hash(identifier); | ||
} | ||
|
||
@Override | ||
public boolean equals(Object o) { | ||
if (this == o) { | ||
return true; | ||
} | ||
if (o == null || getClass() != o.getClass()) { | ||
return false; | ||
} | ||
SortableIdentifier that = (SortableIdentifier) o; | ||
return Objects.equals(toString(), that.toString()); | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return identifier; | ||
} | ||
|
||
@Override | ||
public int compareTo(SortableIdentifier o) { | ||
return this.toString().compareTo(o.toString()); | ||
} | ||
|
||
private static String newIdentifierString() { | ||
return String.format(IDENTIFIER_FORMATTING, System.currentTimeMillis(), UUID.randomUUID()); | ||
} | ||
|
||
private void validate(String identifier) { | ||
if (identifier.length() == UUID_LENGTH || identifier.length() == SORTABLE_ID_LENGTH) { | ||
return; | ||
} else { | ||
throw new IllegalArgumentException("Invalid sortable identifier"); | ||
} | ||
} | ||
} |
23 changes: 23 additions & 0 deletions
23
identifiers/src/main/java/no/unit/nva/identifiers/SortableIdentifierDeserializer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package no.unit.nva.identifiers; | ||
|
||
import com.fasterxml.jackson.core.JsonParser; | ||
import com.fasterxml.jackson.databind.DeserializationContext; | ||
import com.fasterxml.jackson.databind.JsonDeserializer; | ||
import java.io.IOException; | ||
import nva.commons.core.JacocoGenerated; | ||
|
||
public class SortableIdentifierDeserializer extends JsonDeserializer<SortableIdentifier> { | ||
|
||
@JacocoGenerated | ||
public SortableIdentifierDeserializer() { | ||
super(); | ||
} | ||
|
||
@JacocoGenerated | ||
@Override | ||
public SortableIdentifier deserialize(JsonParser p, DeserializationContext ctxt) | ||
throws IOException { | ||
String value = p.getValueAsString(); | ||
return new SortableIdentifier(value); | ||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
identifiers/src/main/java/no/unit/nva/identifiers/SortableIdentifierSerializer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package no.unit.nva.identifiers; | ||
|
||
import com.fasterxml.jackson.core.JsonGenerator; | ||
import com.fasterxml.jackson.databind.JsonSerializer; | ||
import com.fasterxml.jackson.databind.SerializerProvider; | ||
import java.util.Objects; | ||
import java.util.Optional; | ||
import nva.commons.core.JacocoGenerated; | ||
|
||
public class SortableIdentifierSerializer extends JsonSerializer<SortableIdentifier> { | ||
|
||
public static final String NULL_AS_STRING = "null"; | ||
public static final String SERIALIZATION_EXCEPTION_ERROR = "Could not serialize SortableIdentifier with value: "; | ||
|
||
@JacocoGenerated | ||
public SortableIdentifierSerializer() { | ||
super(); | ||
} | ||
|
||
@Override | ||
public void serialize(SortableIdentifier value, JsonGenerator gen, SerializerProvider serializers) { | ||
try { | ||
if (Objects.nonNull(value)) { | ||
gen.writeString(value.toString()); | ||
} else { | ||
gen.writeNull(); | ||
} | ||
} catch (Exception e) { | ||
throw new RuntimeException(SERIALIZATION_EXCEPTION_ERROR + printIdentifierValue(value), e); | ||
} | ||
} | ||
|
||
private String printIdentifierValue(SortableIdentifier value) { | ||
return Optional.ofNullable(value).map(SortableIdentifier::toString).orElse(NULL_AS_STRING); | ||
} | ||
} |
128 changes: 128 additions & 0 deletions
128
identifiers/src/test/java/no/unit/nva/identifiers/SortableIdentifierTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
package no.unit.nva.identifiers; | ||
|
||
import static org.hamcrest.MatcherAssert.assertThat; | ||
import static org.hamcrest.core.Is.is; | ||
import static org.hamcrest.core.IsEqual.equalTo; | ||
import static org.hamcrest.core.StringContains.containsString; | ||
import static org.junit.jupiter.api.Assertions.assertThrows; | ||
import static org.mockito.ArgumentMatchers.anyString; | ||
import static org.mockito.Mockito.doThrow; | ||
import static org.mockito.Mockito.mock; | ||
import com.fasterxml.jackson.core.JsonGenerator; | ||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import java.io.IOException; | ||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Random; | ||
import java.util.UUID; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
import nva.commons.core.JsonUtils; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.function.Executable; | ||
|
||
public class SortableIdentifierTest { | ||
|
||
public static final int MIN_NUMBER_OF_SHUFFLES = 5; | ||
public static final int ADDITIONAL_SHUFFLES = 10; | ||
public static final String DELIMITER = "-"; | ||
public static final SortableIdentifier SAMPLE_IDENTIFIER = SortableIdentifier.next(); | ||
public static final String SAMPLE_CLASS_ID_FIELD = String.format("\"id\" : \"%s\"", | ||
SAMPLE_IDENTIFIER.toString()); | ||
public static final String SAMPLE_EXAMPLE_CLASS_JSON = "{" + SAMPLE_CLASS_ID_FIELD + "}"; | ||
|
||
@Test | ||
public void sortableIdentifierStringContainsSixParts() { | ||
SortableIdentifier identifier = SortableIdentifier.next(); | ||
String identifierString = identifier.toString(); | ||
String[] identifierParts = identifierString.split(DELIMITER); | ||
assertThat(identifierParts.length, is(equalTo(6))); | ||
} | ||
|
||
@Test | ||
public void sortableIdentifierAcceptsUuidString() { | ||
UUID oldId = UUID.randomUUID(); | ||
SortableIdentifier identifier = new SortableIdentifier(oldId.toString()); | ||
assertThat(identifier.toString(), is(equalTo(oldId.toString()))); | ||
} | ||
|
||
@Test | ||
public void sortableIdentifierSerializesAsString() throws JsonProcessingException { | ||
ExampleClass exampleClass = new ExampleClass(); | ||
exampleClass.setId(SAMPLE_IDENTIFIER); | ||
String json = JsonUtils.objectMapper.writeValueAsString(exampleClass); | ||
assertThat(json, containsString(SAMPLE_CLASS_ID_FIELD)); | ||
} | ||
|
||
@Test | ||
public void sortableIdentifierDeserializesFromString() throws JsonProcessingException { | ||
ExampleClass actual = JsonUtils.objectMapper.readValue(SAMPLE_EXAMPLE_CLASS_JSON, ExampleClass.class); | ||
SortableIdentifier actualIdentifier = actual.getId(); | ||
assertThat(actualIdentifier, is(equalTo(SAMPLE_IDENTIFIER))); | ||
} | ||
|
||
@Test | ||
public void sortableIdentifierIsSortable() throws InterruptedException { | ||
final Map<SortableIdentifier, Integer> expectedIdentifierOrder = sortableIdentifiersWithExpectedOrder(); | ||
List<SortableIdentifier> idStrings = new ArrayList<>(expectedIdentifierOrder.keySet()); | ||
shuffle(idStrings); | ||
Collections.sort(idStrings); | ||
|
||
for (int actualIndex = 0; actualIndex < idStrings.size(); actualIndex++) { | ||
SortableIdentifier idString = idStrings.get(actualIndex); | ||
int expectedIndex = expectedIdentifierOrder.get(idString); | ||
assertThat(actualIndex, is(equalTo(expectedIndex))); | ||
} | ||
} | ||
|
||
@Test | ||
public void serializationErrorReturnsMessageWithSortableIdentifierValue() throws IOException { | ||
SortableIdentifier identifier = SortableIdentifier.next(); | ||
String expectedMessage = "expectedMessage"; | ||
|
||
SortableIdentifierSerializer serializer = new SortableIdentifierSerializer(); | ||
IllegalStateException exceptionCause = new IllegalStateException(expectedMessage); | ||
|
||
JsonGenerator jsonGenerator = mock(JsonGenerator.class); | ||
doThrow(exceptionCause).when(jsonGenerator).writeString(anyString()); | ||
|
||
Executable action = () -> serializer.serialize(identifier, jsonGenerator, null); | ||
RuntimeException actualException = assertThrows(RuntimeException.class, action); | ||
Throwable cause = actualException.getCause(); | ||
assertThat(actualException.getMessage(), containsString(identifier.toString())); | ||
assertThat(actualException.getMessage(), | ||
containsString(SortableIdentifierSerializer.SERIALIZATION_EXCEPTION_ERROR)); | ||
assertThat(cause, is(equalTo(exceptionCause))); | ||
} | ||
|
||
private void shuffle(List<SortableIdentifier> idStrings) { | ||
int numberOfShuffles = MIN_NUMBER_OF_SHUFFLES + new Random().nextInt(ADDITIONAL_SHUFFLES); | ||
for (int i = 0; i < numberOfShuffles; i++) { | ||
Collections.shuffle(idStrings); | ||
} | ||
} | ||
|
||
private Map<SortableIdentifier, Integer> sortableIdentifiersWithExpectedOrder() throws InterruptedException { | ||
final Map<SortableIdentifier, Integer> expectedIdentifierIndices = new ConcurrentHashMap<>(); | ||
int sampleSize = 1000; | ||
for (int index = 0; index < sampleSize; index++) { | ||
Thread.sleep(2); | ||
expectedIdentifierIndices.put(SortableIdentifier.next(), index); | ||
} | ||
return expectedIdentifierIndices; | ||
} | ||
|
||
private static class ExampleClass { | ||
|
||
private SortableIdentifier id; | ||
|
||
public SortableIdentifier getId() { | ||
return id; | ||
} | ||
|
||
public void setId(SortableIdentifier id) { | ||
this.id = id; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,4 +6,5 @@ include 'doi' | |
include 'lambdaauthorizer' | ||
include 'logutils' | ||
include 'eventhandlers' | ||
include 'identifiers' | ||
|