Repository: redpen-cc/redpen
Pull request #558 (2016-03-04 08:57:52)

Be able to support all available validators in external editors/wordpress
Introduces new API in ConfigurationBuilder that registers all available validators for a particular language.


Patch:
diff --git a/redpen-core/src/main/java/cc/redpen/config/Configuration.java b/redpen-core/src/main/java/cc/redpen/config/Configuration.java
index ba1ab8ec..e42d21be 100644
--- a/redpen-core/src/main/java/cc/redpen/config/Configuration.java
+++ b/redpen-core/src/main/java/cc/redpen/config/Configuration.java
@@ -21,6 +21,7 @@ import cc.redpen.RedPenException;
 import cc.redpen.tokenizer.JapaneseTokenizer;
 import cc.redpen.tokenizer.RedPenTokenizer;
 import cc.redpen.tokenizer.WhiteSpaceTokenizer;
+import cc.redpen.validator.ValidatorFactory;
 
 import java.io.File;
 import java.io.IOException;
@@ -31,6 +32,7 @@ import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
 
+import static java.util.Arrays.asList;
 import static java.util.stream.Collectors.toList;
 import static org.apache.commons.lang3.StringUtils.isEmpty;
 
@@ -46,6 +48,13 @@ public class Configuration implements Serializable, Cloneable {
     private final File base;
     private final boolean secure;
 
+    /**
+     * @return default supported languages and variants that can be used with {@link #builder(String)}
+     */
+    public static List<String> getDefaultConfigKeys() {
+        return asList("en", "ja", "ja.hankaku", "ja.zenkaku2");
+    }
+
     Configuration(File base, SymbolTable symbolTable, List<ValidatorConfiguration> validatorConfigs, String lang, boolean secure) {
         this.base = base;
         this.symbolTable = symbolTable;
@@ -214,8 +223,11 @@ public class Configuration implements Serializable, Cloneable {
         return new ConfigurationBuilder();
     }
 
-    public static ConfigurationBuilder builder(String lang) {
-        return new ConfigurationBuilder().setLanguage(lang);
+    public static ConfigurationBuilder builder(String key) {
+        int dotPos = key.indexOf('.');
+        ConfigurationBuilder builder = new ConfigurationBuilder().setLanguage(dotPos > 0 ? key.substring(0, dotPos) : key);
+        if (dotPos > 0) builder.setVariant(key.substring(dotPos+1));
+        return builder;
     }
 
     /**
@@ -259,6 +271,12 @@ public class Configuration implements Serializable, Cloneable {
             return this;
         }
 
+        public ConfigurationBuilder addAvailableValidatorConfigs() {
+            checkBuilt();
+            validatorConfigs.addAll(ValidatorFactory.getConfigurations(lang));
+            return this;
+        }
+
         public ConfigurationBuilder setVariant(String variant) {
             checkBuilt();
             this.variant = Optional.of(variant);
diff --git a/redpen-core/src/main/java/cc/redpen/validator/ValidatorFactory.java b/redpen-core/src/main/java/cc/redpen/validator/ValidatorFactory.java
index ea32b03e..55aa9d06 100644
--- a/redpen-core/src/main/java/cc/redpen/validator/ValidatorFactory.java
+++ b/redpen-core/src/main/java/cc/redpen/validator/ValidatorFactory.java
@@ -20,73 +20,112 @@ package cc.redpen.validator;
 import cc.redpen.RedPenException;
 import cc.redpen.config.Configuration;
 import cc.redpen.config.ValidatorConfiguration;
+import cc.redpen.validator.section.*;
+import cc.redpen.validator.sentence.*;
 
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
+
+import static java.util.Arrays.asList;
+import static java.util.stream.Collectors.*;
 
 /**
  * Factory class of validators.
  */
 public class ValidatorFactory {
-    private static final List<String> VALIDATOR_PACKAGES = new ArrayList<>();
+    private static final String validatorPackage = Validator.class.getPackage().getName();
+    private static final List<String> VALIDATOR_PACKAGES = asList(validatorPackage, validatorPackage + ".sentence", validatorPackage + ".section");
+    static final Map<String, Validator> validators = new LinkedHashMap<>();
+
+    public static void registerValidator(Class<? extends Validator> clazz) {
+        validators.put(clazz.getSimpleName().replace("Validator", ""), createValidator(clazz));
+    }
 
     static {
-        addValidatorPackage("cc.redpen.validator");
-        addValidatorPackage("cc.redpen.validator.sentence");
-        addValidatorPackage("cc.redpen.validator.section");
+        // section
+        registerValidator(DuplicatedSectionValidator.class);
+        registerValidator(FrequentSentenceStartValidator.class);
+        registerValidator(ParagraphNumberValidator.class);
+        registerValidator(ParagraphStartWithValidator.class);
+        registerValidator(SectionLengthValidator.class);
+        registerValidator(UnexpandedAcronymValidator.class);
+        registerValidator(WordFrequencyValidator.class);
+
+        // sentence
+        registerValidator(CommaNumberValidator.class);
+        registerValidator(ContractionValidator.class);
+        registerValidator(DoubledJoshiValidator.class);
+        registerValidator(DoubledWordValidator.class);
+        registerValidator(DoubleNegativeValidator.class);
+        registerValidator(EndOfSentenceValidator.class);
+        registerValidator(HankakuKanaValidator.class);
+        registerValidator(HyphenationValidator.class);
+        registerValidator(InvalidExpressionValidator.class);
+        registerValidator(InvalidSymbolValidator.class);
+        registerValidator(InvalidWordValidator.class);
+        registerValidator(JapaneseStyleValidator.class);
+        registerValidator(KatakanaEndHyphenValidator.class);
+        registerValidator(KatakanaSpellCheckValidator.class);
+        registerValidator(NumberFormatValidator.class);
+        registerValidator(OkuriganaValidator.class);
+        registerValidator(ParenthesizedSentenceValidator.class);
+        registerValidator(QuotationValidator.class);
+        registerValidator(SentenceLengthValidator.class);
+        registerValidator(SpaceBeginningOfSentenceValidator.class);
+        registerValidator(SpaceBetweenAlphabeticalWordValidator.class);
+        registerValidator(SpellingValidator.class);
+        registerValidator(StartWithCapitalLetterValidator.class);
+        registerValidator(SuccessiveWordValidator.class);
+        registerValidator(SuggestExpressionValidator.class);
+        registerValidator(SymbolWithSpaceValidator.class);
+        registerValidator(WeakExpressionValidator.class);
+        registerValidator(WordNumberValidator.class);
+
+        // other
+        registerValidator(JavaScriptValidator.class);
     }
 
-    // can be made public if package needs to be added outside RedPen.
-    private static void addValidatorPackage(String packageToAdd) {
-        VALIDATOR_PACKAGES.add(packageToAdd);
+    public static List<ValidatorConfiguration> getConfigurations(String lang) {
+        return validators.entrySet().stream().filter(e -> {
+            List<String> supportedLanguages = e.getValue().getSupportedLanguages();
+            return supportedLanguages.isEmpty() || supportedLanguages.contains(lang);
+        }).map(e -> new ValidatorConfiguration(e.getKey())).collect(toList());
     }
 
     public static Validator getInstance(String validatorName) throws RedPenException {
-        Configuration conf = Configuration.builder()
-                .addValidatorConfig(new ValidatorConfiguration(validatorName))
-                .build();
+        Configuration conf = Configuration.builder().addValidatorConfig(new ValidatorConfiguration(validatorName)).build();
         return getInstance(conf.getValidatorConfigs().get(0), conf);
     }
 
-    // store validator constructors to save reflection API call costs
-    private static final Map<String, Constructor> validatorConstructorMap = new ConcurrentHashMap<>();
-
     public static Validator getInstance(ValidatorConfiguration config, Configuration globalConfig) throws RedPenException {
-        Constructor<?> constructor = validatorConstructorMap.computeIfAbsent(config.getValidatorClassName(), validatorClassName -> {
+        Validator prototype = validators.get(config.getConfigurationName());
+        Class<? extends Validator> validatorClass = prototype != null ? prototype.getClass() : loadPlugin(config.getConfigurationName());
+        Validator validator = createValidator(validatorClass);
+        validator.preInit(config, globalConfig);
+        return validator;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Class<? extends Validator> loadPlugin(String name) throws RedPenException {
+        for (String p : VALIDATOR_PACKAGES) {
             try {
-                for (String validatorPackage : VALIDATOR_PACKAGES) {
-                    String fqValidatorClassName = validatorPackage + "." + validatorClassName;
-                    try {
-                        Class<?> clazz = Class.forName(fqValidatorClassName);
-                        // ensure the class extends Validator
-                        Class<?> superclass = clazz.getSuperclass();
-                        if (!superclass.equals(cc.redpen.validator.Validator.class)) {
-                            throw new RuntimeException(fqValidatorClassName + " doesn't extend cc.redpen.validator.Validator");
-                        }
-                        return clazz.getConstructor();
-                    } catch (ClassNotFoundException ignore) {
-                    }
-                }
-            } catch (NoSuchMethodException e) {
-                throw new RuntimeException(e);
+                Class<? extends Validator> validatorClass = (Class)Class.forName(p + "." + name + "Validator");
+                registerValidator(validatorClass);
+                return validatorClass;
+            }
+            catch (ClassNotFoundException ignore) {
             }
-            // unable to find Validator
-            return null;
-        });
-
-        if (constructor == null) {
-            throw new RedPenException("There is no such Validator: " + config.getConfigurationName());
         }
+        throw new RedPenException("There is no such validator: " + name);
+    }
+
+    private static Validator createValidator(Class<? extends Validator> clazz) {
         try {
-            Validator validator = (Validator) constructor.newInstance();
-            validator.preInit(config, globalConfig);
-            return validator;
-        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
-            throw new RuntimeException(e);
+            return clazz.newInstance();
+        }
+        catch (InstantiationException | IllegalAccessException e) {
+            throw new RuntimeException("Cannot create instance of " + clazz + " using default constructor");
         }
     }
 }


Test patch:
diff --git a/redpen-core/src/test/java/cc/redpen/config/ConfigurationTest.java b/redpen-core/src/test/java/cc/redpen/config/ConfigurationTest.java
index 669780fd..bcff86a0 100644
--- a/redpen-core/src/test/java/cc/redpen/config/ConfigurationTest.java
+++ b/redpen-core/src/test/java/cc/redpen/config/ConfigurationTest.java
@@ -174,8 +174,7 @@ public class ConfigurationTest {
 
     @Test
     public void canBeCloned() throws Exception {
-        Configuration conf = Configuration.builder("ja")
-          .setVariant("hankaku")
+        Configuration conf = Configuration.builder("ja.hankaku")
           .addValidatorConfig(new ValidatorConfiguration("SentenceLength")).build();
 
         Configuration clone = conf.clone();
@@ -193,8 +192,7 @@ public class ConfigurationTest {
 
     @Test
     public void equals() throws Exception {
-        Configuration conf = Configuration.builder("ja")
-          .setVariant("hankaku")
+        Configuration conf = Configuration.builder("ja.hankaku")
           .addValidatorConfig(new ValidatorConfiguration("SentenceLength")).build();
 
         Configuration clone = conf.clone();
@@ -211,8 +209,7 @@ public class ConfigurationTest {
 
     @Test
     public void serializable() throws Exception {
-        Configuration conf = Configuration.builder("ja")
-          .setVariant("hankaku")
+        Configuration conf = Configuration.builder("ja.hankaku")
           .addValidatorConfig(new ValidatorConfiguration("SentenceLength")).build();
 
         ByteArrayOutputStream bytes = new ByteArrayOutputStream();
@@ -225,4 +222,15 @@ public class ConfigurationTest {
         assertEquals(conf, conf2);
         assertEquals(conf.getTokenizer().getClass(), conf2.getTokenizer().getClass());
     }
+
+    @Test
+    public void addAvailableValidatorsForLanguage() throws Exception {
+        Configuration ja = Configuration.builder("ja").addAvailableValidatorConfigs().build();
+        assertTrue(ja.getValidatorConfigs().contains(new ValidatorConfiguration("SentenceLength")));
+        assertTrue(ja.getValidatorConfigs().contains(new ValidatorConfiguration("HankakuKana")));
+
+        Configuration en = Configuration.builder("en").addAvailableValidatorConfigs().build();
+        assertTrue(en.getValidatorConfigs().contains(new ValidatorConfiguration("SentenceLength")));
+        assertFalse(en.getValidatorConfigs().contains(new ValidatorConfiguration("HankakuKana")));
+    }
 }
diff --git a/redpen-core/src/test/java/cc/redpen/validator/ValidatorFactoryTest.java b/redpen-core/src/test/java/cc/redpen/validator/ValidatorFactoryTest.java
index b95aecb2..7d36d8d5 100644
--- a/redpen-core/src/test/java/cc/redpen/validator/ValidatorFactoryTest.java
+++ b/redpen-core/src/test/java/cc/redpen/validator/ValidatorFactoryTest.java
@@ -20,58 +20,72 @@ package cc.redpen.validator;
 import cc.redpen.RedPenException;
 import cc.redpen.config.Configuration;
 import cc.redpen.config.ValidatorConfiguration;
-import cc.redpen.model.Sentence;
+import cc.redpen.validator.sentence.SentenceLengthValidator;
 import org.junit.Test;
 
-import static junit.framework.Assert.fail;
+import java.io.File;
+import java.lang.reflect.Modifier;
 
-class NotImplementInterfaceValidator {}
+import static org.junit.Assert.*;
 
-class NoConstructorWithConfigsValidator extends Validator {
-    @Override
-    public void validate(Sentence sentence) {
+class CustomValidator extends Validator {
+}
+
+class NoDefaultConstructorValidator extends Validator {
+    @SuppressWarnings("UnusedParameters")
+    public NoDefaultConstructorValidator(String blah) {
     }
 }
 
 public class ValidatorFactoryTest {
     @Test
-    public void testCreateValidator() {
-        Configuration conf = Configuration.builder()
-                .addValidatorConfig(new ValidatorConfiguration("SentenceLength"))
-                .build();
-        try {
-            ValidatorFactory.getInstance(
-                    conf.getValidatorConfigs().get(0), conf);
-        } catch (RedPenException e) {
-            fail();
-        }
+    public void createValidator() throws RedPenException {
+        Configuration conf = Configuration.builder().addValidatorConfig(new ValidatorConfiguration("SentenceLength")).build();
+        assertEquals(SentenceLengthValidator.class, ValidatorFactory.getInstance(conf.getValidatorConfigs().get(0), conf).getClass());
+    }
+
+    @Test
+    public void validatorPluginsAreCreatedAndRegistered() throws RedPenException {
+        Configuration conf = Configuration.builder().addValidatorConfig(new ValidatorConfiguration("Custom")).build();
+        assertEquals(CustomValidator.class, ValidatorFactory.getInstance(conf.getValidatorConfigs().get(0), conf).getClass());
+        assertTrue(ValidatorFactory.getConfigurations("en").contains(new ValidatorConfiguration("Custom")));
     }
 
     @Test(expected = RedPenException.class)
-    public void testThrowExceptionWhenCreateNonExistValidator() throws RedPenException {
-        Configuration conf = Configuration.builder()
-                .addValidatorConfig(new ValidatorConfiguration("Foobar"))
-                .build();
-        ValidatorFactory.getInstance(
-                conf.getValidatorConfigs().get(0), conf);
+    public void validatorDoesNotExist() throws RedPenException {
+        Configuration conf = Configuration.builder().addValidatorConfig(new ValidatorConfiguration("Foobar")).build();
+        ValidatorFactory.getInstance(conf.getValidatorConfigs().get(0), conf);
     }
 
     @Test(expected = RuntimeException.class)
-    public void testThrowExceptionWhenCreateValidatorNotImplementsInterface() throws RedPenException {
-        Configuration conf = Configuration.builder()
-                .addValidatorConfig(new ValidatorConfiguration("NotImplementInterface"))
-                .build();
-        ValidatorFactory.getInstance(
-                conf.getValidatorConfigs().get(0), conf);
+    public void noDefaultConstructor() {
+        ValidatorFactory.registerValidator(NoDefaultConstructorValidator.class);
     }
 
-    @Test(expected = RuntimeException.class)
-    public void testThrowExceptionWhenCreateValidatorWithoutConstructorWithConfigs() throws RedPenException {
-        Configuration conf = Configuration.builder()
-                .addValidatorConfig(new ValidatorConfiguration("NoConstructorWithConfigs"))
-                .build();
-        ValidatorFactory.getInstance(
-                conf.getValidatorConfigs().get(0), conf);
+    @Test
+    public void allDefaultValidatorsAreRegistered() throws Exception {
+        String validatorsPackage = Validator.class.getPackage().getName();
+        File classes = new File(Validator.class.getProtectionDomain().getCodeSource().getLocation().getPath());
+        File validators = new File(classes, validatorsPackage.replace(".", "/"));
+        checkValidators(validatorsPackage, validators);
     }
 
+    @SuppressWarnings("ConstantConditions")
+    private void checkValidators(String validatorsPackage, File validators) throws ClassNotFoundException {
+        for (File file : validators.listFiles()) {
+            String name = file.getName();
+            if (file.isDirectory()) {
+                checkValidators(validatorsPackage + '.' + name, file);
+            }
+            else if (name.endsWith("Validator.class")) {
+                Class<?> validatorClass = Class.forName(validatorsPackage + "." + name.substring(0, name.length() - 6));
+                if (Modifier.isAbstract(validatorClass.getModifiers())) continue;
+                String validatorName = name.substring(0, name.length() - "Validator.class".length());
+                Validator validator = ValidatorFactory.validators.get(validatorName);
+                assertNotNull(validatorName + " must be registered in " + ValidatorFactory.class, validator);
+                assertTrue(validatorClass + " must extend " + Validator.class, validator instanceof Validator);
+                assertTrue("Registered validator " + name + " must be of " + validatorClass, validatorClass.isAssignableFrom(validator.getClass()));
+            }
+        }
+    }
 }