Skip to contentMethod: reduceToMostSpecificSubclasses(List)
1: /*
2: * JB4JSON-LD
3: * Copyright (C) 2023 Czech Technical University in Prague
4: *
5: * This library is free software; you can redistribute it and/or
6: * modify it under the terms of the GNU Lesser General Public
7: * License as published by the Free Software Foundation; either
8: * version 3.0 of the License, or (at your option) any later version.
9: *
10: * This library is distributed in the hope that it will be useful,
11: * but WITHOUT ANY WARRANTY; without even the implied warranty of
12: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13: * Lesser General Public License for more details.
14: *
15: * You should have received a copy of the GNU Lesser General Public
16: * License along with this library.
17: */
18: package cz.cvut.kbss.jsonld.deserialization.util;
19:
20: import cz.cvut.kbss.jopa.model.annotations.OWLClass;
21: import cz.cvut.kbss.jsonld.common.BeanAnnotationProcessor;
22: import cz.cvut.kbss.jsonld.exception.AmbiguousTargetTypeException;
23: import cz.cvut.kbss.jsonld.exception.TargetTypeException;
24: import org.slf4j.Logger;
25: import org.slf4j.LoggerFactory;
26:
27: import java.lang.reflect.Modifier;
28: import java.util.ArrayList;
29: import java.util.Collection;
30: import java.util.List;
31: import java.util.stream.Collectors;
32:
33: /**
34: * Resolves the type of instance into which a JSON-LD object will be deserialized.
35: */
36: public class TargetClassResolver {
37:
38: private static final Logger LOG = LoggerFactory.getLogger(TargetClassResolver.class);
39:
40: private final TypeMap typeMap;
41:
42: private final TargetClassResolverConfig config;
43:
44: public TargetClassResolver(TypeMap typeMap) {
45: this.typeMap = typeMap;
46: this.config = new TargetClassResolverConfig();
47: }
48:
49: public TargetClassResolver(TypeMap typeMap, TargetClassResolverConfig config) {
50: this.typeMap = typeMap;
51: this.config = config;
52: }
53:
54: /**
55: * Resolves object deserialization target class based on the specified type info.
56: *
57: * @param <T> The type of the expected target class
58: * @param expectedClass Expected class as specified by deserialization return type of field type
59: * @param types Types of the JSON-LD object to deserialize
60: * @return Resolved target class. It has to be a subtype of the {@code expectedClass}
61: * @throws TargetTypeException If the resulting candidate is not assignable to the expected class or it cannot be
62: * determined
63: */
64: public <T> Class<? extends T> getTargetClass(Class<T> expectedClass, Collection<String> types) {
65: if (types.isEmpty() && config.shouldAllowAssumingTargetType()) {
66: LOG.trace("Assuming target type to be " + expectedClass);
67: return expectedClass;
68: }
69: final List<Class<?>> candidates = getTargetClassCandidates(types);
70: final Class<?> targetCandidate;
71: reduceTargetClassCandidates(expectedClass, candidates);
72: final List<Class<?>> reducedCandidates = new ArrayList<>(candidates);
73: reduceToMostSpecificSubclasses(candidates);
74: if (candidates.isEmpty()) {
75: if (doesExpectedClassMatchesTypes(expectedClass, types)) {
76: targetCandidate = expectedClass;
77: } else {
78: throw new TargetTypeException(
79: "Neither " + expectedClass + " nor any of its subclasses matches the types " + types + ".");
80: }
81: } else {
82: targetCandidate = selectFinalTargetClass(candidates, reducedCandidates, types);
83: }
84: assert expectedClass.isAssignableFrom(targetCandidate);
85: return (Class<? extends T>) targetCandidate;
86: }
87:
88: private List<Class<?>> getTargetClassCandidates(Collection<String> types) {
89: return types.stream().flatMap(t -> typeMap.get(t).stream()).collect(Collectors.toList());
90: }
91:
92: private void reduceTargetClassCandidates(Class<?> expectedClass, List<Class<?>> candidates) {
93: candidates.removeIf(c -> !expectedClass.isAssignableFrom(c) || Modifier.isAbstract(c.getModifiers()));
94: }
95:
96: private void reduceToMostSpecificSubclasses(List<Class<?>> candidates) {
97: candidates.removeIf(cls -> candidates.stream().anyMatch(c -> !cls.equals(c) && cls.isAssignableFrom(c)));
98: }
99:
100: private Class<?> selectFinalTargetClass(List<Class<?>> mostSpecificCandidates, List<Class<?>> candidates,
101: Collection<String> types) {
102: assert mostSpecificCandidates.size() > 0;
103: if (mostSpecificCandidates.size() > 1) {
104: if (!config.isOptimisticTypeResolutionEnabled()) {
105: throw ambiguousTargetType(types, mostSpecificCandidates);
106: }
107: if (config.shouldPreferSuperclass()) {
108: return selectTargetClassWithSuperclassPreference(mostSpecificCandidates, candidates);
109: }
110: }
111: return pickOne(mostSpecificCandidates);
112: }
113:
114: private static Class<?> pickOne(List<Class<?>> candidates) {
115: return candidates.size() == 1 ? candidates.get(0) :
116: candidates.stream().filter(BeanAnnotationProcessor::hasPropertiesField).findFirst()
117: .orElse(candidates.get(0));
118: }
119:
120: private Class<?> selectTargetClassWithSuperclassPreference(List<Class<?>> mostSpecificCandidates,
121: List<Class<?>> candidates) {
122: candidates.removeAll(mostSpecificCandidates);
123: reduceToMostGeneralSuperclasses(candidates);
124: return pickOne(candidates);
125: }
126:
127: private void reduceToMostGeneralSuperclasses(List<Class<?>> candidates) {
128: candidates.removeIf(cls -> candidates.stream().anyMatch(c -> !cls.equals(c) && c.isAssignableFrom(cls)));
129: }
130:
131: private static AmbiguousTargetTypeException ambiguousTargetType(Collection<String> types,
132: List<Class<?>> candidates) {
133: return new AmbiguousTargetTypeException(
134: "Object with types " + types + " matches multiple equivalent target classes: " + candidates);
135: }
136:
137: private boolean doesExpectedClassMatchesTypes(Class<?> expectedClass, Collection<String> types) {
138: final OWLClass owlClass = expectedClass.getDeclaredAnnotation(OWLClass.class);
139: return owlClass != null && types.contains(owlClass.iri());
140: }
141: }