diff --git a/tools/federation/src/main/java/org/eclipse/rdf4j/federated/repository/FedXConfigParser.java b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/repository/FedXConfigParser.java new file mode 100644 index 00000000000..249be3be50a --- /dev/null +++ b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/repository/FedXConfigParser.java @@ -0,0 +1,129 @@ +/******************************************************************************* + * Copyright (c) 2024 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +package org.eclipse.rdf4j.federated.repository; + +import static org.eclipse.rdf4j.federated.repository.FedXRepositoryConfig.NAMESPACE; + +import org.eclipse.rdf4j.federated.FedXConfig; +import org.eclipse.rdf4j.model.BNode; +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.Model; +import org.eclipse.rdf4j.model.Resource; +import org.eclipse.rdf4j.model.ValueFactory; +import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.model.util.Models; +import org.eclipse.rdf4j.model.util.Values; +import org.eclipse.rdf4j.repository.config.RepositoryConfigException; + +/** + * A parser & exporter of {@link FedXConfig} to fine-tune FedX repositories when configured via + * {@link FedXRepositoryConfig}. + * + * @author Iotic Labs + */ +public class FedXConfigParser { + + private static final ValueFactory vf = SimpleValueFactory.getInstance(); + + /** + * IRI of the property populating {@link FedXConfig#getEnforceMaxQueryTime()} + */ + public static final IRI CONFIG_ENFORCE_MAX_QUERY_TIME = vf.createIRI(NAMESPACE, "enforceMaxQueryTime"); + + /** + * IRI of the property populating {@link FedXConfig#isEnableMonitoring()} + */ + public static final IRI CONFIG_ENABLE_MONITORING = vf.createIRI(NAMESPACE, "enableMonitoring"); + + /** + * IRI of the property populating {@link FedXConfig#isLogQueryPlan()} + */ + public static final IRI CONFIG_LOG_QUERY_PLAN = vf.createIRI(NAMESPACE, "logQueryPlan"); + + /** + * IRI of the property populating {@link FedXConfig#isDebugQueryPlan()} + */ + public static final IRI CONFIG_DEBUG_QUERY_PLAN = vf.createIRI(NAMESPACE, "debugQueryPlan"); + + /** + * IRI of the property populating {@link FedXConfig#isLogQueries()} + */ + public static final IRI CONFIG_LOG_QUERIES = vf.createIRI(NAMESPACE, "logQueries"); + + /** + * IRI of the property populating {@link FedXConfig#getSourceSelectionCacheSpec()} + */ + public static final IRI CONFIG_SOURCE_SELECTION_CACHE_SPEC = vf.createIRI(NAMESPACE, "sourceSelectionCacheSpec"); + + private FedXConfigParser() { + } + + /** + * Updates the provided {@link FedXConfig} with properties from the supplied model. + * + * @param config the configuration to be amended. + * @param m the model from which to read configuration properties + * @param confNode the subject against which to expect {@link FedXConfig} overrides. + * + * @return The updated {@link FedXConfig} + * + * @throws RepositoryConfigException if any of the overridden fields are deemed to be invalid + */ + public static FedXConfig parse(FedXConfig config, Model m, Resource confNode) throws RepositoryConfigException { + Models.objectLiteral(m.getStatements(confNode, CONFIG_ENFORCE_MAX_QUERY_TIME, null)) + .ifPresent(value -> config.withEnforceMaxQueryTime(value.intValue())); + + Models.objectLiteral(m.getStatements(confNode, CONFIG_ENABLE_MONITORING, null)) + .ifPresent(value -> config.withEnableMonitoring(value.booleanValue())); + + Models.objectLiteral(m.getStatements(confNode, CONFIG_LOG_QUERY_PLAN, null)) + .ifPresent(value -> config.withLogQueryPlan(value.booleanValue())); + + Models.objectLiteral(m.getStatements(confNode, CONFIG_DEBUG_QUERY_PLAN, null)) + .ifPresent(value -> config.withDebugQueryPlan(value.booleanValue())); + + Models.objectLiteral(m.getStatements(confNode, CONFIG_LOG_QUERIES, null)) + .ifPresent(value -> config.withLogQueries(value.booleanValue())); + + Models.objectLiteral(m.getStatements(confNode, CONFIG_SOURCE_SELECTION_CACHE_SPEC, null)) + .ifPresent(value -> config.withSourceSelectionCacheSpec(value.stringValue())); + + return config; + } + + /** + * Export the provided {@link FedXConfig} to its RDF representation. + * + * @param config the configuration to export + * @param m the model to which to write configuration properties + * + * @return the node against which the configuration has been written + */ + public static Resource export(FedXConfig config, Model m) { + BNode confNode = Values.bnode(); + + m.add(confNode, CONFIG_ENFORCE_MAX_QUERY_TIME, vf.createLiteral(config.getEnforceMaxQueryTime())); + + m.add(confNode, CONFIG_ENABLE_MONITORING, vf.createLiteral(config.isEnableMonitoring())); + + m.add(confNode, CONFIG_LOG_QUERY_PLAN, vf.createLiteral(config.isLogQueryPlan())); + + m.add(confNode, CONFIG_DEBUG_QUERY_PLAN, vf.createLiteral(config.isDebugQueryPlan())); + + m.add(confNode, CONFIG_LOG_QUERIES, vf.createLiteral(config.isLogQueries())); + + if (config.getSourceSelectionCacheSpec() != null) { + m.add(confNode, CONFIG_SOURCE_SELECTION_CACHE_SPEC, vf.createLiteral(config.getSourceSelectionCacheSpec())); + } + + return confNode; + } +} diff --git a/tools/federation/src/main/java/org/eclipse/rdf4j/federated/repository/FedXRepositoryConfig.java b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/repository/FedXRepositoryConfig.java index a91e4a9f841..fb21d9bce47 100644 --- a/tools/federation/src/main/java/org/eclipse/rdf4j/federated/repository/FedXRepositoryConfig.java +++ b/tools/federation/src/main/java/org/eclipse/rdf4j/federated/repository/FedXRepositoryConfig.java @@ -60,6 +60,12 @@ * # optionally define data config * #fedx:fedxConfig "fedxConfig.prop" ; * fedx:dataConfig "dataConfig.ttl" ; + * + * # optionally define FedXConfig overrides + * fedx:config [ + * fedx:sourceSelectionCacheSpec "maximumSize=0" ; + * fedx:enforceMaxQueryTime 30 ; + * ] * ]; * rep:repositoryID "fedx" ; * rdfs:label "FedX Federation" . @@ -87,6 +93,11 @@ public class FedXRepositoryConfig extends AbstractRepositoryImplConfig { */ public static final IRI DATA_CONFIG = vf.createIRI(NAMESPACE, "dataConfig"); + /** + * IRI of the property pointing to the {@link FedXConfig} + */ + public static final IRI FEDX_CONFIG = vf.createIRI(NAMESPACE, "config"); + /** * IRI of the property pointing to a federation member node */ @@ -152,6 +163,11 @@ public Resource export(Model m) { m.add(implNode, DATA_CONFIG, vf.createLiteral(getDataConfig())); } + if (getConfig() != null) { + Resource confNode = FedXConfigParser.export(getConfig(), m); + m.add(implNode, FEDX_CONFIG, confNode); + } + if (getMembers() != null) { Model members = getMembers(); @@ -187,6 +203,14 @@ public void parse(Model m, Resource implNode) throws RepositoryConfigException { Models.objectLiteral(m.getStatements(implNode, DATA_CONFIG, null)) .ifPresent(value -> setDataConfig(value.stringValue())); + Models.objectResource(m.getStatements(implNode, FEDX_CONFIG, null)) + .ifPresent(res -> { + if (getConfig() == null) { + setConfig(new FedXConfig()); + } + setConfig(FedXConfigParser.parse(getConfig(), m, res)); + }); + Set memberNodes = m.filter(implNode, MEMBER, null).objects(); if (!memberNodes.isEmpty()) { Model members = new TreeModel(); diff --git a/tools/federation/src/test/java/org/eclipse/rdf4j/federated/repository/FedXConfigParserTest.java b/tools/federation/src/test/java/org/eclipse/rdf4j/federated/repository/FedXConfigParserTest.java new file mode 100644 index 00000000000..270c708c1be --- /dev/null +++ b/tools/federation/src/test/java/org/eclipse/rdf4j/federated/repository/FedXConfigParserTest.java @@ -0,0 +1,130 @@ +/******************************************************************************* + * Copyright (c) 2024 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +package org.eclipse.rdf4j.federated.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; + +import org.eclipse.rdf4j.federated.FedXConfig; +import org.eclipse.rdf4j.model.Model; +import org.eclipse.rdf4j.model.Resource; +import org.eclipse.rdf4j.model.impl.TreeModel; +import org.eclipse.rdf4j.model.util.Models; +import org.eclipse.rdf4j.model.util.Values; +import org.eclipse.rdf4j.rio.RDFFormat; +import org.eclipse.rdf4j.rio.Rio; +import org.junit.jupiter.api.Test; + +public class FedXConfigParserTest { + + @Test + public void testParse() throws Exception { + Model model = readConfig("/tests/rdf4jserver/config-fedXConfig-only.ttl"); + + FedXConfig config = FedXConfigParser.parse(new FedXConfig(), model, Values.iri("http://example.org/conf")); + + assertThat(config.getEnforceMaxQueryTime()).isEqualTo(1234); + assertThat(config.isEnableMonitoring()).isTrue(); + assertThat(config.isLogQueryPlan()).isTrue(); + assertThat(config.isDebugQueryPlan()).isTrue(); + assertThat(config.isLogQueries()).isTrue(); + assertThat(config.getSourceSelectionCacheSpec()).isEqualTo("spec-goes-here"); + } + + @Test + public void testParseWithEmptyConfig() throws Exception { + Model model = new TreeModel(); + + FedXConfig config = FedXConfigParser.parse(new FedXConfig(), model, Values.iri("http://example.org/conf")); + + // expecting defaults + assertThat(config.getEnforceMaxQueryTime()).isEqualTo(30); + assertThat(config.isEnableMonitoring()).isFalse(); + assertThat(config.isLogQueryPlan()).isFalse(); + assertThat(config.isDebugQueryPlan()).isFalse(); + assertThat(config.isLogQueries()).isFalse(); + assertThat(config.getSourceSelectionCacheSpec()).isNull(); + } + + @Test + public void testExport() throws Exception { + Model model = readConfig("/tests/rdf4jserver/config-fedXConfig-only.ttl"); + + FedXConfig config = FedXConfigParser.parse(new FedXConfig(), model, Values.iri("http://example.org/conf")); + + Model export = new TreeModel(); + Resource configNode = FedXConfigParser.export(config, export); + + assertThat(export.filter(configNode, null, null)).hasSize(6); + + assertThat( + Models.objectLiteral( + export.getStatements(configNode, FedXConfigParser.CONFIG_ENFORCE_MAX_QUERY_TIME, null))) + .hasValueSatisfying(v -> assertThat(v.intValue()).isEqualTo(1234)); + assertThat( + Models.objectLiteral( + export.getStatements(configNode, FedXConfigParser.CONFIG_ENABLE_MONITORING, null))) + .hasValueSatisfying(v -> assertThat(v.booleanValue()).isTrue()); + assertThat( + Models.objectLiteral( + export.getStatements(configNode, FedXConfigParser.CONFIG_LOG_QUERY_PLAN, null))) + .hasValueSatisfying(v -> assertThat(v.booleanValue()).isTrue()); + assertThat( + Models.objectLiteral( + export.getStatements(configNode, FedXConfigParser.CONFIG_DEBUG_QUERY_PLAN, null))) + .hasValueSatisfying(v -> assertThat(v.booleanValue()).isTrue()); + assertThat( + Models.objectLiteral( + export.getStatements(configNode, FedXConfigParser.CONFIG_LOG_QUERIES, null))) + .hasValueSatisfying(v -> assertThat(v.booleanValue()).isTrue()); + assertThat( + Models.objectLiteral( + export.getStatements(configNode, FedXConfigParser.CONFIG_SOURCE_SELECTION_CACHE_SPEC, null))) + .hasValueSatisfying(v -> assertThat(v.stringValue()).isEqualTo("spec-goes-here")); + } + + @Test + public void testExportWithEmptyConfig() throws Exception { + Model export = new TreeModel(); + Resource configNode = FedXConfigParser.export(new FedXConfig(), export); + + // Note: 5 instead of 6 since CONFIG_SOURCE_SELECTION_CACHE_SPEC is null and thus should not be populated + assertThat(export.filter(configNode, null, null)).hasSize(5); + + assertThat( + Models.objectLiteral( + export.getStatements(configNode, FedXConfigParser.CONFIG_ENFORCE_MAX_QUERY_TIME, null))) + .hasValueSatisfying(v -> assertThat(v.intValue()).isEqualTo(30)); + assertThat( + Models.objectLiteral( + export.getStatements(configNode, FedXConfigParser.CONFIG_ENABLE_MONITORING, null))) + .hasValueSatisfying(v -> assertThat(v.booleanValue()).isFalse()); + assertThat( + Models.objectLiteral( + export.getStatements(configNode, FedXConfigParser.CONFIG_LOG_QUERY_PLAN, null))) + .hasValueSatisfying(v -> assertThat(v.booleanValue()).isFalse()); + assertThat( + Models.objectLiteral( + export.getStatements(configNode, FedXConfigParser.CONFIG_DEBUG_QUERY_PLAN, null))) + .hasValueSatisfying(v -> assertThat(v.booleanValue()).isFalse()); + assertThat( + Models.objectLiteral( + export.getStatements(configNode, FedXConfigParser.CONFIG_LOG_QUERIES, null))) + .hasValueSatisfying(v -> assertThat(v.booleanValue()).isFalse()); + } + + protected Model readConfig(String configResource) throws Exception { + try (InputStream in = FedXRepositoryConfigTest.class.getResourceAsStream(configResource)) { + return Rio.parse(in, "http://example.org/", RDFFormat.TURTLE); + } + } +} diff --git a/tools/federation/src/test/java/org/eclipse/rdf4j/federated/repository/FedXRepositoryConfigTest.java b/tools/federation/src/test/java/org/eclipse/rdf4j/federated/repository/FedXRepositoryConfigTest.java index 685450e7784..3e8b9d1814b 100644 --- a/tools/federation/src/test/java/org/eclipse/rdf4j/federated/repository/FedXRepositoryConfigTest.java +++ b/tools/federation/src/test/java/org/eclipse/rdf4j/federated/repository/FedXRepositoryConfigTest.java @@ -14,12 +14,15 @@ import static org.eclipse.rdf4j.model.util.Models.subject; import java.io.InputStream; +import java.util.Optional; +import org.eclipse.rdf4j.federated.FedXConfig; import org.eclipse.rdf4j.federated.util.Vocabulary.FEDX; import org.eclipse.rdf4j.model.Model; import org.eclipse.rdf4j.model.Resource; import org.eclipse.rdf4j.model.Value; import org.eclipse.rdf4j.model.impl.TreeModel; +import org.eclipse.rdf4j.model.util.Models; import org.eclipse.rdf4j.repository.config.RepositoryConfigSchema; import org.eclipse.rdf4j.rio.RDFFormat; import org.eclipse.rdf4j.rio.Rio; @@ -36,6 +39,7 @@ public void testParseConfig() throws Exception { config.parse(model, implNode(model)); Assertions.assertNull(config.getDataConfig()); + Assertions.assertNull(config.getConfig()); Model members = config.getMembers(); assertThat(members.filter(null, FEDX.STORE, null).size()).isEqualTo(2); @@ -57,6 +61,40 @@ public void testParseConfig_DataConfig() throws Exception { } + @Test + public void testParseConfig_FedXConfig() throws Exception { + Model model = readConfig("/tests/rdf4jserver/config-withFedXConfig.ttl"); + + FedXRepositoryConfig config = new FedXRepositoryConfig(); + config.parse(model, implNode(model)); + + FedXConfig fedXConf = config.getConfig(); + Assertions.assertNotNull(fedXConf); + + // Spot-check only: per-property testing is covered in FedXConfigParserTest + assertThat(fedXConf.getEnforceMaxQueryTime()).isEqualTo(42); + assertThat(fedXConf.isEnableMonitoring()).isTrue(); + // A non-overridden option + assertThat(fedXConf.isDebugQueryPlan()).isFalse(); + + Assertions.assertNull(config.getMembers()); + Assertions.assertNull(config.getDataConfig()); + } + + @Test + public void testParseConfig_FedXConfig_Overrides_Existing_Config() throws Exception { + Model model = readConfig("/tests/rdf4jserver/config-withFedXConfig.ttl"); + + FedXRepositoryConfig config = new FedXRepositoryConfig(); + config.setConfig(new FedXConfig().withEnforceMaxQueryTime(33)); + config.parse(model, implNode(model)); + + FedXConfig fedXConf = config.getConfig(); + Assertions.assertNotNull(fedXConf); + // Read config should take precedence over pre-read state + assertThat(fedXConf.getEnforceMaxQueryTime()).isEqualTo(42); + } + @Test public void testExport() throws Exception { Model model = readConfig("/tests/rdf4jserver/config.ttl"); @@ -70,10 +108,33 @@ public void testExport() throws Exception { assertThat(export.filter(implNode, FedXRepositoryConfig.MEMBER, null).size()).isEqualTo(2); + assertThat(export.filter(implNode, FedXRepositoryConfig.FEDX_CONFIG, null)).isEmpty(); + assertThat(export.filter(null, FEDX.REPOSITORY_NAME, null).objects().stream().map(Value::stringValue)) .containsExactly("endpoint1", "endpoint2"); } + @Test + public void testExport_FedXConfig() throws Exception { + Model model = readConfig("/tests/rdf4jserver/config-withFedXConfig.ttl"); + + FedXRepositoryConfig config = new FedXRepositoryConfig(); + config.parse(model, implNode(model)); + + // export into model + Model export = new TreeModel(); + Resource implNode = config.export(export); + + // Spot-check only: per-property testing is covered in FedXConfigParserTest + Optional confNode = Models + .objectResource(export.getStatements(implNode, FedXRepositoryConfig.FEDX_CONFIG, null)); + assertThat(confNode).hasValueSatisfying(node -> { + assertThat(Models + .objectLiteral(export.getStatements(node, FedXConfigParser.CONFIG_ENFORCE_MAX_QUERY_TIME, null))) + .hasValueSatisfying(v -> assertThat(v.intValue()).isEqualTo(42)); + }); + } + protected Model readConfig(String configResource) throws Exception { try (InputStream in = FedXRepositoryConfigTest.class.getResourceAsStream(configResource)) { return Rio.parse(in, "http://example.org/", RDFFormat.TURTLE); diff --git a/tools/federation/src/test/resources/tests/rdf4jserver/config-fedXConfig-only.ttl b/tools/federation/src/test/resources/tests/rdf4jserver/config-fedXConfig-only.ttl new file mode 100644 index 00000000000..3b8d0d715cd --- /dev/null +++ b/tools/federation/src/test/resources/tests/rdf4jserver/config-fedXConfig-only.ttl @@ -0,0 +1,16 @@ +# +# RDF4J FedXConfig overrides example +# +@prefix ex: . +@prefix rdfs: . +@prefix rep: . +@prefix fedx: . + +# All overrides should have non-default values for test purposes +ex:conf + fedx:enforceMaxQueryTime 1234 ; + fedx:enableMonitoring true ; + fedx:logQueryPlan true ; + fedx:debugQueryPlan true ; + fedx:logQueries true ; + fedx:sourceSelectionCacheSpec "spec-goes-here" . diff --git a/tools/federation/src/test/resources/tests/rdf4jserver/config-withFedXConfig.ttl b/tools/federation/src/test/resources/tests/rdf4jserver/config-withFedXConfig.ttl new file mode 100644 index 00000000000..35a50f0042c --- /dev/null +++ b/tools/federation/src/test/resources/tests/rdf4jserver/config-withFedXConfig.ttl @@ -0,0 +1,17 @@ +# +# RDF4J configuration template for a FedX Repository with FedXConfig overrides +# +@prefix rdfs: . +@prefix rep: . +@prefix fedx: . + +[] a rep:Repository ; + rep:repositoryImpl [ + rep:repositoryType "fedx:FedXRepository" ; + fedx:config [ + fedx:enforceMaxQueryTime 42 ; + fedx:enableMonitoring true ; + ]; + ]; + rep:repositoryID "my-federation" ; + rdfs:label "FedX Federation" .