Skip to content

Commit 8b2ae49

Browse files
committed
Merge remote-tracking branch 'origin/dev-v3' into 109-multipleFAIR_DOs
# Conflicts: # build.gradle
2 parents 1d481c4 + 857d912 commit 8b2ae49

File tree

11 files changed

+293
-172
lines changed

11 files changed

+293
-172
lines changed

build.gradle

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,8 @@ dependencies {
8181
implementation "org.springframework.data:spring-data-elasticsearch"
8282

8383
// More flexibility when (de-)serializing json:
84-
//implementation("com.monitorjbl:spring-json-view:1.1.0")
85-
implementation("com.github.everit-org.json-schema:org.everit.json.schema:1.14.4")
86-
84+
implementation(group: 'com.networknt', name: 'json-schema-validator', version: '1.5.7');
85+
8786
implementation('org.apache.httpcomponents:httpclient:4.5.14')
8887
implementation('org.apache.httpcomponents:httpclient-cache:4.5.14')
8988

@@ -188,15 +187,15 @@ jacocoTestReport {
188187
afterEvaluate {
189188
//exclude some classes/package from code coverage report
190189
classDirectories.setFrom(files(classDirectories.files.collect {
191-
fileTree(dir: it, exclude: [\
192-
'edu/kit/datamanager/pit/configuration/**', \
193-
'edu/kit/datamanager/pit/web/converter/**', \
194-
'edu/kit/datamanager/pit/web/ExtendedErrorAttributes**', \
195-
'edu/kit/datamanager/pit/web/UncontrolledExceptionHandler**', \
196-
'edu/kit/datamanager/pit/common/**', \
197-
'edu/kit/datamanager/pit/Application*'
198-
])
199-
}))
190+
fileTree(dir: it, exclude: [\
191+
'edu/kit/datamanager/pit/configuration/**', \
192+
'edu/kit/datamanager/pit/web/converter/**', \
193+
'edu/kit/datamanager/pit/web/ExtendedErrorAttributes**', \
194+
'edu/kit/datamanager/pit/web/UncontrolledExceptionHandler**', \
195+
'edu/kit/datamanager/pit/common/**', \
196+
'edu/kit/datamanager/pit/Application*'
197+
])
198+
}))
200199
}
201200
}
202201

src/main/java/edu/kit/datamanager/pit/common/ExternalServiceException.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66

77
public class ExternalServiceException extends ResponseStatusException {
88

9-
private static final String MESSAGE_TERM_SERVICE = "Service";
9+
private static final String MESSAGE_TERM_SERVICE = "Service ";
1010
private static final long serialVersionUID = 1L;
1111
private static final HttpStatus HTTP_STATUS = HttpStatus.SERVICE_UNAVAILABLE;
1212

1313
public ExternalServiceException(String serviceName) {
14-
super(HTTP_STATUS, "Service " + serviceName + " not available.");
14+
super(HTTP_STATUS, MESSAGE_TERM_SERVICE + serviceName + " not available.");
1515
}
1616

1717
public ExternalServiceException(String serviceName, Throwable e) {

src/main/java/edu/kit/datamanager/pit/domain/Operations.java

Lines changed: 64 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package edu.kit.datamanager.pit.domain;
22

33
import java.io.IOException;
4+
import java.time.format.DateTimeFormatter;
45
import java.util.ArrayList;
56
import java.util.Arrays;
67
import java.util.Collection;
@@ -15,13 +16,11 @@
1516
import edu.kit.datamanager.pit.typeregistry.AttributeInfo;
1617
import edu.kit.datamanager.pit.typeregistry.ITypeRegistry;
1718
import org.apache.commons.lang3.stream.Streams;
18-
import org.joda.time.DateTime;
19-
import org.joda.time.format.DateTimeFormatter;
20-
import org.joda.time.format.ISODateTimeFormat;
19+
import java.time.ZonedDateTime;
2120

2221
/**
2322
* Simple operations on PID records.
24-
*
23+
* <p>
2524
* Caches results e.g. for type queries
2625
*/
2726
public class Operations {
@@ -35,8 +34,8 @@ public class Operations {
3534
"21.T11148/397d831aa3a9d18eb52c"
3635
};
3736

38-
private ITypeRegistry typeRegistry;
39-
private IIdentifierSystem identifierSystem;
37+
private final ITypeRegistry typeRegistry;
38+
private final IIdentifierSystem identifierSystem;
4039

4140
public Operations(ITypeRegistry typeRegistry, IIdentifierSystem identifierSystem) {
4241
this.typeRegistry = typeRegistry;
@@ -45,133 +44,125 @@ public Operations(ITypeRegistry typeRegistry, IIdentifierSystem identifierSystem
4544

4645
/**
4746
* Tries to get the date when a FAIR DO was created from a PID record.
48-
*
47+
* <p>
4948
* Strategy:
5049
* - try to get it from known "dateCreated" types
51-
* - as a fallback, try to get it by its human readable name
52-
*
50+
* - as a fallback, try to get it by its human-readable name
51+
* <p>
5352
* Semantic reasoning in some sense is planned but not yet supported.
5453
*
5554
* @param pidRecord the record to extract the information from.
56-
* @return the date, if it could been extracted.
55+
* @return the date, if it could have been extracted.
5756
* @throws IOException on IO errors regarding resolving types.
5857
*/
5958
public Optional<Date> findDateCreated(PIDRecord pidRecord) throws IOException {
6059
/* try known types */
6160
List<String> knownDateTypes = Arrays.asList(Operations.KNOWN_DATE_CREATED);
6261
Optional<Date> date = knownDateTypes
63-
.stream()
64-
.map(pidRecord::getPropertyValues)
65-
.map(Arrays::asList)
66-
.flatMap(List<String>::stream)
67-
.map(this::extractDate)
68-
.filter(Optional<Date>::isPresent)
69-
.map(Optional<Date>::get)
70-
.sorted(Comparator.comparingLong(Date::getTime))
71-
.findFirst();
62+
.stream()
63+
.map(pidRecord::getPropertyValues)
64+
.map(Arrays::asList)
65+
.flatMap(List<String>::stream)
66+
.map(this::extractDate)
67+
.filter(Optional::isPresent)
68+
.map(Optional::get)
69+
.min(Comparator.comparingLong(Date::getTime));
7270
if (date.isPresent()) {
7371
return date;
7472
}
7573

7674
Collection<AttributeInfo> types = new ArrayList<>();
7775
List<CompletableFuture<?>> futures = Streams.failableStream(
7876
pidRecord.getPropertyIdentifiers().stream())
79-
.filter(attributePid -> this.identifierSystem.isPidRegistered(attributePid))
80-
.map(attributePid -> {
81-
return this.typeRegistry
77+
.filter(this.identifierSystem::isPidRegistered)
78+
.map(attributePid -> this.typeRegistry
8279
.queryAttributeInfo(attributePid)
83-
.thenAcceptAsync(types::add);
84-
})
80+
.thenAcceptAsync(types::add))
8581
.collect(Collectors.toList());
8682
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
8783

8884
/*
89-
* as a last fallback, try find types with human readable names containing
85+
* as a last fallback, try to find types with human-readable names containing
9086
* "dateCreated" or "createdAt" or "creationDate".
9187
*
9288
* This can be removed as soon as we have some default FAIR DO types new type
9389
* definitions can refer to (e.g. "extend" them or declare the same meaning as
9490
* our known types, see above)
9591
*/
9692
return types
97-
.stream()
98-
.filter(type ->
99-
type.name().equalsIgnoreCase("dateCreated")
100-
|| type.name().equalsIgnoreCase("createdAt")
101-
|| type.name().equalsIgnoreCase("creationDate"))
102-
.map(type -> pidRecord.getPropertyValues(type.pid()))
103-
.map(Arrays::asList)
104-
.flatMap(List<String>::stream)
105-
.map(this::extractDate)
106-
.filter(Optional<Date>::isPresent)
107-
.map(Optional<Date>::get)
108-
.sorted(Comparator.comparingLong(Date::getTime))
109-
.findFirst();
93+
.stream()
94+
.filter(type ->
95+
type.name().equalsIgnoreCase("dateCreated")
96+
|| type.name().equalsIgnoreCase("createdAt")
97+
|| type.name().equalsIgnoreCase("creationDate"))
98+
.map(type -> pidRecord.getPropertyValues(type.pid()))
99+
.map(Arrays::asList)
100+
.flatMap(List<String>::stream)
101+
.map(this::extractDate)
102+
.filter(Optional::isPresent)
103+
.map(Optional::get)
104+
.min(Comparator.comparingLong(Date::getTime));
110105
}
111106

112107
/**
113108
* Tries to get the date when a FAIR DO was modified from a PID record.
114-
*
109+
* <p>
115110
* Strategy:
116111
* - try to get it from known "dateModified" types
117-
* - as a fallback, try to get it by its human readable name
118-
*
112+
* - as a fallback, try to get it by its human-readable name
113+
* <p>
119114
* Semantic reasoning in some sense is planned but not yet supported.
120115
*
121116
* @param pidRecord the record to extract the information from.
122-
* @return the date, if it could been extracted.
117+
* @return the date, if it could have been extracted.
123118
* @throws IOException on IO errors regarding resolving types.
124119
*/
125120
public Optional<Date> findDateModified(PIDRecord pidRecord) throws IOException {
126121
/* try known types */
127122
List<String> knownDateTypes = Arrays.asList(Operations.KNOWN_DATE_MODIFIED);
128123
Optional<Date> date = knownDateTypes
129-
.stream()
130-
.map(pidRecord::getPropertyValues)
131-
.map(Arrays::asList)
132-
.flatMap(List<String>::stream)
133-
.map(this::extractDate)
134-
.filter(Optional<Date>::isPresent)
135-
.map(Optional<Date>::get)
136-
.sorted(Comparator.comparingLong(Date::getTime))
137-
.findFirst();
124+
.stream()
125+
.map(pidRecord::getPropertyValues)
126+
.map(Arrays::asList)
127+
.flatMap(List<String>::stream)
128+
.map(this::extractDate)
129+
.filter(Optional::isPresent)
130+
.map(Optional::get)
131+
.min(Comparator.comparingLong(Date::getTime));
138132
if (date.isPresent()) {
139133
return date;
140134
}
141135

142136
Collection<AttributeInfo> types = new ArrayList<>();
143137
List<CompletableFuture<?>> futures = Streams.failableStream(pidRecord.getPropertyIdentifiers().stream())
144-
.filter(attributePid -> this.identifierSystem.isPidRegistered(attributePid))
145-
.map(attributePid -> {
146-
return this.typeRegistry
138+
.filter(this.identifierSystem::isPidRegistered)
139+
.map(attributePid -> this.typeRegistry
147140
.queryAttributeInfo(attributePid)
148-
.thenAcceptAsync(types::add);
149-
})
141+
.thenAcceptAsync(types::add))
150142
.collect(Collectors.toList());
151143
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
152144

153145
/*
154-
* as a last fallback, try find types with human readable names containing
146+
* as a last fallback, try to find types with human-readable names containing
155147
* "dateModified" or "lastModified" or "modificationDate".
156148
*
157149
* This can be removed as soon as we have some default FAIR DO types new type
158150
* definitions can refer to (e.g. "extend" them or declare the same meaning as
159151
* our known types, see above)
160152
*/
161153
return types
162-
.stream()
163-
.filter(type ->
164-
type.name().equalsIgnoreCase("dateModified")
165-
|| type.name().equalsIgnoreCase("lastModified")
166-
|| type.name().equalsIgnoreCase("modificationDate"))
167-
.map(type -> pidRecord.getPropertyValues(type.pid()))
168-
.map(Arrays::asList)
169-
.flatMap(List<String>::stream)
170-
.map(this::extractDate)
171-
.filter(Optional<Date>::isPresent)
172-
.map(Optional<Date>::get)
173-
.sorted(Comparator.comparingLong(Date::getTime))
174-
.findFirst();
154+
.stream()
155+
.filter(type ->
156+
type.name().equalsIgnoreCase("dateModified")
157+
|| type.name().equalsIgnoreCase("lastModified")
158+
|| type.name().equalsIgnoreCase("modificationDate"))
159+
.map(type -> pidRecord.getPropertyValues(type.pid()))
160+
.map(Arrays::asList)
161+
.flatMap(List<String>::stream)
162+
.map(this::extractDate)
163+
.filter(Optional::isPresent)
164+
.map(Optional::get)
165+
.min(Comparator.comparingLong(Date::getTime));
175166
}
176167

177168
/**
@@ -181,10 +172,10 @@ public Optional<Date> findDateModified(PIDRecord pidRecord) throws IOException {
181172
* @return the extracted Date object.
182173
*/
183174
protected Optional<Date> extractDate(String dateString) {
184-
DateTimeFormatter dateFormatter = ISODateTimeFormat.dateTime();
175+
DateTimeFormatter dateFormatter = DateTimeFormatter.ISO_DATE_TIME;
185176
try {
186-
DateTime dateTime = dateFormatter.parseDateTime(dateString);
187-
return Optional.of(dateTime.toDate());
177+
ZonedDateTime dateTime = ZonedDateTime.parse(dateString, dateFormatter);
178+
return Optional.of(Date.from(dateTime.toInstant()));
188179
} catch (Exception e) {
189180
return Optional.empty();
190181
}
Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
package edu.kit.datamanager.pit.typeregistry;
22

3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.JsonNode;
5+
import com.fasterxml.jackson.databind.node.TextNode;
6+
import com.networknt.schema.JsonSchema;
7+
import com.networknt.schema.ValidationMessage;
8+
import edu.kit.datamanager.pit.Application;
39
import edu.kit.datamanager.pit.typeregistry.schema.SchemaInfo;
4-
import org.everit.json.schema.Schema;
5-
import org.everit.json.schema.ValidationException;
6-
import org.json.JSONObject;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
712

813
import java.util.Collection;
914
import java.util.Objects;
15+
import java.util.Set;
1016

1117
/**
1218
* @param pid the pid of this attribute
@@ -22,23 +28,50 @@ public record AttributeInfo(
2228
String typeName,
2329
Collection<SchemaInfo> jsonSchema
2430
) {
31+
private static final Logger log = LoggerFactory.getLogger(AttributeInfo.class);
32+
2533
public boolean validate(String value) {
2634
return this.jsonSchema().stream()
27-
.map(SchemaInfo::schema)
28-
.filter(Objects::nonNull)
29-
.anyMatch(schema -> validate(schema, value));
35+
.filter(schemaInfo -> schemaInfo.error() == null)
36+
.filter(schemaInfo -> schemaInfo.schema() != null)
37+
.peek(schemaInfo -> log.warn("Found valid schema from {} to validate {} / {}.", schemaInfo.origin(), pid, value))
38+
.anyMatch(schemaInfo -> this.validate(schemaInfo.schema(), value));
3039
}
3140

32-
private boolean validate(Schema schema, String value) {
33-
Object toValidate = value;
34-
if (value.startsWith("{")) {
35-
toValidate = new JSONObject(value);
36-
}
41+
private boolean validate(JsonSchema schema, String value) {
3742
try {
38-
schema.validate(toValidate);
39-
} catch (ValidationException e) {
43+
JsonNode toValidate = valueToJsonNode(value);
44+
Set<ValidationMessage> errors = schema.validate(toValidate, executionContext -> {
45+
// By default, since Draft 2019-09, the format keyword only generates annotations and not assertions
46+
executionContext.getExecutionConfig().setFormatAssertionsEnabled(true);
47+
});
48+
if (!errors.isEmpty()) {
49+
log.warn("Validation errors for value '{}': {}", value, errors);
50+
}
51+
return errors.isEmpty();
52+
} catch (Exception e) {
53+
log.error("Exception during validation for value '{}': {}", value, e.getMessage(), e);
4054
return false;
4155
}
42-
return true;
4356
}
44-
}
57+
58+
/**
59+
* Converts the given value to a JsonNode.
60+
*
61+
* @param value the value to convert
62+
* @return a JsonNode representation of the value
63+
*/
64+
public static JsonNode valueToJsonNode(String value) {
65+
JsonNode toValidate;
66+
if (value.isBlank()) {
67+
return new TextNode(value);
68+
}
69+
try {
70+
toValidate = Application.jsonObjectMapper().readTree(value);
71+
} catch (JsonProcessingException e) {
72+
log.warn("Failed to parse value '{}' as JSON, treating it as a plain text node.", value);
73+
toValidate = new TextNode(value);
74+
}
75+
return toValidate;
76+
}
77+
}

0 commit comments

Comments
 (0)