From cd2b6936d6f9c6728b0fdca36d174e0d0a662b62 Mon Sep 17 00:00:00 2001 From: Florent Biville Date: Thu, 24 Jul 2025 23:24:47 +0300 Subject: [PATCH] feat: introduce config object, make service loading optional --- .../v1/ImportSpecificationDeserializer.java | 86 ++++---- .../v1/ImportSpecificationSettings.java | 199 ++++++++++++++++++ .../java/org/neo4j/importer/v1/Plugins.java | 113 ++++++++++ ...g.neo4j.importer.v1.actions.ActionProvider | 1 - ...lidator => import-spec-validators.builtin} | 0 5 files changed, 356 insertions(+), 43 deletions(-) create mode 100644 core/src/main/java/org/neo4j/importer/v1/ImportSpecificationSettings.java create mode 100644 core/src/main/java/org/neo4j/importer/v1/Plugins.java delete mode 100644 core/src/main/resources/META-INF/services/org.neo4j.importer.v1.actions.ActionProvider rename core/src/main/resources/{META-INF/services/org.neo4j.importer.v1.validation.SpecificationValidator => import-spec-validators.builtin} (100%) diff --git a/core/src/main/java/org/neo4j/importer/v1/ImportSpecificationDeserializer.java b/core/src/main/java/org/neo4j/importer/v1/ImportSpecificationDeserializer.java index c076449b..9ac25be9 100644 --- a/core/src/main/java/org/neo4j/importer/v1/ImportSpecificationDeserializer.java +++ b/core/src/main/java/org/neo4j/importer/v1/ImportSpecificationDeserializer.java @@ -16,6 +16,8 @@ */ package org.neo4j.importer.v1; +import static org.neo4j.importer.v1.ImportSpecificationSettings.DEFAULT_SETTINGS; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.StreamReadFeature; import com.fasterxml.jackson.databind.JsonNode; @@ -28,18 +30,11 @@ import com.networknt.schema.SpecVersion.VersionFlag; import java.io.IOException; import java.io.Reader; -import java.util.List; -import java.util.Optional; -import java.util.ServiceLoader; -import java.util.ServiceLoader.Provider; -import java.util.stream.Collectors; import org.neo4j.importer.v1.actions.Action; import org.neo4j.importer.v1.actions.ActionDeserializer; -import org.neo4j.importer.v1.actions.ActionProvider; import org.neo4j.importer.v1.distribution.Neo4jDistribution; import org.neo4j.importer.v1.sources.Source; import org.neo4j.importer.v1.sources.SourceDeserializer; -import org.neo4j.importer.v1.sources.SourceProvider; import org.neo4j.importer.v1.validation.ActionError; import org.neo4j.importer.v1.validation.InvalidSpecificationException; import org.neo4j.importer.v1.validation.Neo4jDistributionValidator; @@ -59,6 +54,21 @@ public class ImportSpecificationDeserializer { private static final JsonSchema SCHEMA = JsonSchemaFactory.getInstance(VersionFlag.V202012) .getSchema(ImportSpecificationDeserializer.class.getResourceAsStream("/spec.v1.json")); + @Deprecated + public static ImportSpecification deserialize(Reader spec) throws SpecificationException { + return unpack(spec, DEFAULT_SETTINGS); + } + + @Deprecated + public static ImportSpecification deserialize(Reader spec, Neo4jDistribution neo4jDistribution) + throws SpecificationException { + return unpack( + spec, + ImportSpecificationSettings.builder() + .withRuntimeValidationEnabledAgainst(neo4jDistribution) + .build()); + } + /** * Returns an instance of {@link ImportSpecification} based on the provided {@link Reader} content. * The result is guaranteed to be consistent with the specification JSON schema. @@ -72,26 +82,26 @@ public class ImportSpecificationDeserializer { * @return an {@link ImportSpecification} * @throws SpecificationException if parsing, deserialization or validation fail */ - public static ImportSpecification deserialize(Reader spec) throws SpecificationException { - return deserialize(spec, Optional.empty()); - } - - public static ImportSpecification deserialize(Reader spec, Neo4jDistribution neo4jDistribution) + public static ImportSpecification deserialize(Reader spec, ImportSpecificationSettings settings) throws SpecificationException { - - return deserialize(spec, Optional.of(neo4jDistribution)); + return unpack(spec, settings); } - private static ImportSpecification deserialize( - Reader rawSpecification, Optional neo4jDistribution) throws SpecificationException { + private static ImportSpecification unpack(Reader rawSpecification, ImportSpecificationSettings settings) + throws SpecificationException { - YAMLMapper mapper = initMapper(); + YAMLMapper mapper = initMapper(settings); JsonNode json = parse(mapper, rawSpecification); - validateSchema(SCHEMA, json); + validateSchema(json); ImportSpecification specification = deserialize(mapper, json); - validateStatically(specification); - validateRuntime(specification, neo4jDistribution); + validateStatically(specification, settings); + var neo4jDistribution = settings.getNeo4jDistribution(); + if (neo4jDistribution + .filter(ImportSpecificationDeserializer::isDistributionSupported) + .isPresent()) { + validateRuntime(specification, neo4jDistribution.get()); + } return specification; } @@ -110,13 +120,13 @@ private static ImportSpecification deserialize( */ @Deprecated public static void validateStatically(ImportSpecification specification) throws SpecificationException { - SpecificationValidators.of(loadValidators()).validate(specification); + validateStatically(specification, DEFAULT_SETTINGS); } - private static YAMLMapper initMapper() { + private static YAMLMapper initMapper(ImportSpecificationSettings settings) { var module = new SimpleModule(); - module.addDeserializer(Source.class, new SourceDeserializer(loadProviders(SourceProvider.class))); - module.addDeserializer(Action.class, new ActionDeserializer(loadProviders(ActionProvider.class))); + module.addDeserializer(Source.class, new SourceDeserializer(Plugins.loadSourceProviders(settings))); + module.addDeserializer(Action.class, new ActionDeserializer(Plugins.loadActionProviders(settings))); return YAMLMapper.builder() .addModule(module) .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) @@ -152,9 +162,9 @@ private static ImportSpecification deserialize(ObjectMapper mapper, JsonNode jso } } - private static void validateSchema(JsonSchema schema, JsonNode json) throws InvalidSpecificationException { + private static void validateSchema(JsonNode json) throws InvalidSpecificationException { Builder builder = SpecificationValidationResult.builder(); - schema.validate(json) + SCHEMA.validate(json) .forEach(msg -> builder.addError( msg.getInstanceLocation().toString(), String.format("SCHM-%s", msg.getCode()), @@ -165,27 +175,19 @@ private static void validateSchema(JsonSchema schema, JsonNode json) throws Inva } } - private static List loadValidators() { - return ServiceLoader.load(SpecificationValidator.class).stream() - .map(Provider::get) - .collect(Collectors.toList()); + private static void validateStatically(ImportSpecification specification, ImportSpecificationSettings settings) + throws SpecificationException { + var validators = Plugins.loadSpecificationValidators(settings); + SpecificationValidators.of(validators).validate(specification); } - private static void validateRuntime(ImportSpecification spec, Optional neo4jDistribution) + private static void validateRuntime(ImportSpecification spec, Neo4jDistribution neo4jDistribution) throws SpecificationException { - if (neo4jDistribution - .filter(distribution -> distribution.isVersionLargerThanOrEqual("4.4")) - .isEmpty()) { - return; - } - var runtimeValidator = new Neo4jDistributionValidator(neo4jDistribution.get()); + var runtimeValidator = new Neo4jDistributionValidator(neo4jDistribution); SpecificationValidators.of(runtimeValidator).validate(spec); } - @SuppressWarnings("unchecked") - private static List loadProviders(Class type) { - return ServiceLoader.load(type).stream() - .map(provider -> (T) provider.get()) - .collect(Collectors.toList()); + private static boolean isDistributionSupported(Neo4jDistribution distribution) { + return distribution.isVersionLargerThanOrEqual("4.4"); } } diff --git a/core/src/main/java/org/neo4j/importer/v1/ImportSpecificationSettings.java b/core/src/main/java/org/neo4j/importer/v1/ImportSpecificationSettings.java new file mode 100644 index 00000000..dad150ea --- /dev/null +++ b/core/src/main/java/org/neo4j/importer/v1/ImportSpecificationSettings.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.importer.v1; + +import static java.util.Collections.unmodifiableSet; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; +import org.neo4j.importer.v1.actions.Action; +import org.neo4j.importer.v1.actions.ActionProvider; +import org.neo4j.importer.v1.actions.plugin.CypherActionProvider; +import org.neo4j.importer.v1.distribution.Neo4jDistribution; +import org.neo4j.importer.v1.sources.Source; +import org.neo4j.importer.v1.sources.SourceProvider; +import org.neo4j.importer.v1.targets.EntityTargetExtension; +import org.neo4j.importer.v1.targets.EntityTargetExtensionProvider; +import org.neo4j.importer.v1.validation.SpecificationValidator; + +public class ImportSpecificationSettings { + + public static final ImportSpecificationSettings DEFAULT_SETTINGS = builder().build(); + + private final boolean automaticPluginDiscoveryEnabled; + private final Set> specificationValidators; + private final Set>> sourceProviders; + private final Set>> actionProviders; + private final Set>> + entityTargetExtensionProviders; + private final Neo4jDistribution neo4jDistribution; + + public ImportSpecificationSettings( + boolean automaticPluginDiscoveryEnabled, + Set> specificationValidators, + Set>> sourceProviders, + Set>> actionProviders, + Set>> + entityTargetExtensionProviders, + Neo4jDistribution neo4jDistribution) { + this.automaticPluginDiscoveryEnabled = automaticPluginDiscoveryEnabled; + this.specificationValidators = unmodifiableSet(specificationValidators); + this.sourceProviders = unmodifiableSet(sourceProviders); + this.actionProviders = unmodifiableSet(actionProviders); + this.entityTargetExtensionProviders = unmodifiableSet(entityTargetExtensionProviders); + this.neo4jDistribution = neo4jDistribution; + } + + public static Builder builder() { + return new Builder(); + } + + public boolean isAutomaticPluginDiscoveryEnabled() { + return automaticPluginDiscoveryEnabled; + } + + public Set> getSpecificationValidators() { + return specificationValidators; + } + + public Set>> getSourceProviders() { + return sourceProviders; + } + + public Set>> getActionProviders() { + return actionProviders; + } + + public Set>> + getEntityTargetExtensionProviders() { + return entityTargetExtensionProviders; + } + + public Optional getNeo4jDistribution() { + return Optional.ofNullable(neo4jDistribution); + } + + @Override + public String toString() { + return "ImportSpecificationConfig{" + "enableAutomaticPluginDiscovery=" + + automaticPluginDiscoveryEnabled + ", sourceProviders=" + + sourceProviders + ", actionProviders=" + + actionProviders + ", entityTargetExtensionProviders=" + + entityTargetExtensionProviders + ", neo4jDistribution=" + + neo4jDistribution + '}'; + } + + public static class Builder { + private boolean automaticPluginDiscoveryEnabled = true; + private final Set> specificationValidators = + Plugins.loadBuiltInSpecificationValidators(); + private final Set>> sourceProviders = new HashSet<>(); + private final Set>> actionProviders = + new HashSet<>(Set.of(CypherActionProvider.class)); + private final Set>> + entityTargetExtensionProviders = new HashSet<>(); + private Neo4jDistribution neo4jDistribution; + + private Builder() {} + + public Builder withAutomaticPluginDiscoveryDisabled() { + this.automaticPluginDiscoveryEnabled = false; + return this; + } + + public Builder withAutomaticPluginDiscoveryEnabled() { + this.automaticPluginDiscoveryEnabled = true; + return this; + } + + @SafeVarargs + public final Builder withSpecificationValidators( + Class specificationValidator, Class... otherProviders) { + return withSpecificationValidators(concat(specificationValidator, otherProviders)); + } + + public Builder withSpecificationValidators( + Set> specificationValidators) { + this.specificationValidators.addAll(specificationValidators); + return this; + } + + @SafeVarargs + public final Builder withSourceProviders( + Class> sourceProvider, + Class>... otherProviders) { + return withSourceProviders(concat(sourceProvider, otherProviders)); + } + + public Builder withSourceProviders(Set>> sourceProviders) { + this.sourceProviders.addAll(sourceProviders); + return this; + } + + @SafeVarargs + public final Builder withActionProviders( + Class> actionProvider, + Class>... otherProviders) { + return withActionProviders(concat(actionProvider, otherProviders)); + } + + public Builder withActionProviders(Set>> actionProviders) { + this.actionProviders.addAll(actionProviders); + return this; + } + + @SafeVarargs + public final Builder withEntityTargetExtensionProviders( + Class> + entityTargetExtensionProvider, + Class>... otherProviders) { + return withEntityTargetExtensionProviders(concat(entityTargetExtensionProvider, otherProviders)); + } + + public Builder withEntityTargetExtensionProviders( + Set>> + entityTargetExtensionProviders) { + this.entityTargetExtensionProviders.addAll(entityTargetExtensionProviders); + return this; + } + + public Builder withRuntimeValidationEnabledAgainst(Neo4jDistribution neo4jDistribution) { + this.neo4jDistribution = neo4jDistribution; + return this; + } + + public ImportSpecificationSettings build() { + return new ImportSpecificationSettings( + automaticPluginDiscoveryEnabled, + specificationValidators, + sourceProviders, + actionProviders, + entityTargetExtensionProviders, + neo4jDistribution); + } + + private static Set concat(T sourceProvider, T[] otherProviders) { + var providers = new LinkedHashSet(1 + otherProviders.length); + providers.add(sourceProvider); + providers.addAll(Arrays.asList(otherProviders)); + return providers; + } + } +} diff --git a/core/src/main/java/org/neo4j/importer/v1/Plugins.java b/core/src/main/java/org/neo4j/importer/v1/Plugins.java new file mode 100644 index 00000000..190da60b --- /dev/null +++ b/core/src/main/java/org/neo4j/importer/v1/Plugins.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.importer.v1; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.stream.Collectors; +import org.neo4j.importer.v1.actions.Action; +import org.neo4j.importer.v1.actions.ActionProvider; +import org.neo4j.importer.v1.sources.Source; +import org.neo4j.importer.v1.sources.SourceProvider; +import org.neo4j.importer.v1.validation.SpecificationValidator; + +public class Plugins { + + static List loadSpecificationValidators(ImportSpecificationSettings settings) { + var providers = settings.getSpecificationValidators().stream() + .map(Plugins::instantiate) + .collect(Collectors.toCollection(() -> new ArrayList())); + if (!settings.isAutomaticPluginDiscoveryEnabled()) { + return providers; + } + providers.addAll(discoverServiceImplementations(SpecificationValidator.class)); + return providers; + } + + static List> loadSourceProviders(ImportSpecificationSettings settings) { + var providers = settings.getSourceProviders().stream() + .map(Plugins::instantiate) + .collect(Collectors.toCollection(() -> new ArrayList>())); + if (!settings.isAutomaticPluginDiscoveryEnabled()) { + return providers; + } + providers.addAll(discoverServiceImplementations(SourceProvider.class)); + return providers; + } + + static List> loadActionProviders(ImportSpecificationSettings settings) { + var providers = settings.getActionProviders().stream() + .map(Plugins::instantiate) + .collect(Collectors.toCollection(() -> new ArrayList>())); + if (!settings.isAutomaticPluginDiscoveryEnabled()) { + return providers; + } + providers.addAll(discoverServiceImplementations(ActionProvider.class)); + return providers; + } + + @SuppressWarnings("unchecked") + static Set> loadBuiltInSpecificationValidators() { + try (var stream = Plugins.class.getResourceAsStream("/import-spec-validators.builtin")) { + if (stream == null) { + throw new RuntimeException("Unable to discover built-in specification validators"); + } + try (var reader = new BufferedReader(new InputStreamReader(stream))) { + return reader.lines() + .map(String::trim) + .filter(line -> !line.isEmpty() && !line.startsWith("#")) + .map(type -> (Class) loadClass(type)) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + } catch (IOException e) { + throw new RuntimeException("Error while discovering built-in specification validators", e); + } + } + + @SuppressWarnings("unchecked") + private static List discoverServiceImplementations(Class type) { + return ServiceLoader.load(type).stream() + .map(provider -> (T) provider.get()) + .collect(Collectors.toList()); + } + + private static T instantiate(Class type) { + try { + return type.getDeclaredConstructor().newInstance(); + } catch (InstantiationException + | IllegalAccessException + | InvocationTargetException + | NoSuchMethodException e) { + throw new RuntimeException(String.format("Unable to instantiate plugin class: %s", type), e); + } + } + + private static Class loadClass(String name) { + try { + return Class.forName(name); + } catch (ClassNotFoundException e) { + throw new RuntimeException(String.format("Unable to load plugin class: %s", name), e); + } + } +} diff --git a/core/src/main/resources/META-INF/services/org.neo4j.importer.v1.actions.ActionProvider b/core/src/main/resources/META-INF/services/org.neo4j.importer.v1.actions.ActionProvider deleted file mode 100644 index e480cc55..00000000 --- a/core/src/main/resources/META-INF/services/org.neo4j.importer.v1.actions.ActionProvider +++ /dev/null @@ -1 +0,0 @@ -org.neo4j.importer.v1.actions.plugin.CypherActionProvider diff --git a/core/src/main/resources/META-INF/services/org.neo4j.importer.v1.validation.SpecificationValidator b/core/src/main/resources/import-spec-validators.builtin similarity index 100% rename from core/src/main/resources/META-INF/services/org.neo4j.importer.v1.validation.SpecificationValidator rename to core/src/main/resources/import-spec-validators.builtin