This is an automated email from the ASF dual-hosted git repository.

terrymanu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shardingsphere.git


The following commit(s) were added to refs/heads/master by this push:
     new 1821958ecbd Refactor rule configuration reference validation (#39920)
1821958ecbd is described below

commit 1821958ecbd48f2c3495570e9c6d50fcdbd8f953
Author: Liang Zhang <[email protected]>
AuthorDate: Tue Sep 22 14:27:30 2026 +0800

    Refactor rule configuration reference validation (#39920)
    
    * Refactor WhereClauseShardingConditionEngine
    
    * Refactor rule configuration reference validation
---
 .../encrypt/config/EncryptRuleConfiguration.java   |   5 +-
 .../EncryptRuleConfigurationValidator.java         |  60 -------
 .../validator/ValidEncryptRuleConfiguration.java   |  57 -------
 .../config/EncryptRuleConfigurationTest.java       |   9 +
 .../mask/config/MaskRuleConfiguration.java         |   4 +-
 .../validator/MaskRuleConfigurationValidator.java  |  49 ------
 .../mask/config/MaskRuleConfigurationTest.java     |   8 +
 .../ReadwriteSplittingRuleConfiguration.java       |   4 +-
 ...adwriteSplittingRuleConfigurationValidator.java |  47 -----
 .../ValidReadwriteSplittingRuleConfiguration.java  |  57 -------
 .../ReadwriteSplittingRuleConfigurationTest.java   |  13 ++
 .../reference/ConfigurationReferenceExists.java    |  36 +++-
 .../ConfigurationReferenceExistsValidator.java     | 133 +++++++++++++++
 .../ConfigurationReferenceExistsValidatorTest.java | 190 +++++++++++++++++++++
 14 files changed, 389 insertions(+), 283 deletions(-)

diff --git 
a/features/encrypt/api/src/main/java/org/apache/shardingsphere/encrypt/config/EncryptRuleConfiguration.java
 
b/features/encrypt/api/src/main/java/org/apache/shardingsphere/encrypt/config/EncryptRuleConfiguration.java
index 71ad246c497..f25aaabdac8 100644
--- 
a/features/encrypt/api/src/main/java/org/apache/shardingsphere/encrypt/config/EncryptRuleConfiguration.java
+++ 
b/features/encrypt/api/src/main/java/org/apache/shardingsphere/encrypt/config/EncryptRuleConfiguration.java
@@ -21,10 +21,10 @@ import com.cedarsoftware.util.CaseInsensitiveSet;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
 import 
org.apache.shardingsphere.encrypt.config.rule.EncryptTableRuleConfiguration;
-import 
org.apache.shardingsphere.encrypt.config.validator.ValidEncryptRuleConfiguration;
 import 
org.apache.shardingsphere.infra.algorithm.core.config.AlgorithmConfiguration;
 import 
org.apache.shardingsphere.infra.config.rule.function.EnhancedRuleConfiguration;
 import 
org.apache.shardingsphere.infra.config.rule.scope.DatabaseRuleConfiguration;
+import 
org.apache.shardingsphere.infra.config.rule.validator.constraint.reference.ConfigurationReferenceExists;
 import 
org.apache.shardingsphere.infra.config.rule.validator.constraint.spi.SPITypeExists;
 import 
org.apache.shardingsphere.infra.config.rule.validator.group.RuleConfigurationTypeValidationGroup;
 
@@ -40,7 +40,8 @@ import java.util.stream.Collectors;
  */
 @RequiredArgsConstructor
 @Getter
-@ValidEncryptRuleConfiguration(groups = 
RuleConfigurationTypeValidationGroup.class)
+@ConfigurationReferenceExists(referencePaths = 
{"tables.columns.cipher.encryptorName", 
"tables.columns.assistedQuery.encryptorName", 
"tables.columns.likeQuery.encryptorName"}, pool = "encryptors",
+        groups = RuleConfigurationTypeValidationGroup.class)
 public final class EncryptRuleConfiguration implements 
DatabaseRuleConfiguration, EnhancedRuleConfiguration {
     
     @NotNull
diff --git 
a/features/encrypt/api/src/main/java/org/apache/shardingsphere/encrypt/config/validator/EncryptRuleConfigurationValidator.java
 
b/features/encrypt/api/src/main/java/org/apache/shardingsphere/encrypt/config/validator/EncryptRuleConfigurationValidator.java
deleted file mode 100644
index 1f54d6aaa10..00000000000
--- 
a/features/encrypt/api/src/main/java/org/apache/shardingsphere/encrypt/config/validator/EncryptRuleConfigurationValidator.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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.apache.shardingsphere.encrypt.config.validator;
-
-import org.apache.shardingsphere.encrypt.config.EncryptRuleConfiguration;
-import 
org.apache.shardingsphere.encrypt.config.rule.EncryptColumnItemRuleConfiguration;
-import 
org.apache.shardingsphere.encrypt.config.rule.EncryptColumnRuleConfiguration;
-import 
org.apache.shardingsphere.encrypt.config.rule.EncryptTableRuleConfiguration;
-
-import javax.validation.ConstraintValidator;
-import javax.validation.ConstraintValidatorContext;
-
-/**
- * Encrypt rule configuration validator.
- */
-public final class EncryptRuleConfigurationValidator implements 
ConstraintValidator<ValidEncryptRuleConfiguration, EncryptRuleConfiguration> {
-    
-    @Override
-    public boolean isValid(final EncryptRuleConfiguration value, final 
ConstraintValidatorContext context) {
-        return value.getTables().stream().allMatch(each -> isTableValid(each, 
value, context));
-    }
-    
-    private boolean isTableValid(final EncryptTableRuleConfiguration 
tableConfig, final EncryptRuleConfiguration ruleConfig, final 
ConstraintValidatorContext context) {
-        return tableConfig.getColumns().stream().allMatch(each -> 
isColumnValid(each, ruleConfig, context));
-    }
-    
-    private boolean isColumnValid(final EncryptColumnRuleConfiguration 
columnConfig, final EncryptRuleConfiguration ruleConfig, final 
ConstraintValidatorContext context) {
-        if (!isEncryptorReferenceValid(columnConfig.getCipher(), ruleConfig, 
context)) {
-            return false;
-        }
-        if (columnConfig.getAssistedQuery().isPresent() && 
!isEncryptorReferenceValid(columnConfig.getAssistedQuery().get(), ruleConfig, 
context)) {
-            return false;
-        }
-        return !columnConfig.getLikeQuery().isPresent() || 
isEncryptorReferenceValid(columnConfig.getLikeQuery().get(), ruleConfig, 
context);
-    }
-    
-    private boolean isEncryptorReferenceValid(final 
EncryptColumnItemRuleConfiguration columnItemConfig, final 
EncryptRuleConfiguration ruleConfig, final ConstraintValidatorContext context) {
-        if 
(ruleConfig.getEncryptors().containsKey(columnItemConfig.getEncryptorName())) {
-            return true;
-        }
-        context.disableDefaultConstraintViolation();
-        context.buildConstraintViolationWithTemplate(String.format("references 
unconfigured encryptor `%s`", 
columnItemConfig.getEncryptorName())).addPropertyNode("tables").addConstraintViolation();
-        return false;
-    }
-}
diff --git 
a/features/encrypt/api/src/main/java/org/apache/shardingsphere/encrypt/config/validator/ValidEncryptRuleConfiguration.java
 
b/features/encrypt/api/src/main/java/org/apache/shardingsphere/encrypt/config/validator/ValidEncryptRuleConfiguration.java
deleted file mode 100644
index aa87f2799ee..00000000000
--- 
a/features/encrypt/api/src/main/java/org/apache/shardingsphere/encrypt/config/validator/ValidEncryptRuleConfiguration.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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.apache.shardingsphere.encrypt.config.validator;
-
-import javax.validation.Constraint;
-import javax.validation.Payload;
-import java.lang.annotation.Documented;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Valid encrypt rule configuration constraint.
- */
-@Target(ElementType.TYPE)
-@Retention(RetentionPolicy.RUNTIME)
-@Constraint(validatedBy = EncryptRuleConfigurationValidator.class)
-@Documented
-public @interface ValidEncryptRuleConfiguration {
-    
-    /**
-     * Get message.
-     *
-     * @return message
-     */
-    String message() default "contains invalid references";
-    
-    /**
-     * Get groups.
-     *
-     * @return groups
-     */
-    Class<?>[] groups() default {};
-    
-    /**
-     * Get payload.
-     *
-     * @return payload
-     */
-    Class<? extends Payload>[] payload() default {};
-}
diff --git 
a/features/encrypt/api/src/test/java/org/apache/shardingsphere/encrypt/config/EncryptRuleConfigurationTest.java
 
b/features/encrypt/api/src/test/java/org/apache/shardingsphere/encrypt/config/EncryptRuleConfigurationTest.java
index 627b5e66f9c..2439035ab56 100644
--- 
a/features/encrypt/api/src/test/java/org/apache/shardingsphere/encrypt/config/EncryptRuleConfigurationTest.java
+++ 
b/features/encrypt/api/src/test/java/org/apache/shardingsphere/encrypt/config/EncryptRuleConfigurationTest.java
@@ -36,6 +36,7 @@ import java.util.Properties;
 import java.util.stream.Stream;
 
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.is;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -93,6 +94,14 @@ class EncryptRuleConfigurationTest {
         assertThrows(InvalidRuleConfigurationException.class, () -> 
RuleConfigurationValidator.validate(ruleConfig));
     }
     
+    @Test
+    void assertUnconfiguredEncryptorViolation() {
+        EncryptRuleConfiguration ruleConfig = createRuleConfiguration(new 
EncryptColumnRuleConfiguration(
+                "foo_col", new 
EncryptColumnItemRuleConfiguration("foo_cipher", "bar_encryptor")));
+        InvalidRuleConfigurationException actual = 
assertThrows(InvalidRuleConfigurationException.class, () -> 
RuleConfigurationValidator.validate(ruleConfig));
+        assertThat(actual.getMessage(), containsString("Property `tables` 
references unconfigured encryptors `bar_encryptor`."));
+    }
+    
     private static Stream<Arguments> invalidRuleConfigurationArguments() {
         EncryptColumnRuleConfiguration invalidAssistedQueryName = 
createValidColumnConfiguration();
         invalidAssistedQueryName.setAssistedQuery(new 
EncryptColumnItemRuleConfiguration("", "foo_encryptor"));
diff --git 
a/features/mask/api/src/main/java/org/apache/shardingsphere/mask/config/MaskRuleConfiguration.java
 
b/features/mask/api/src/main/java/org/apache/shardingsphere/mask/config/MaskRuleConfiguration.java
index 436d4c64f33..6685ff93404 100644
--- 
a/features/mask/api/src/main/java/org/apache/shardingsphere/mask/config/MaskRuleConfiguration.java
+++ 
b/features/mask/api/src/main/java/org/apache/shardingsphere/mask/config/MaskRuleConfiguration.java
@@ -23,10 +23,10 @@ import lombok.RequiredArgsConstructor;
 import 
org.apache.shardingsphere.infra.algorithm.core.config.AlgorithmConfiguration;
 import 
org.apache.shardingsphere.infra.config.rule.function.EnhancedRuleConfiguration;
 import 
org.apache.shardingsphere.infra.config.rule.scope.DatabaseRuleConfiguration;
+import 
org.apache.shardingsphere.infra.config.rule.validator.constraint.reference.ConfigurationReferenceExists;
 import 
org.apache.shardingsphere.infra.config.rule.validator.constraint.spi.SPITypeExists;
 import 
org.apache.shardingsphere.infra.config.rule.validator.group.RuleConfigurationTypeValidationGroup;
 import org.apache.shardingsphere.mask.config.rule.MaskTableRuleConfiguration;
-import 
org.apache.shardingsphere.mask.config.validator.ValidMaskRuleConfiguration;
 
 import javax.validation.Valid;
 import javax.validation.constraints.NotBlank;
@@ -40,7 +40,7 @@ import java.util.stream.Collectors;
  */
 @RequiredArgsConstructor
 @Getter
-@ValidMaskRuleConfiguration(groups = 
RuleConfigurationTypeValidationGroup.class)
+@ConfigurationReferenceExists(referencePaths = "tables.columns.maskAlgorithm", 
pool = "maskAlgorithms", groups = RuleConfigurationTypeValidationGroup.class)
 public final class MaskRuleConfiguration implements DatabaseRuleConfiguration, 
EnhancedRuleConfiguration {
     
     @NotNull
diff --git 
a/features/mask/api/src/main/java/org/apache/shardingsphere/mask/config/validator/MaskRuleConfigurationValidator.java
 
b/features/mask/api/src/main/java/org/apache/shardingsphere/mask/config/validator/MaskRuleConfigurationValidator.java
deleted file mode 100644
index ff30672097c..00000000000
--- 
a/features/mask/api/src/main/java/org/apache/shardingsphere/mask/config/validator/MaskRuleConfigurationValidator.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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.apache.shardingsphere.mask.config.validator;
-
-import org.apache.shardingsphere.mask.config.MaskRuleConfiguration;
-import org.apache.shardingsphere.mask.config.rule.MaskColumnRuleConfiguration;
-import org.apache.shardingsphere.mask.config.rule.MaskTableRuleConfiguration;
-
-import javax.validation.ConstraintValidator;
-import javax.validation.ConstraintValidatorContext;
-
-/**
- * Mask rule configuration validator.
- */
-public final class MaskRuleConfigurationValidator implements 
ConstraintValidator<ValidMaskRuleConfiguration, MaskRuleConfiguration> {
-    
-    @Override
-    public boolean isValid(final MaskRuleConfiguration value, final 
ConstraintValidatorContext context) {
-        return value.getTables().stream().allMatch(each -> isTableValid(each, 
value, context));
-    }
-    
-    private boolean isTableValid(final MaskTableRuleConfiguration tableConfig, 
final MaskRuleConfiguration ruleConfig, final ConstraintValidatorContext 
context) {
-        return tableConfig.getColumns().stream().allMatch(each -> 
isColumnValid(each, ruleConfig, context));
-    }
-    
-    private boolean isColumnValid(final MaskColumnRuleConfiguration 
columnConfig, final MaskRuleConfiguration ruleConfig, final 
ConstraintValidatorContext context) {
-        if 
(ruleConfig.getMaskAlgorithms().containsKey(columnConfig.getMaskAlgorithm())) {
-            return true;
-        }
-        context.disableDefaultConstraintViolation();
-        context.buildConstraintViolationWithTemplate(String.format("references 
unconfigured mask algorithm `%s`", 
columnConfig.getMaskAlgorithm())).addPropertyNode("tables").addConstraintViolation();
-        return false;
-    }
-}
diff --git 
a/features/mask/api/src/test/java/org/apache/shardingsphere/mask/config/MaskRuleConfigurationTest.java
 
b/features/mask/api/src/test/java/org/apache/shardingsphere/mask/config/MaskRuleConfigurationTest.java
index fba5178de20..cf1e999367d 100644
--- 
a/features/mask/api/src/test/java/org/apache/shardingsphere/mask/config/MaskRuleConfigurationTest.java
+++ 
b/features/mask/api/src/test/java/org/apache/shardingsphere/mask/config/MaskRuleConfigurationTest.java
@@ -34,6 +34,7 @@ import java.util.Properties;
 import java.util.stream.Stream;
 
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.is;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -75,6 +76,13 @@ class MaskRuleConfigurationTest {
         assertThrows(InvalidRuleConfigurationException.class, () -> 
RuleConfigurationValidator.validate(ruleConfig));
     }
     
+    @Test
+    void assertUnconfiguredMaskAlgorithmViolation() {
+        MaskRuleConfiguration ruleConfig = createRuleConfiguration(new 
MaskColumnRuleConfiguration("foo_col", "bar_mask"));
+        InvalidRuleConfigurationException actual = 
assertThrows(InvalidRuleConfigurationException.class, () -> 
RuleConfigurationValidator.validate(ruleConfig));
+        assertThat(actual.getMessage(), containsString("Property `tables` 
references unconfigured maskAlgorithms `bar_mask`."));
+    }
+    
     private static Stream<Arguments> invalidRuleConfigurationArguments() {
         AlgorithmConfiguration algorithmConfig = new 
AlgorithmConfiguration("FIXTURE", new Properties());
         return Stream.of(
diff --git 
a/features/readwrite-splitting/api/src/main/java/org/apache/shardingsphere/readwritesplitting/config/ReadwriteSplittingRuleConfiguration.java
 
b/features/readwrite-splitting/api/src/main/java/org/apache/shardingsphere/readwritesplitting/config/ReadwriteSplittingRuleConfiguration.java
index 1e573d01325..1dd16925812 100644
--- 
a/features/readwrite-splitting/api/src/main/java/org/apache/shardingsphere/readwritesplitting/config/ReadwriteSplittingRuleConfiguration.java
+++ 
b/features/readwrite-splitting/api/src/main/java/org/apache/shardingsphere/readwritesplitting/config/ReadwriteSplittingRuleConfiguration.java
@@ -22,10 +22,10 @@ import lombok.RequiredArgsConstructor;
 import 
org.apache.shardingsphere.infra.algorithm.core.config.AlgorithmConfiguration;
 import 
org.apache.shardingsphere.infra.config.rule.function.DistributedRuleConfiguration;
 import 
org.apache.shardingsphere.infra.config.rule.scope.DatabaseRuleConfiguration;
+import 
org.apache.shardingsphere.infra.config.rule.validator.constraint.reference.ConfigurationReferenceExists;
 import 
org.apache.shardingsphere.infra.config.rule.validator.constraint.spi.SPITypeExists;
 import 
org.apache.shardingsphere.infra.config.rule.validator.group.RuleConfigurationTypeValidationGroup;
 import 
org.apache.shardingsphere.readwritesplitting.config.rule.ReadwriteSplittingDataSourceGroupRuleConfiguration;
-import 
org.apache.shardingsphere.readwritesplitting.config.validator.ValidReadwriteSplittingRuleConfiguration;
 
 import javax.validation.Valid;
 import javax.validation.constraints.NotBlank;
@@ -38,7 +38,7 @@ import java.util.Map;
  */
 @RequiredArgsConstructor
 @Getter
-@ValidReadwriteSplittingRuleConfiguration(groups = 
RuleConfigurationTypeValidationGroup.class)
+@ConfigurationReferenceExists(referencePaths = 
"dataSourceGroups.loadBalancerName", pool = "loadBalancers", allowEmpty = true, 
groups = RuleConfigurationTypeValidationGroup.class)
 public final class ReadwriteSplittingRuleConfiguration implements 
DatabaseRuleConfiguration, DistributedRuleConfiguration {
     
     @NotNull
diff --git 
a/features/readwrite-splitting/api/src/main/java/org/apache/shardingsphere/readwritesplitting/config/validator/ReadwriteSplittingRuleConfigurationValidator.java
 
b/features/readwrite-splitting/api/src/main/java/org/apache/shardingsphere/readwritesplitting/config/validator/ReadwriteSplittingRuleConfigurationValidator.java
deleted file mode 100644
index 1c732eb681a..00000000000
--- 
a/features/readwrite-splitting/api/src/main/java/org/apache/shardingsphere/readwritesplitting/config/validator/ReadwriteSplittingRuleConfigurationValidator.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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.apache.shardingsphere.readwritesplitting.config.validator;
-
-import com.google.common.base.Strings;
-import 
org.apache.shardingsphere.readwritesplitting.config.ReadwriteSplittingRuleConfiguration;
-import 
org.apache.shardingsphere.readwritesplitting.config.rule.ReadwriteSplittingDataSourceGroupRuleConfiguration;
-
-import javax.validation.ConstraintValidator;
-import javax.validation.ConstraintValidatorContext;
-
-/**
- * Readwrite-splitting rule configuration validator.
- */
-public final class ReadwriteSplittingRuleConfigurationValidator implements 
ConstraintValidator<ValidReadwriteSplittingRuleConfiguration, 
ReadwriteSplittingRuleConfiguration> {
-    
-    @Override
-    public boolean isValid(final ReadwriteSplittingRuleConfiguration value, 
final ConstraintValidatorContext context) {
-        return value.getDataSourceGroups().stream().allMatch(each -> 
isLoadBalancerReferenceValid(each, value, context));
-    }
-    
-    private boolean isLoadBalancerReferenceValid(final 
ReadwriteSplittingDataSourceGroupRuleConfiguration dataSourceGroupConfig,
-                                                 final 
ReadwriteSplittingRuleConfiguration ruleConfig, final 
ConstraintValidatorContext context) {
-        if (Strings.isNullOrEmpty(dataSourceGroupConfig.getLoadBalancerName()) 
|| 
ruleConfig.getLoadBalancers().containsKey(dataSourceGroupConfig.getLoadBalancerName()))
 {
-            return true;
-        }
-        context.disableDefaultConstraintViolation();
-        context.buildConstraintViolationWithTemplate(String.format("references 
unconfigured load balancer `%s`", dataSourceGroupConfig.getLoadBalancerName()))
-                .addPropertyNode("dataSourceGroups").addConstraintViolation();
-        return false;
-    }
-}
diff --git 
a/features/readwrite-splitting/api/src/main/java/org/apache/shardingsphere/readwritesplitting/config/validator/ValidReadwriteSplittingRuleConfiguration.java
 
b/features/readwrite-splitting/api/src/main/java/org/apache/shardingsphere/readwritesplitting/config/validator/ValidReadwriteSplittingRuleConfiguration.java
deleted file mode 100644
index b4cd57e365e..00000000000
--- 
a/features/readwrite-splitting/api/src/main/java/org/apache/shardingsphere/readwritesplitting/config/validator/ValidReadwriteSplittingRuleConfiguration.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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.apache.shardingsphere.readwritesplitting.config.validator;
-
-import javax.validation.Constraint;
-import javax.validation.Payload;
-import java.lang.annotation.Documented;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Valid readwrite-splitting rule configuration constraint.
- */
-@Target(ElementType.TYPE)
-@Retention(RetentionPolicy.RUNTIME)
-@Constraint(validatedBy = ReadwriteSplittingRuleConfigurationValidator.class)
-@Documented
-public @interface ValidReadwriteSplittingRuleConfiguration {
-    
-    /**
-     * Get message.
-     *
-     * @return message
-     */
-    String message() default "contains invalid references";
-    
-    /**
-     * Get groups.
-     *
-     * @return groups
-     */
-    Class<?>[] groups() default {};
-    
-    /**
-     * Get payload.
-     *
-     * @return payload
-     */
-    Class<? extends Payload>[] payload() default {};
-}
diff --git 
a/features/readwrite-splitting/api/src/test/java/org/apache/shardingsphere/readwritesplitting/config/ReadwriteSplittingRuleConfigurationTest.java
 
b/features/readwrite-splitting/api/src/test/java/org/apache/shardingsphere/readwritesplitting/config/ReadwriteSplittingRuleConfigurationTest.java
index ced5782afa7..74f058f6bbc 100644
--- 
a/features/readwrite-splitting/api/src/test/java/org/apache/shardingsphere/readwritesplitting/config/ReadwriteSplittingRuleConfigurationTest.java
+++ 
b/features/readwrite-splitting/api/src/test/java/org/apache/shardingsphere/readwritesplitting/config/ReadwriteSplittingRuleConfigurationTest.java
@@ -22,6 +22,7 @@ import 
org.apache.shardingsphere.infra.config.rule.validator.RuleConfigurationVa
 import 
org.apache.shardingsphere.infra.exception.kernel.metadata.rule.InvalidRuleConfigurationException;
 import 
org.apache.shardingsphere.readwritesplitting.config.rule.ReadwriteSplittingDataSourceGroupRuleConfiguration;
 import 
org.apache.shardingsphere.readwritesplitting.transaction.TransactionalReadQueryStrategy;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
@@ -31,6 +32,8 @@ import java.util.Collections;
 import java.util.Properties;
 import java.util.stream.Stream;
 
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
@@ -51,6 +54,8 @@ class ReadwriteSplittingRuleConfigurationTest {
                         new ReadwriteSplittingDataSourceGroupRuleConfiguration(
                                 "foo_group", "foo_write", 
Collections.singletonList("foo_read"), null)),
                         Collections.emptyMap())),
+                Arguments.of("Group with empty load balancer name", 
createRuleConfiguration(new ReadwriteSplittingDataSourceGroupRuleConfiguration(
+                        "foo_group", "foo_write", 
Collections.singletonList("foo_read"), ""))),
                 Arguments.of("Complete configuration", new 
ReadwriteSplittingRuleConfiguration(Collections.singleton(dataSourceGroupConfig),
                         Collections.singletonMap("foo_load_balancer", new 
AlgorithmConfiguration("FIXTURE", new Properties())))));
     }
@@ -61,6 +66,14 @@ class ReadwriteSplittingRuleConfigurationTest {
         assertThrows(InvalidRuleConfigurationException.class, () -> 
RuleConfigurationValidator.validate(ruleConfig));
     }
     
+    @Test
+    void assertUnconfiguredLoadBalancerViolation() {
+        ReadwriteSplittingRuleConfiguration ruleConfig = 
createRuleConfiguration(new ReadwriteSplittingDataSourceGroupRuleConfiguration(
+                "foo_group", "foo_write", 
Collections.singletonList("foo_read"), "bar_load_balancer"));
+        InvalidRuleConfigurationException actual = 
assertThrows(InvalidRuleConfigurationException.class, () -> 
RuleConfigurationValidator.validate(ruleConfig));
+        assertThat(actual.getMessage(), containsString("Property 
`dataSourceGroups` references unconfigured loadBalancers 
`bar_load_balancer`."));
+    }
+    
     private static Stream<Arguments> invalidRuleConfigurationArguments() {
         AlgorithmConfiguration loadBalancer = new 
AlgorithmConfiguration("FIXTURE", new Properties());
         return Stream.of(
diff --git 
a/features/mask/api/src/main/java/org/apache/shardingsphere/mask/config/validator/ValidMaskRuleConfiguration.java
 
b/infra/common/src/main/java/org/apache/shardingsphere/infra/config/rule/validator/constraint/reference/ConfigurationReferenceExists.java
similarity index 55%
rename from 
features/mask/api/src/main/java/org/apache/shardingsphere/mask/config/validator/ValidMaskRuleConfiguration.java
rename to 
infra/common/src/main/java/org/apache/shardingsphere/infra/config/rule/validator/constraint/reference/ConfigurationReferenceExists.java
index e180304829a..82cd7bd5ca0 100644
--- 
a/features/mask/api/src/main/java/org/apache/shardingsphere/mask/config/validator/ValidMaskRuleConfiguration.java
+++ 
b/infra/common/src/main/java/org/apache/shardingsphere/infra/config/rule/validator/constraint/reference/ConfigurationReferenceExists.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.shardingsphere.mask.config.validator;
+package 
org.apache.shardingsphere.infra.config.rule.validator.constraint.reference;
 
 import javax.validation.Constraint;
 import javax.validation.Payload;
@@ -26,20 +26,42 @@ import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
 /**
- * Valid mask rule configuration constraint.
+ * Constraint for names that reference configurations in a map on the same 
rule configuration.
+ * Property paths use public getters. Collections are traversed, and optional 
values are unwrapped.
  */
 @Target(ElementType.TYPE)
 @Retention(RetentionPolicy.RUNTIME)
-@Constraint(validatedBy = MaskRuleConfigurationValidator.class)
+@Constraint(validatedBy = ConfigurationReferenceExistsValidator.class)
 @Documented
-public @interface ValidMaskRuleConfiguration {
+public @interface ConfigurationReferenceExists {
     
     /**
-     * Get message.
+     * Get reference property paths from the annotated object.
      *
-     * @return message
+     * @return reference property paths
      */
-    String message() default "contains invalid references";
+    String[] referencePaths();
+    
+    /**
+     * Get the name of the map property containing configured values.
+     *
+     * @return configuration pool property name
+     */
+    String pool();
+    
+    /**
+     * Whether an empty reference name is allowed.
+     *
+     * @return whether an empty reference name is allowed
+     */
+    boolean allowEmpty() default false;
+    
+    /**
+     * Get the violation message. Use {pool} for the configuration pool 
property and {reference} for the missing name.
+     *
+     * @return violation message
+     */
+    String message() default "references unconfigured {pool} `{reference}`";
     
     /**
      * Get groups.
diff --git 
a/infra/common/src/main/java/org/apache/shardingsphere/infra/config/rule/validator/constraint/reference/ConfigurationReferenceExistsValidator.java
 
b/infra/common/src/main/java/org/apache/shardingsphere/infra/config/rule/validator/constraint/reference/ConfigurationReferenceExistsValidator.java
new file mode 100644
index 00000000000..75c464e1567
--- /dev/null
+++ 
b/infra/common/src/main/java/org/apache/shardingsphere/infra/config/rule/validator/constraint/reference/ConfigurationReferenceExistsValidator.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.shardingsphere.infra.config.rule.validator.constraint.reference;
+
+import org.apache.shardingsphere.infra.exception.ShardingSpherePreconditions;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+import javax.validation.ValidationException;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Configuration reference exists validator.
+ */
+public final class ConfigurationReferenceExistsValidator implements 
ConstraintValidator<ConfigurationReferenceExists, Object> {
+    
+    private ReferencePathNode referencePaths;
+    
+    private String poolProperty;
+    
+    private boolean allowEmpty;
+    
+    private String message;
+    
+    @Override
+    public void initialize(final ConfigurationReferenceExists 
constraintAnnotation) {
+        ShardingSpherePreconditions.checkState(0 != 
constraintAnnotation.referencePaths().length, () -> new ValidationException("At 
least one configuration reference property is required."));
+        referencePaths = new ReferencePathNode();
+        for (String each : constraintAnnotation.referencePaths()) {
+            referencePaths.addPath(parseReferencePaths(each));
+        }
+        poolProperty = constraintAnnotation.pool();
+        ShardingSpherePreconditions.checkState(!poolProperty.isEmpty() && 
!poolProperty.contains("."),
+                () -> new ValidationException("Configuration pool must be a 
property on the annotated object."));
+        allowEmpty = constraintAnnotation.allowEmpty();
+        message = constraintAnnotation.message();
+    }
+    
+    private String[] parseReferencePaths(final String referencePath) {
+        String[] result = referencePath.split("\\.", -1);
+        for (String each : result) {
+            ShardingSpherePreconditions.checkNotEmpty(each, () -> new 
ValidationException(String.format("Invalid configuration reference property 
path `%s`.", referencePath)));
+        }
+        return result;
+    }
+    
+    @Override
+    public boolean isValid(final Object value, final 
ConstraintValidatorContext context) {
+        Object pool = getProperty(value, poolProperty);
+        ShardingSpherePreconditions.checkState(null == pool || pool instanceof 
Map, () -> new ValidationException(String.format("Configuration pool property 
`%s` must be a map.", poolProperty)));
+        Map<?, ?> configurations = null == pool ? Collections.emptyMap() : 
(Map<?, ?>) pool;
+        return areReferencesValid(value, referencePaths, configurations, 
context, null, "");
+    }
+    
+    private Object getProperty(final Object value, final String property) {
+        try {
+            Method getter = value.getClass().getMethod("get" + 
Character.toUpperCase(property.charAt(0)) + property.substring(1));
+            return getter.invoke(value);
+        } catch (final ReflectiveOperationException ex) {
+            throw new ValidationException(String.format("Can not read property 
`%s` from `%s`.", property, value.getClass().getName()), ex);
+        }
+    }
+    
+    private boolean areReferencesValid(final Object value, final 
ReferencePathNode node, final Map<?, ?> pool,
+                                       final ConstraintValidatorContext 
context, final String rootProperty, final String path) {
+        if (null == value) {
+            return true;
+        }
+        if (value instanceof Optional) {
+            return areReferencesValid(((Optional<?>) value).orElse(null), 
node, pool, context, rootProperty, path);
+        }
+        if (value instanceof Collection) {
+            return areCollectionReferencesValid((Collection<?>) value, node, 
pool, context, rootProperty, path);
+        }
+        return (!node.pathEnd || isReferenceValid(value, pool, context, 
rootProperty, path)) && areChildReferencesValid(value, node, pool, context, 
rootProperty, path);
+    }
+    
+    private boolean areCollectionReferencesValid(final Collection<?> values, 
final ReferencePathNode node, final Map<?, ?> pool,
+                                                 final 
ConstraintValidatorContext context, final String rootProperty, final String 
path) {
+        return values.stream().allMatch(each -> areReferencesValid(each, node, 
pool, context, rootProperty, path));
+    }
+    
+    private boolean isReferenceValid(final Object value, final Map<?, ?> pool, 
final ConstraintValidatorContext context, final String rootProperty, final 
String path) {
+        ShardingSpherePreconditions.checkState(value instanceof String, () -> 
new ValidationException(String.format("Configuration reference property `%s` 
must be a string.", path)));
+        if (allowEmpty && ((String) value).isEmpty() || 
pool.containsKey(value)) {
+            return true;
+        }
+        context.disableDefaultConstraintViolation();
+        context.buildConstraintViolationWithTemplate(message.replace("{pool}", 
poolProperty).replace("{reference}", (String) 
value)).addPropertyNode(rootProperty).addConstraintViolation();
+        return false;
+    }
+    
+    private boolean areChildReferencesValid(final Object value, final 
ReferencePathNode node, final Map<?, ?> pool,
+                                            final ConstraintValidatorContext 
context, final String rootProperty, final String path) {
+        return node.children.entrySet().stream().allMatch(each -> 
areReferencesValid(
+                getProperty(value, each.getKey()), each.getValue(), pool, 
context, null == rootProperty ? each.getKey() : rootProperty, path.isEmpty() ? 
each.getKey() : path + "." + each.getKey()));
+    }
+    
+    private static final class ReferencePathNode {
+        
+        private final Map<String, ReferencePathNode> children = new 
LinkedHashMap<>();
+        
+        private boolean pathEnd;
+        
+        private void addPath(final String[] properties) {
+            ReferencePathNode current = this;
+            for (String each : properties) {
+                current = current.children.computeIfAbsent(each, ignored -> 
new ReferencePathNode());
+            }
+            current.pathEnd = true;
+        }
+    }
+}
diff --git 
a/infra/common/src/test/java/org/apache/shardingsphere/infra/config/rule/validator/constraint/reference/ConfigurationReferenceExistsValidatorTest.java
 
b/infra/common/src/test/java/org/apache/shardingsphere/infra/config/rule/validator/constraint/reference/ConfigurationReferenceExistsValidatorTest.java
new file mode 100644
index 00000000000..931bf608e94
--- /dev/null
+++ 
b/infra/common/src/test/java/org/apache/shardingsphere/infra/config/rule/validator/constraint/reference/ConfigurationReferenceExistsValidatorTest.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.shardingsphere.infra.config.rule.validator.constraint.reference;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.apache.bval.jsr.ApacheValidationProvider;
+import org.junit.jupiter.api.Test;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.Validation;
+import javax.validation.ValidationException;
+import javax.validation.Validator;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ConfigurationReferenceExistsValidatorTest {
+    
+    private static final Validator VALIDATOR = 
Validation.byProvider(ApacheValidationProvider.class).configure().buildValidatorFactory().getValidator();
+    
+    @Test
+    void assertConfiguredDirectReference() {
+        assertTrue(VALIDATOR.validate(new DirectConfiguration("foo_provider", 
Collections.singletonMap("foo_provider", "configured"))).isEmpty());
+    }
+    
+    @Test
+    void assertUnconfiguredDirectReference() {
+        Set<ConstraintViolation<DirectConfiguration>> actual = 
VALIDATOR.validate(new DirectConfiguration("bar_provider", 
Collections.singletonMap("foo_provider", "configured")));
+        assertThat(actual.size(), is(1));
+        assertThat(actual.iterator().next().getPropertyPath().toString(), 
is("providerName"));
+        assertThat(actual.iterator().next().getMessage(), is("references 
unconfigured providers `bar_provider`"));
+    }
+    
+    @Test
+    void assertUnconfiguredReferenceWithMissingPool() {
+        Set<ConstraintViolation<DirectConfiguration>> actual = 
VALIDATOR.validate(new DirectConfiguration("bar_provider", null));
+        assertThat(actual.size(), is(1));
+        assertThat(actual.iterator().next().getMessage(), is("references 
unconfigured providers `bar_provider`"));
+    }
+    
+    @Test
+    void assertConfiguredNestedReference() {
+        NestedConfiguration config = new 
NestedConfiguration(Collections.singleton(new Group(Collections.singleton(
+                new Item(new Reference("foo_algorithm"), null)))), 
Collections.singletonMap("foo_algorithm", "configured"));
+        assertTrue(VALIDATOR.validate(config).isEmpty());
+    }
+    
+    @Test
+    void assertFirstUnconfiguredNestedReference() {
+        NestedConfiguration config = new 
NestedConfiguration(Collections.singleton(new Group(Collections.singleton(
+                new Item(new Reference("bar_algorithm"), new 
Reference("baz_algorithm"))))), Collections.emptyMap());
+        Set<ConstraintViolation<NestedConfiguration>> actual = 
VALIDATOR.validate(config);
+        assertThat(actual.size(), is(1));
+        assertThat(actual.iterator().next().getPropertyPath().toString(), 
is("groups"));
+        assertThat(actual.iterator().next().getMessage(), is("references 
unconfigured algorithms `bar_algorithm`"));
+    }
+    
+    @Test
+    void assertReferencesValidatedPerSourceObject() {
+        Item first = new Item(new Reference("foo_algorithm"), new 
Reference("first_missing"));
+        Item second = new Item(new Reference("second_missing"), null);
+        NestedConfiguration config = new 
NestedConfiguration(Collections.singleton(new Group(Arrays.asList(first, 
second))),
+                Collections.singletonMap("foo_algorithm", "configured"));
+        Set<ConstraintViolation<NestedConfiguration>> actual = 
VALIDATOR.validate(config);
+        assertThat(actual.size(), is(1));
+        assertThat(actual.iterator().next().getMessage(), is("references 
unconfigured algorithms `first_missing`"));
+    }
+    
+    @Test
+    void assertEmptyReference() {
+        NestedConfiguration config = new 
NestedConfiguration(Collections.singleton(new Group(Collections.singleton(new 
Item(new Reference(""), null)))), Collections.emptyMap());
+        assertTrue(VALIDATOR.validate(config).isEmpty());
+    }
+    
+    @Test
+    void assertEmptyReferenceWithMissingPool() {
+        NestedConfiguration config = new 
NestedConfiguration(Collections.singleton(new Group(Collections.singleton(new 
Item(new Reference(""), null)))), null);
+        assertTrue(VALIDATOR.validate(config).isEmpty());
+    }
+    
+    @Test
+    void assertOptionalReference() {
+        NestedConfiguration config = new 
NestedConfiguration(Collections.singleton(new Group(Collections.singleton(
+                new Item(new Reference("foo_algorithm"), new 
Reference("foo_algorithm"))))), Collections.singletonMap("foo_algorithm", 
"configured"));
+        assertTrue(VALIDATOR.validate(config).isEmpty());
+    }
+    
+    @Test
+    void assertDisallowedEmptyReference() {
+        Set<ConstraintViolation<DirectConfiguration>> actual = 
VALIDATOR.validate(new DirectConfiguration("", Collections.emptyMap()));
+        assertThat(actual.size(), is(1));
+    }
+    
+    @Test
+    void assertInvalidReferenceProperty() {
+        assertThrows(ValidationException.class, () -> VALIDATOR.validate(new 
InvalidReferenceConfiguration(Collections.emptyMap())));
+    }
+    
+    @Test
+    void assertInvalidPoolProperty() {
+        assertThrows(ValidationException.class, () -> VALIDATOR.validate(new 
InvalidPoolConfiguration("not a map")));
+    }
+    
+    @RequiredArgsConstructor
+    @Getter
+    @ConfigurationReferenceExists(referencePaths = "providerName", pool = 
"providers")
+    public static final class DirectConfiguration {
+        
+        private final String providerName;
+        
+        private final Map<String, String> providers;
+    }
+    
+    @RequiredArgsConstructor
+    @Getter
+    @ConfigurationReferenceExists(referencePaths = 
{"groups.items.primary.name", "groups.items.secondary.name"}, pool = 
"algorithms", allowEmpty = true)
+    public static final class NestedConfiguration {
+        
+        private final Collection<Group> groups;
+        
+        private final Map<String, String> algorithms;
+    }
+    
+    @RequiredArgsConstructor
+    @Getter
+    public static final class Group {
+        
+        private final Collection<Item> items;
+    }
+    
+    @RequiredArgsConstructor
+    @Getter
+    public static final class Item {
+        
+        private final Reference primary;
+        
+        private final Reference secondary;
+        
+        public Optional<Reference> getSecondary() {
+            return Optional.ofNullable(secondary);
+        }
+    }
+    
+    @RequiredArgsConstructor
+    @Getter
+    public static final class Reference {
+        
+        private final String name;
+    }
+    
+    @RequiredArgsConstructor
+    @Getter
+    @ConfigurationReferenceExists(referencePaths = "missing", pool = 
"providers")
+    public static final class InvalidReferenceConfiguration {
+        
+        private final Map<String, String> providers;
+    }
+    
+    @RequiredArgsConstructor
+    @Getter
+    @ConfigurationReferenceExists(referencePaths = "pool", pool = "pool")
+    public static final class InvalidPoolConfiguration {
+        
+        private final String pool;
+    }
+}

Reply via email to