Skip to content

Commit 6a46ce7

Browse files
committed
[Enhancement #69] Replace pending references with empty object (with id) when ASSUME_TARGET_TYPE is configured.
1 parent 4e1a470 commit 6a46ce7

File tree

8 files changed

+196
-6
lines changed

8 files changed

+196
-6
lines changed

src/main/java/cz/cvut/kbss/jsonld/common/BeanAnnotationProcessor.java

+26-3
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,31 @@
1717
*/
1818
package cz.cvut.kbss.jsonld.common;
1919

20+
import cz.cvut.kbss.jopa.model.annotations.Id;
21+
import cz.cvut.kbss.jopa.model.annotations.Namespace;
22+
import cz.cvut.kbss.jopa.model.annotations.Namespaces;
23+
import cz.cvut.kbss.jopa.model.annotations.OWLAnnotationProperty;
24+
import cz.cvut.kbss.jopa.model.annotations.OWLClass;
25+
import cz.cvut.kbss.jopa.model.annotations.OWLDataProperty;
26+
import cz.cvut.kbss.jopa.model.annotations.OWLObjectProperty;
2027
import cz.cvut.kbss.jopa.model.annotations.Properties;
21-
import cz.cvut.kbss.jopa.model.annotations.*;
28+
import cz.cvut.kbss.jopa.model.annotations.Types;
2229
import cz.cvut.kbss.jsonld.JsonLd;
2330
import cz.cvut.kbss.jsonld.annotation.JsonLdAttributeOrder;
2431
import cz.cvut.kbss.jsonld.exception.JsonLdSerializationException;
2532

2633
import java.lang.reflect.AnnotatedElement;
2734
import java.lang.reflect.Field;
2835
import java.lang.reflect.Modifier;
29-
import java.util.*;
36+
import java.util.ArrayList;
37+
import java.util.Arrays;
38+
import java.util.Comparator;
39+
import java.util.HashSet;
40+
import java.util.List;
41+
import java.util.Map;
42+
import java.util.Objects;
43+
import java.util.Optional;
44+
import java.util.Set;
3045
import java.util.function.BiPredicate;
3146
import java.util.function.Function;
3247
import java.util.stream.Collectors;
@@ -126,7 +141,7 @@ private static Optional<String> expandIri(String iri, Class<?> declaringClass) {
126141
}
127142
}
128143
return declaringClass.getSuperclass() != null ? expandIri(iri, declaringClass.getSuperclass()) :
129-
Optional.empty();
144+
Optional.empty();
130145
}
131146

132147
private static Optional<String> resolveNamespace(AnnotatedElement annotated, String prefix) {
@@ -418,6 +433,14 @@ public static String getAttributeIdentifier(Field field) {
418433
throw new JsonLdSerializationException("Field " + field + " is not JSON-LD serializable.");
419434
}
420435

436+
/**
437+
* Gets the identifier field of the specified class.
438+
* <p>
439+
* That is, gets the field annotated with {@link Id}, even inherited.
440+
*
441+
* @param cls Class whose identifier field to retrieve
442+
* @return Matching field, optionally empty
443+
*/
421444
public static Optional<Field> getIdentifierField(Class<?> cls) {
422445
return getMarshallableFields(cls, (f, c) -> f.isAnnotationPresent(Id.class)).stream().findFirst();
423446
}

src/main/java/cz/cvut/kbss/jsonld/deserialization/expanded/ExpandedJsonLdDeserializer.java

+5
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
*/
1818
package cz.cvut.kbss.jsonld.deserialization.expanded;
1919

20+
import cz.cvut.kbss.jsonld.ConfigParam;
2021
import cz.cvut.kbss.jsonld.Configuration;
2122
import cz.cvut.kbss.jsonld.deserialization.DefaultInstanceBuilder;
2223
import cz.cvut.kbss.jsonld.deserialization.DeserializationContext;
2324
import cz.cvut.kbss.jsonld.deserialization.InstanceBuilder;
2425
import cz.cvut.kbss.jsonld.deserialization.JsonLdDeserializer;
26+
import cz.cvut.kbss.jsonld.deserialization.reference.AssumedTypeReferenceReplacer;
2527
import cz.cvut.kbss.jsonld.deserialization.reference.PendingReferenceRegistry;
2628
import cz.cvut.kbss.jsonld.exception.JsonLdDeserializationException;
2729
import jakarta.json.JsonArray;
@@ -59,6 +61,9 @@ public <T> T deserialize(JsonValue jsonLd, Class<T> resultClass) {
5961
final InstanceBuilder instanceBuilder = new DefaultInstanceBuilder(classResolver, referenceRegistry);
6062
new ObjectDeserializer(instanceBuilder, new DeserializerConfig(configuration(), classResolver, deserializers), resultClass)
6163
.processValue(root);
64+
if (configuration().is(ConfigParam.ASSUME_TARGET_TYPE)) {
65+
new AssumedTypeReferenceReplacer().replacePendingReferencesWithAssumedTypedObjects(referenceRegistry);
66+
}
6267
referenceRegistry.verifyNoUnresolvedReferencesExist();
6368
assert resultClass.isAssignableFrom(instanceBuilder.getCurrentRoot().getClass());
6469
return resultClass.cast(instanceBuilder.getCurrentRoot());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package cz.cvut.kbss.jsonld.deserialization.reference;
2+
3+
import cz.cvut.kbss.jsonld.JsonLd;
4+
import cz.cvut.kbss.jsonld.common.BeanAnnotationProcessor;
5+
import cz.cvut.kbss.jsonld.common.BeanClassProcessor;
6+
import cz.cvut.kbss.jsonld.deserialization.util.DataTypeTransformer;
7+
import cz.cvut.kbss.jsonld.exception.UnknownPropertyException;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
11+
import java.lang.reflect.Field;
12+
import java.util.HashMap;
13+
import java.util.Map;
14+
import java.util.Objects;
15+
import java.util.Optional;
16+
17+
/**
18+
* Replaces pending references with objects of assumed target type.
19+
* <p>
20+
* These objects have only identifier value set.
21+
*/
22+
public class AssumedTypeReferenceReplacer {
23+
24+
private static final Logger LOG = LoggerFactory.getLogger(AssumedTypeReferenceReplacer.class);
25+
26+
/**
27+
* Replaces pending references from the specified {@link PendingReferenceRegistry} with empty objects of the assumed
28+
* target type.
29+
* <p>
30+
* The objects will have only the identifiers set.
31+
* <p>
32+
* If unable to determine target type (typically for pending collection item references), the pending reference is
33+
* skipped.
34+
*
35+
* @param registry Registry from which the pending references should be replaced
36+
*/
37+
public void replacePendingReferencesWithAssumedTypedObjects(PendingReferenceRegistry registry) {
38+
Objects.requireNonNull(registry);
39+
final Map<String, Class<?>> idsToTypes = getAssumedTargetTypes(registry);
40+
idsToTypes.forEach((id, type) -> {
41+
final Object instance = BeanClassProcessor.createInstance(type);
42+
final Optional<Field> idField = BeanAnnotationProcessor.getIdentifierField(type);
43+
if (idField.isEmpty()) {
44+
throw UnknownPropertyException.create(JsonLd.ID, type);
45+
}
46+
Object identifier = id;
47+
if (!idField.get().getType().isAssignableFrom(String.class)) {
48+
identifier = DataTypeTransformer.transformValue(id, idField.get().getType());
49+
}
50+
BeanClassProcessor.setFieldValue(idField.get(), instance, identifier);
51+
registry.resolveReferences(id, instance);
52+
});
53+
}
54+
55+
private static Map<String, Class<?>> getAssumedTargetTypes(PendingReferenceRegistry registry) {
56+
final Map<String, Class<?>> idsToTypes = new HashMap<>();
57+
registry.getPendingReferences().forEach((id, refs) -> {
58+
assert refs != null;
59+
assert !refs.isEmpty();
60+
final Optional<Class<?>> targetType =
61+
refs.stream().filter(ref -> ref.getTargetType().isPresent()).findFirst().flatMap(
62+
PendingReference::getTargetType);
63+
if (targetType.isEmpty()) {
64+
LOG.debug("No assumed target type found for reference with id '{}'. Skipping the reference.", id);
65+
} else {
66+
idsToTypes.put(id, targetType.get());
67+
}
68+
});
69+
return idsToTypes;
70+
}
71+
}

src/main/java/cz/cvut/kbss/jsonld/deserialization/reference/PendingReference.java

+14
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
*/
1818
package cz.cvut.kbss.jsonld.deserialization.reference;
1919

20+
import java.util.Optional;
21+
2022
/**
2123
* Represents a pending reference.
2224
* <p>
@@ -35,4 +37,16 @@ public interface PendingReference {
3537
* @param referencedObject The object referenced by this pending reference
3638
*/
3739
void apply(Object referencedObject);
40+
41+
/**
42+
* Gets the target type of the pending reference.
43+
* <p>
44+
* If the target type cannot be reliably determined (for instance, because it is a collection), this method should
45+
* return {@link Optional#empty()}.
46+
*
47+
* @return The target type
48+
*/
49+
default Optional<Class<?>> getTargetType() {
50+
return Optional.empty();
51+
}
3852
}

src/main/java/cz/cvut/kbss/jsonld/deserialization/reference/PendingReferenceRegistry.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public void addPendingReference(String identifier, Object targetObject, Field ta
4444
}
4545

4646
private void addReference(String identifier, PendingReference reference) {
47-
final Set<PendingReference> refs = pendingReferences.computeIfAbsent(identifier, (id) -> new HashSet<>());
47+
final Set<PendingReference> refs = pendingReferences.computeIfAbsent(identifier, (id) -> new LinkedHashSet<>());
4848
refs.add(reference);
4949
}
5050

@@ -91,4 +91,8 @@ public void verifyNoUnresolvedReferencesExist() {
9191
"There are unresolved references to objects " + pendingReferences.keySet());
9292
}
9393
}
94+
95+
Map<String, Set<PendingReference>> getPendingReferences() {
96+
return Collections.unmodifiableMap(pendingReferences);
97+
}
9498
}

src/main/java/cz/cvut/kbss/jsonld/deserialization/reference/SingularPendingReference.java

+6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import java.lang.reflect.Field;
2424
import java.util.Objects;
25+
import java.util.Optional;
2526

2627
/**
2728
* Represents a singular pending reference.
@@ -50,6 +51,11 @@ public void apply(Object referencedObject) {
5051
BeanClassProcessor.setFieldValue(targetField, targetObject, referencedObject);
5152
}
5253

54+
@Override
55+
public Optional<Class<?>> getTargetType() {
56+
return Optional.of(targetField.getType());
57+
}
58+
5359
@Override
5460
public boolean equals(Object o) {
5561
if (this == o) {

src/main/java/cz/cvut/kbss/jsonld/deserialization/util/TargetClassResolver.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public TargetClassResolver(TypeMap typeMap, TargetClassResolverConfig config) {
6363
*/
6464
public <T> Class<? extends T> getTargetClass(Class<T> expectedClass, Collection<String> types) {
6565
if (types.isEmpty() && config.shouldAllowAssumingTargetType()) {
66-
LOG.trace("Assuming target type to be " + expectedClass);
66+
LOG.trace("Assuming target type to be {}", expectedClass);
6767
return expectedClass;
6868
}
6969
final List<Class<?>> candidates = getTargetClassCandidates(types);
@@ -99,7 +99,7 @@ private void reduceToMostSpecificSubclasses(List<Class<?>> candidates) {
9999

100100
private Class<?> selectFinalTargetClass(List<Class<?>> mostSpecificCandidates, List<Class<?>> candidates,
101101
Collection<String> types) {
102-
assert mostSpecificCandidates.size() > 0;
102+
assert !mostSpecificCandidates.isEmpty();
103103
if (mostSpecificCandidates.size() > 1) {
104104
if (!config.isOptimisticTypeResolutionEnabled()) {
105105
throw ambiguousTargetType(types, mostSpecificCandidates);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package cz.cvut.kbss.jsonld.deserialization.reference;
2+
3+
import cz.cvut.kbss.jsonld.environment.Generator;
4+
import cz.cvut.kbss.jsonld.environment.model.Employee;
5+
import cz.cvut.kbss.jsonld.environment.model.User;
6+
import cz.cvut.kbss.jsonld.exception.UnknownPropertyException;
7+
import org.junit.jupiter.api.Test;
8+
9+
import java.net.URI;
10+
import java.util.ArrayList;
11+
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertFalse;
14+
import static org.junit.jupiter.api.Assertions.assertNotNull;
15+
import static org.junit.jupiter.api.Assertions.assertSame;
16+
import static org.junit.jupiter.api.Assertions.assertThrows;
17+
18+
class AssumedTypeReferenceReplacerTest {
19+
20+
private final PendingReferenceRegistry registry = new PendingReferenceRegistry();
21+
22+
private final AssumedTypeReferenceReplacer sut = new AssumedTypeReferenceReplacer();
23+
24+
@Test
25+
void replacePendingReferencesWithAssumedTypedObjectsReplacesPendingSingularReferenceWithObjectWithId() throws Exception {
26+
final URI id = Generator.generateUri();
27+
final Employee target = Generator.generateEmployee();
28+
target.setEmployer(null);
29+
registry.addPendingReference(id.toString(), target, Employee.getEmployerField());
30+
sut.replacePendingReferencesWithAssumedTypedObjects(registry);
31+
assertNotNull(target.getEmployer());
32+
assertEquals(id, target.getEmployer().getUri());
33+
}
34+
35+
@Test
36+
void replacePendingReferenceReplacesAllReferencesWithSameObject() throws Exception {
37+
final URI id = Generator.generateUri();
38+
final Employee targetOne = Generator.generateEmployee();
39+
targetOne.setEmployer(null);
40+
registry.addPendingReference(id.toString(), targetOne, Employee.getEmployerField());
41+
final Employee targetTwo = Generator.generateEmployee();
42+
targetTwo.setEmployer(null);
43+
registry.addPendingReference(id.toString(), targetTwo, Employee.getEmployerField());
44+
sut.replacePendingReferencesWithAssumedTypedObjects(registry);
45+
assertNotNull(targetOne.getEmployer());
46+
assertEquals(id, targetOne.getEmployer().getUri());
47+
assertNotNull(targetTwo.getEmployer());
48+
assertSame(targetOne.getEmployer(), targetTwo.getEmployer());
49+
}
50+
51+
@Test
52+
void replacePendingReferencesThrowsUnknownPropertyExceptionWhenTargetTypeDoesNotHaveIdentifierField() throws Exception {
53+
final URI id = Generator.generateUri();
54+
final Employee target = Generator.generateEmployee();
55+
registry.addPendingReference(id.toString(), target, User.getUsernameField());
56+
assertThrows(UnknownPropertyException.class, () -> sut.replacePendingReferencesWithAssumedTypedObjects(registry));
57+
}
58+
59+
@Test
60+
void replacePendingReferencesSkipsPendingReferencesWithoutClearTargetType() {
61+
final URI id = Generator.generateUri();
62+
registry.addPendingReference(id.toString(), new ArrayList<>());
63+
64+
sut.replacePendingReferencesWithAssumedTypedObjects(registry);
65+
assertFalse(registry.getPendingReferences().isEmpty());
66+
}
67+
}

0 commit comments

Comments
 (0)