Skip to content

Commit

Permalink
[Enhancement #68] Cache type map for deserialization to prevent repea…
Browse files Browse the repository at this point in the history
…ted classpath scanning.
  • Loading branch information
ledsoft committed Aug 26, 2024
1 parent 6a46ce7 commit 8bbd765
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 21 deletions.
35 changes: 25 additions & 10 deletions src/main/java/cz/cvut/kbss/jsonld/ConfigParam.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ public enum ConfigParam {
/**
* Whether to require an identifier when serializing an object.
* <p>
* If set to {@code true} and no identifier is found (either there is no identifier field or its value is {@code
* null}), an exception will be thrown. If configured to {@code false}, a blank node identifier is generated if no
* id is present.
* If set to {@code true} and no identifier is found (either there is no identifier field or its value is
* {@code null}), an exception will be thrown. If configured to {@code false}, a blank node identifier is generated
* if no id is present.
*/
REQUIRE_ID("requireId"),

Expand All @@ -59,8 +59,8 @@ public enum ConfigParam {
* Enables optimistic target type resolution.
* <p>
* This means that when a an ambiguous target type is encountered during deserialization of an object (i.e.,
* multiple concrete classes match the data type), instead of throwing an {@link
* cz.cvut.kbss.jsonld.exception.AmbiguousTargetTypeException}, one of the classes will be selected for
* multiple concrete classes match the data type), instead of throwing an
* {@link cz.cvut.kbss.jsonld.exception.AmbiguousTargetTypeException}, one of the classes will be selected for
* instantiation.
* <p>
* Note that enabling this behavior should probably be done together with setting {@link #IGNORE_UNKNOWN_PROPERTIES}
Expand Down Expand Up @@ -98,19 +98,34 @@ public enum ConfigParam {
/**
* Format string used to serialize and deserialize datetime values.
* <p>
* Note that if {@link #SERIALIZE_DATETIME_AS_MILLIS} is enabled, this parameter has no effect on serialization of datetime.
* Note that if {@link #SERIALIZE_DATETIME_AS_MILLIS} is enabled, this parameter has no effect on serialization of
* datetime.
* <p>
* Also note that this format applies only to full datetime values. Date or time values have to be formatted per-attribute.
* Also note that this format applies only to full datetime values. Date or time values have to be formatted
* per-attribute.
*/
DATE_TIME_FORMAT("datetimeFormat"),

/**
* Whether to serialize individuals using expanded term definition in context.
*
* <p>
* This basically means that the individual's identifier is provided directly as a string and an expanded term
* definition (consisting of a {@literal @id} and {@literal @type}) is added into the context, specifying that the string is an identifier.
* definition (consisting of a {@literal @id} and {@literal @type}) is added into the context, specifying that the
* string is an identifier.
*/
SERIALIZE_INDIVIDUALS_USING_EXPANDED_DEFINITION("serializeIndividualsUsingExpandedDefinition"),

/**
* Whether to disable the type map cache.
* <p>
* Type map is used to map JSON-LD types to Java classes for deserialization. Since this map is built by scanning
* the classpath (see the {@link #SCAN_PACKAGE} parameter), it is cached by default to avoid repeated scanning of
* the classpath.
* <p>
* If every deserializer instance should get a fresh type map based on the current configuration, disable this
* cache.
*/
SERIALIZE_INDIVIDUALS_USING_EXPANDED_DEFINITION("serializeIndividualsUsingExpandedDefinition");
DISABLE_TYPE_MAP_CACHE("disableTypeMapCache");

private final String name;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
*/
public abstract class JsonLdDeserializer implements Configured {

private static final TypeMap TYPE_MAP = new TypeMap();

private final Configuration configuration;

protected final TargetClassResolver classResolver;
Expand All @@ -53,19 +55,33 @@ protected JsonLdDeserializer(Configuration configuration) {
}

private TargetClassResolver initializeTargetClassResolver() {
final TypeMap typeMap = new TypeMap();
final String scanPath = configuration.get(ConfigParam.SCAN_PACKAGE, "");
final TypeMap typeMap = discoverAvailableTypes(scanPath, configuration.is(ConfigParam.DISABLE_TYPE_MAP_CACHE));
return new TargetClassResolver(typeMap,
new TargetClassResolverConfig(
configuration.is(ConfigParam.ASSUME_TARGET_TYPE),
configuration().is(ConfigParam.ENABLE_OPTIMISTIC_TARGET_TYPE_RESOLUTION),
configuration().is(ConfigParam.PREFER_SUPERCLASS)));
}

/**
* Finds potential deserialization target types on the classpath.
*
* @param scanPath Path to scan on classpath
* @return Map of types to Java classes
*/
private static TypeMap discoverAvailableTypes(String scanPath, boolean disableCache) {
final TypeMap map = disableCache ? new TypeMap() : TYPE_MAP;
if (!map.isEmpty()) {
return map;
}
new ClasspathScanner(c -> {
final OWLClass ann = c.getDeclaredAnnotation(OWLClass.class);
if (ann != null) {
typeMap.register(BeanAnnotationProcessor.expandIriIfNecessary(ann.iri(), c), c);
map.register(BeanAnnotationProcessor.expandIriIfNecessary(ann.iri(), c), c);
}
}).processClasses(scanPath);
return new TargetClassResolver(typeMap,
new TargetClassResolverConfig(
configuration.is(ConfigParam.ASSUME_TARGET_TYPE),
configuration().is(ConfigParam.ENABLE_OPTIMISTIC_TARGET_TYPE_RESOLUTION),
configuration().is(ConfigParam.PREFER_SUPERCLASS)));
return map;
}

@Override
Expand All @@ -78,9 +94,9 @@ public Configuration configuration() {
* <p>
* If a deserializer already existed for the type, it is replaced by the new one.
*
* @param type Target type to register the deserializer for
* @param type Target type to register the deserializer for
* @param deserializer Deserializer to register
* @param <T> Target type
* @param <T> Target type
*/
public <T> void registerDeserializer(Class<T> type, ValueDeserializer<T> deserializer) {
Objects.requireNonNull(type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,19 @@
*/
package cz.cvut.kbss.jsonld.deserialization.util;

import java.util.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
* Represents a map of type IRIs to their mapped Java classes.
* <p>
* Used by the deserialization when determining target class from JSON-LD object types.
* <p>
* This class is not synchronized, as it is expected that types will be registered by one thread once and then only queried.
* Read access is not synchronized, as it is expected that types will be registered by one thread once and then only
* queried.
*/
public class TypeMap {

Expand All @@ -41,4 +46,8 @@ public synchronized void register(String type, Class<?> cls) {
public Set<Class<?>> get(String type) {
return typeMap.getOrDefault(type, Collections.emptySet());
}

public synchronized boolean isEmpty() {
return typeMap.isEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import static cz.cvut.kbss.jsonld.environment.TestUtil.readAndExpand;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
Expand All @@ -45,6 +46,7 @@ class JsonLdDeserializerTest {
void constructionScansClasspathAndBuildsTypeMap() throws Exception {
final Configuration config = new Configuration();
config.set(ConfigParam.SCAN_PACKAGE, "cz.cvut.kbss.jsonld");
config.set(ConfigParam.DISABLE_TYPE_MAP_CACHE, "true");
final JsonLdDeserializer deserializer = JsonLdDeserializer.createExpandedDeserializer(config);
assertFalse(typeMap(deserializer).get(Vocabulary.STUDY).isEmpty());
assertTrue(typeMap(deserializer).get(Vocabulary.STUDY).contains(Study.class));
Expand All @@ -60,6 +62,7 @@ private TypeMap typeMap(JsonLdDeserializer deserializer) throws Exception {
void constructionScansClasspathWithSpecifiedPackageAndBuildsTypeMap() throws Exception {
final Configuration config = new Configuration();
config.set(ConfigParam.SCAN_PACKAGE, "cz.cvut.kbss.jsonld.deserialization");
config.set(ConfigParam.DISABLE_TYPE_MAP_CACHE, "true");
final JsonLdDeserializer deserializer = JsonLdDeserializer.createExpandedDeserializer(config);
assertTrue(typeMap(deserializer).get(Vocabulary.GENERIC_MEMBER).isEmpty());
assertTrue(typeMap(deserializer).get(Vocabulary.AGENT).contains(TestClass.class));
Expand All @@ -82,8 +85,38 @@ void deserializationThrowsJsonLdDeserializationExceptionWhenInputIsNotValidJsonL
void constructionExpandsCompactIrisWhenBuildingTypeMap() throws Exception {
final Configuration config = new Configuration();
config.set(ConfigParam.SCAN_PACKAGE, "cz.cvut.kbss.jsonld.environment.model");
config.set(ConfigParam.DISABLE_TYPE_MAP_CACHE, "true");
final JsonLdDeserializer deserializer = JsonLdDeserializer.createExpandedDeserializer(config);
assertFalse(typeMap(deserializer).get(Vocabulary.STUDY).isEmpty());
assertThat(typeMap(deserializer).get(Vocabulary.STUDY), hasItem(StudyWithNamespaces.class));
}

@Test
void constructionCachesTypeMapByDefault() throws Exception {
final Configuration config = new Configuration();
config.set(ConfigParam.SCAN_PACKAGE, "cz.cvut.kbss.jsonld");
final JsonLdDeserializer deserializerOne = JsonLdDeserializer.createExpandedDeserializer(config);
final TypeMap typeMapOne = typeMap(deserializerOne);
assertThat(typeMapOne.get(Vocabulary.STUDY), hasItem(Study.class));
config.set(ConfigParam.SCAN_PACKAGE, "cz.cvut.kbss.jsonld.deserialization");
final JsonLdDeserializer deserializerTwo = JsonLdDeserializer.createExpandedDeserializer(config);
final TypeMap typeMapTwo = typeMap(deserializerTwo);
// If classpath scanning occurred again, the updated scan package would be too specific for Study
assertThat(typeMapTwo.get(Vocabulary.STUDY), hasItem(Study.class));
}

@Test
void constructionBuildsFreshTypeMapWhenTypeMapCacheIsDisabled() throws Exception {
final Configuration config = new Configuration();
config.set(ConfigParam.SCAN_PACKAGE, "cz.cvut.kbss.jsonld");
final JsonLdDeserializer deserializerOne = JsonLdDeserializer.createExpandedDeserializer(config);
final TypeMap typeMapOne = typeMap(deserializerOne);
assertThat(typeMapOne.get(Vocabulary.STUDY), hasItem(Study.class));
config.set(ConfigParam.SCAN_PACKAGE, "cz.cvut.kbss.jsonld.deserialization");
config.set(ConfigParam.DISABLE_TYPE_MAP_CACHE, "true");
final JsonLdDeserializer deserializerTwo = JsonLdDeserializer.createExpandedDeserializer(config);
final TypeMap typeMapTwo = typeMap(deserializerTwo);
// If classpath scanning occurred again, the updated scan package would be too specific for Study
assertThat(typeMapTwo.get(Vocabulary.STUDY), not(hasItem(Study.class)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ private static Map<URI, User> initUsers() {
void setUp() {
final Configuration config = new Configuration();
config.set(ConfigParam.SCAN_PACKAGE, "cz.cvut.kbss.jsonld");
config.set(ConfigParam.DISABLE_TYPE_MAP_CACHE, "true");
this.sut = JsonLdDeserializer.createExpandedDeserializer(config);
}

Expand Down

0 comments on commit 8bbd765

Please sign in to comment.