Skip to content

Commit

Permalink
[Enhancement #69] Replace pending references with empty object (with …
Browse files Browse the repository at this point in the history
…id) when ASSUME_TARGET_TYPE is configured.
  • Loading branch information
ledsoft committed Aug 26, 2024
1 parent 4e1a470 commit 6a46ce7
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,31 @@
*/
package cz.cvut.kbss.jsonld.common;

import cz.cvut.kbss.jopa.model.annotations.Id;
import cz.cvut.kbss.jopa.model.annotations.Namespace;
import cz.cvut.kbss.jopa.model.annotations.Namespaces;
import cz.cvut.kbss.jopa.model.annotations.OWLAnnotationProperty;
import cz.cvut.kbss.jopa.model.annotations.OWLClass;
import cz.cvut.kbss.jopa.model.annotations.OWLDataProperty;
import cz.cvut.kbss.jopa.model.annotations.OWLObjectProperty;
import cz.cvut.kbss.jopa.model.annotations.Properties;
import cz.cvut.kbss.jopa.model.annotations.*;
import cz.cvut.kbss.jopa.model.annotations.Types;
import cz.cvut.kbss.jsonld.JsonLd;
import cz.cvut.kbss.jsonld.annotation.JsonLdAttributeOrder;
import cz.cvut.kbss.jsonld.exception.JsonLdSerializationException;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -126,7 +141,7 @@ private static Optional<String> expandIri(String iri, Class<?> declaringClass) {
}
}
return declaringClass.getSuperclass() != null ? expandIri(iri, declaringClass.getSuperclass()) :
Optional.empty();
Optional.empty();
}

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

/**
* Gets the identifier field of the specified class.
* <p>
* That is, gets the field annotated with {@link Id}, even inherited.
*
* @param cls Class whose identifier field to retrieve
* @return Matching field, optionally empty
*/
public static Optional<Field> getIdentifierField(Class<?> cls) {
return getMarshallableFields(cls, (f, c) -> f.isAnnotationPresent(Id.class)).stream().findFirst();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
*/
package cz.cvut.kbss.jsonld.deserialization.expanded;

import cz.cvut.kbss.jsonld.ConfigParam;
import cz.cvut.kbss.jsonld.Configuration;
import cz.cvut.kbss.jsonld.deserialization.DefaultInstanceBuilder;
import cz.cvut.kbss.jsonld.deserialization.DeserializationContext;
import cz.cvut.kbss.jsonld.deserialization.InstanceBuilder;
import cz.cvut.kbss.jsonld.deserialization.JsonLdDeserializer;
import cz.cvut.kbss.jsonld.deserialization.reference.AssumedTypeReferenceReplacer;
import cz.cvut.kbss.jsonld.deserialization.reference.PendingReferenceRegistry;
import cz.cvut.kbss.jsonld.exception.JsonLdDeserializationException;
import jakarta.json.JsonArray;
Expand Down Expand Up @@ -59,6 +61,9 @@ public <T> T deserialize(JsonValue jsonLd, Class<T> resultClass) {
final InstanceBuilder instanceBuilder = new DefaultInstanceBuilder(classResolver, referenceRegistry);
new ObjectDeserializer(instanceBuilder, new DeserializerConfig(configuration(), classResolver, deserializers), resultClass)
.processValue(root);
if (configuration().is(ConfigParam.ASSUME_TARGET_TYPE)) {
new AssumedTypeReferenceReplacer().replacePendingReferencesWithAssumedTypedObjects(referenceRegistry);
}
referenceRegistry.verifyNoUnresolvedReferencesExist();
assert resultClass.isAssignableFrom(instanceBuilder.getCurrentRoot().getClass());
return resultClass.cast(instanceBuilder.getCurrentRoot());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package cz.cvut.kbss.jsonld.deserialization.reference;

import cz.cvut.kbss.jsonld.JsonLd;
import cz.cvut.kbss.jsonld.common.BeanAnnotationProcessor;
import cz.cvut.kbss.jsonld.common.BeanClassProcessor;
import cz.cvut.kbss.jsonld.deserialization.util.DataTypeTransformer;
import cz.cvut.kbss.jsonld.exception.UnknownPropertyException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
* Replaces pending references with objects of assumed target type.
* <p>
* These objects have only identifier value set.
*/
public class AssumedTypeReferenceReplacer {

private static final Logger LOG = LoggerFactory.getLogger(AssumedTypeReferenceReplacer.class);

/**
* Replaces pending references from the specified {@link PendingReferenceRegistry} with empty objects of the assumed
* target type.
* <p>
* The objects will have only the identifiers set.
* <p>
* If unable to determine target type (typically for pending collection item references), the pending reference is
* skipped.
*
* @param registry Registry from which the pending references should be replaced
*/
public void replacePendingReferencesWithAssumedTypedObjects(PendingReferenceRegistry registry) {
Objects.requireNonNull(registry);
final Map<String, Class<?>> idsToTypes = getAssumedTargetTypes(registry);
idsToTypes.forEach((id, type) -> {
final Object instance = BeanClassProcessor.createInstance(type);
final Optional<Field> idField = BeanAnnotationProcessor.getIdentifierField(type);
if (idField.isEmpty()) {
throw UnknownPropertyException.create(JsonLd.ID, type);
}
Object identifier = id;
if (!idField.get().getType().isAssignableFrom(String.class)) {
identifier = DataTypeTransformer.transformValue(id, idField.get().getType());
}
BeanClassProcessor.setFieldValue(idField.get(), instance, identifier);
registry.resolveReferences(id, instance);
});
}

private static Map<String, Class<?>> getAssumedTargetTypes(PendingReferenceRegistry registry) {
final Map<String, Class<?>> idsToTypes = new HashMap<>();
registry.getPendingReferences().forEach((id, refs) -> {
assert refs != null;
assert !refs.isEmpty();
final Optional<Class<?>> targetType =
refs.stream().filter(ref -> ref.getTargetType().isPresent()).findFirst().flatMap(
PendingReference::getTargetType);
if (targetType.isEmpty()) {
LOG.debug("No assumed target type found for reference with id '{}'. Skipping the reference.", id);
} else {
idsToTypes.put(id, targetType.get());
}
});
return idsToTypes;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
*/
package cz.cvut.kbss.jsonld.deserialization.reference;

import java.util.Optional;

/**
* Represents a pending reference.
* <p>
Expand All @@ -35,4 +37,16 @@ public interface PendingReference {
* @param referencedObject The object referenced by this pending reference
*/
void apply(Object referencedObject);

/**
* Gets the target type of the pending reference.
* <p>
* If the target type cannot be reliably determined (for instance, because it is a collection), this method should
* return {@link Optional#empty()}.
*
* @return The target type
*/
default Optional<Class<?>> getTargetType() {
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public void addPendingReference(String identifier, Object targetObject, Field ta
}

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

Expand Down Expand Up @@ -91,4 +91,8 @@ public void verifyNoUnresolvedReferencesExist() {
"There are unresolved references to objects " + pendingReferences.keySet());
}
}

Map<String, Set<PendingReference>> getPendingReferences() {
return Collections.unmodifiableMap(pendingReferences);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import java.lang.reflect.Field;
import java.util.Objects;
import java.util.Optional;

/**
* Represents a singular pending reference.
Expand Down Expand Up @@ -50,6 +51,11 @@ public void apply(Object referencedObject) {
BeanClassProcessor.setFieldValue(targetField, targetObject, referencedObject);
}

@Override
public Optional<Class<?>> getTargetType() {
return Optional.of(targetField.getType());
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public TargetClassResolver(TypeMap typeMap, TargetClassResolverConfig config) {
*/
public <T> Class<? extends T> getTargetClass(Class<T> expectedClass, Collection<String> types) {
if (types.isEmpty() && config.shouldAllowAssumingTargetType()) {
LOG.trace("Assuming target type to be " + expectedClass);
LOG.trace("Assuming target type to be {}", expectedClass);
return expectedClass;
}
final List<Class<?>> candidates = getTargetClassCandidates(types);
Expand Down Expand Up @@ -99,7 +99,7 @@ private void reduceToMostSpecificSubclasses(List<Class<?>> candidates) {

private Class<?> selectFinalTargetClass(List<Class<?>> mostSpecificCandidates, List<Class<?>> candidates,
Collection<String> types) {
assert mostSpecificCandidates.size() > 0;
assert !mostSpecificCandidates.isEmpty();
if (mostSpecificCandidates.size() > 1) {
if (!config.isOptimisticTypeResolutionEnabled()) {
throw ambiguousTargetType(types, mostSpecificCandidates);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package cz.cvut.kbss.jsonld.deserialization.reference;

import cz.cvut.kbss.jsonld.environment.Generator;
import cz.cvut.kbss.jsonld.environment.model.Employee;
import cz.cvut.kbss.jsonld.environment.model.User;
import cz.cvut.kbss.jsonld.exception.UnknownPropertyException;
import org.junit.jupiter.api.Test;

import java.net.URI;
import java.util.ArrayList;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;

class AssumedTypeReferenceReplacerTest {

private final PendingReferenceRegistry registry = new PendingReferenceRegistry();

private final AssumedTypeReferenceReplacer sut = new AssumedTypeReferenceReplacer();

@Test
void replacePendingReferencesWithAssumedTypedObjectsReplacesPendingSingularReferenceWithObjectWithId() throws Exception {
final URI id = Generator.generateUri();
final Employee target = Generator.generateEmployee();
target.setEmployer(null);
registry.addPendingReference(id.toString(), target, Employee.getEmployerField());
sut.replacePendingReferencesWithAssumedTypedObjects(registry);
assertNotNull(target.getEmployer());
assertEquals(id, target.getEmployer().getUri());
}

@Test
void replacePendingReferenceReplacesAllReferencesWithSameObject() throws Exception {
final URI id = Generator.generateUri();
final Employee targetOne = Generator.generateEmployee();
targetOne.setEmployer(null);
registry.addPendingReference(id.toString(), targetOne, Employee.getEmployerField());
final Employee targetTwo = Generator.generateEmployee();
targetTwo.setEmployer(null);
registry.addPendingReference(id.toString(), targetTwo, Employee.getEmployerField());
sut.replacePendingReferencesWithAssumedTypedObjects(registry);
assertNotNull(targetOne.getEmployer());
assertEquals(id, targetOne.getEmployer().getUri());
assertNotNull(targetTwo.getEmployer());
assertSame(targetOne.getEmployer(), targetTwo.getEmployer());
}

@Test
void replacePendingReferencesThrowsUnknownPropertyExceptionWhenTargetTypeDoesNotHaveIdentifierField() throws Exception {
final URI id = Generator.generateUri();
final Employee target = Generator.generateEmployee();
registry.addPendingReference(id.toString(), target, User.getUsernameField());
assertThrows(UnknownPropertyException.class, () -> sut.replacePendingReferencesWithAssumedTypedObjects(registry));
}

@Test
void replacePendingReferencesSkipsPendingReferencesWithoutClearTargetType() {
final URI id = Generator.generateUri();
registry.addPendingReference(id.toString(), new ArrayList<>());

sut.replacePendingReferencesWithAssumedTypedObjects(registry);
assertFalse(registry.getPendingReferences().isEmpty());
}
}

0 comments on commit 6a46ce7

Please sign in to comment.