r2032 - in trunk/nuiton-validator/src: main/java/org/nuiton/validator main/java/org/nuiton/validator/xwork2 test/java/org/nuiton/validator test/java/org/nuiton/validator/xwork2 test/resources/org/nuiton/validator/model
Author: tchemit Date: 2011-01-22 14:32:05 +0100 (Sat, 22 Jan 2011) New Revision: 2032 Url: http://nuiton.org/repositories/revision/nuiton-utils/2032 Log: improve validator (let see the model and effectives scopes) + add in provider a detector method + add the test for xwork2 Added: trunk/nuiton-validator/src/test/resources/org/nuiton/validator/model/Pet-context-info-validation.xml trunk/nuiton-validator/src/test/resources/org/nuiton/validator/model/Pet-error-validation.xml Modified: trunk/nuiton-validator/src/main/java/org/nuiton/validator/NuitonValidator.java trunk/nuiton-validator/src/main/java/org/nuiton/validator/NuitonValidatorProvider.java trunk/nuiton-validator/src/main/java/org/nuiton/validator/xwork2/XWork2NuitonValidator.java trunk/nuiton-validator/src/main/java/org/nuiton/validator/xwork2/XWork2NuitonValidatorProvider.java trunk/nuiton-validator/src/test/java/org/nuiton/validator/ValidatorTestHelper.java trunk/nuiton-validator/src/test/java/org/nuiton/validator/xwork2/XWork2NuitonValidatorProviderTest.java Modified: trunk/nuiton-validator/src/main/java/org/nuiton/validator/NuitonValidator.java =================================================================== --- trunk/nuiton-validator/src/main/java/org/nuiton/validator/NuitonValidator.java 2011-01-22 13:30:57 UTC (rev 2031) +++ trunk/nuiton-validator/src/main/java/org/nuiton/validator/NuitonValidator.java 2011-01-22 13:32:05 UTC (rev 2032) @@ -24,10 +24,15 @@ */ package org.nuiton.validator; +import java.util.Set; + /** * Contract of a validator. + * <p/> + * To obtain validator, see the {@link NuitonValidatorFactory} api. * * @author tchemit <chemit@codelutin.com> + * @see NuitonValidatorFactory * @since 2.0 */ public interface NuitonValidator<O> { @@ -41,4 +46,20 @@ */ NuitonValidatorResult validate(O object) throws NullPointerException; + /** + * Obtains the model of the validator. + * + * @return the model of the validator + */ + NuitonValidatorModel<O> getModel(); + + /** + * Obtains the set of effective scopes for the validator : means the very + * scopes that the validator is dealing with. + * <p/> + * This is a subset of the model authorized scopes. + * + * @return the set of effective scopes of the validator + */ + Set<NuitonValidatorScope> getEffectiveScopes(); } Modified: trunk/nuiton-validator/src/main/java/org/nuiton/validator/NuitonValidatorProvider.java =================================================================== --- trunk/nuiton-validator/src/main/java/org/nuiton/validator/NuitonValidatorProvider.java 2011-01-22 13:30:57 UTC (rev 2031) +++ trunk/nuiton-validator/src/main/java/org/nuiton/validator/NuitonValidatorProvider.java 2011-01-22 13:32:05 UTC (rev 2032) @@ -24,7 +24,10 @@ */ package org.nuiton.validator; +import java.io.File; import java.util.ServiceLoader; +import java.util.SortedSet; +import java.util.regex.Pattern; /** * Provider of {@link NuitonValidator}. @@ -43,15 +46,61 @@ */ public interface NuitonValidatorProvider { + /** + * Obtains the name of the provider. + * + * @return the name of the provider. + */ String getName(); + /** + * Obtain a validator model, the model should be cached and not be + * reinstanciated at each time a validator model is asked. + * + * @param type type of the class to validate + * @param context context of validation ({@code null} if no context) + * @param scopes filtered scope (if nothing given, then use all scopes) + * @param <O> type of the class to validate + * @return the cached model of validation + */ <O> NuitonValidatorModel<O> getModel(Class<O> type, String context, NuitonValidatorScope... scopes); + /** + * Instanciate a new validator model for the given parameters. + * + * @param type type of the class to validate + * @param context context of validation ({@code null} if no context) + * @param scopes filtered scope (if nothing given, then use all scopes) + * @param <O> type of the class to validate + * @return the new instanciated model of validation + */ <O> NuitonValidatorModel<O> newModel(Class<O> type, String context, NuitonValidatorScope... scopes); + /** + * Obtains a new validator for the given {@code model}. + * + * @param model the model of validator to use + * @param <O> type of class to validate + * @return the new validator + */ <O> NuitonValidator<O> newValidator(NuitonValidatorModel<O> model); + + + /** + * Detects in the given directory validators. + * + * @param sourceRoot rott directory where to seek for validators + * @param contextFilter the pattern of context to seek + * @param scopes scopes to seek (if none given, will seek for all scopes) + * @param types types of class to seek + * @return the set of validators found + */ + SortedSet<NuitonValidator<?>> detectValidators(File sourceRoot, + Pattern contextFilter, + NuitonValidatorScope[] scopes, + Class<?>... types); } Modified: trunk/nuiton-validator/src/main/java/org/nuiton/validator/xwork2/XWork2NuitonValidator.java =================================================================== --- trunk/nuiton-validator/src/main/java/org/nuiton/validator/xwork2/XWork2NuitonValidator.java 2011-01-22 13:30:57 UTC (rev 2031) +++ trunk/nuiton-validator/src/main/java/org/nuiton/validator/xwork2/XWork2NuitonValidator.java 2011-01-22 13:32:05 UTC (rev 2032) @@ -105,7 +105,13 @@ return result; } - protected NuitonValidatorModel<O> getModel() { + @Override + public Set<NuitonValidatorScope> getEffectiveScopes() { + return validators.keySet(); + } + + @Override + public NuitonValidatorModel<O> getModel() { return model; } } Modified: trunk/nuiton-validator/src/main/java/org/nuiton/validator/xwork2/XWork2NuitonValidatorProvider.java =================================================================== --- trunk/nuiton-validator/src/main/java/org/nuiton/validator/xwork2/XWork2NuitonValidatorProvider.java 2011-01-22 13:30:57 UTC (rev 2031) +++ trunk/nuiton-validator/src/main/java/org/nuiton/validator/xwork2/XWork2NuitonValidatorProvider.java 2011-01-22 13:32:05 UTC (rev 2032) @@ -24,15 +24,26 @@ */ package org.nuiton.validator.xwork2; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.nuiton.validator.AbstractNuitonValidatorProvider; import org.nuiton.validator.NuitonValidator; import org.nuiton.validator.NuitonValidatorModel; import org.nuiton.validator.NuitonValidatorScope; +import java.io.File; +import java.io.FilenameFilter; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.EnumSet; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Provider of validator for the xworks nuiton validator. @@ -44,6 +55,11 @@ public static final String PROVIDER_NAME = "xwork2"; + + /** Logger. */ + static private final Log log = + LogFactory.getLog(XWork2NuitonValidatorProvider.class); + public XWork2NuitonValidatorProvider() { super(PROVIDER_NAME); } @@ -69,8 +85,224 @@ } @Override - public <O> NuitonValidator<O> newValidator(NuitonValidatorModel<O> model) { + public <O> XWork2NuitonValidator<O> newValidator(NuitonValidatorModel<O> model) { return new XWork2NuitonValidator<O>(model); } + @Override + public SortedSet<NuitonValidator<?>> detectValidators(File sourceRoot, + Pattern contextFilter, + NuitonValidatorScope[] scopes, + Class<?>... types) { + + if (scopes == null) { + scopes = NuitonValidatorScope.values(); + } + + SortedSet<NuitonValidator<?>> result = + new TreeSet<NuitonValidator<?>>(new ValidatorComparator()); + + for (Class<?> c : types) { + File dir = getClassDir(sourceRoot, c); + if (!dir.exists()) { + + // pas de repertoire adequate + if (log.isDebugEnabled()) { + log.debug("skip non existing directory " + dir); + } + continue; + } + String[] contexts = getContexts(c, dir); + if (log.isDebugEnabled()) { + log.debug("contexts : " + Arrays.toString(contexts)); + } + + if (contexts.length > 0) { + String[] realContexts = getContextsWithoutScopes(contexts); + + if (log.isDebugEnabled()) { + log.debug("realContexts : " + + Arrays.toString(realContexts)); + } + + if (contextFilter != null) { + + // filter contexts + realContexts = getFilterContexts(contextFilter, + realContexts + ); + if (log.isDebugEnabled()) { + log.debug("filterContexts : " + + Arrays.toString(realContexts)); + } + } + + for (String context : realContexts) { + + // on cherche le validateur + NuitonValidator<?> validator = getValidator( + c, + context.isEmpty() ? null : context, + scopes + ); + if (validator != null) { + // on enregistre le validateur + result.add(validator); + } + } + } + } + return result; + } + + /** + * Pour un context et un type d'entité donné, instancie un validateur et + * test si ce validateur est utilisable (i.e qu'il admet des champs à + * valider). + * <p/> + * Si aucun champ n'est trouvé dans le validateur, alors on retourne null. + * + * @param <O> le type du bean + * @param klass le type du bean + * @param context le context du validateur + * @param scopes les scopes a utiliser (si {@code null} alors pas de + * filtre sur les scopes) + * @return le validateur initialisé, ou <code>null</code> si aucun scope + * détecté dans le validateur. + */ + protected <O> NuitonValidator<O> getValidator(Class<O> klass, + String context, + NuitonValidatorScope... scopes) { + + NuitonValidatorModel<O> model = newModel(klass, context, scopes); + + XWork2NuitonValidator<O> valitator = newValidator(model); + + Set<NuitonValidatorScope> realScopes = valitator.getEffectiveScopes(); + if (realScopes.isEmpty()) { + valitator = null; + if (log.isDebugEnabled()) { + log.debug(klass + " : validator skip (no scopes detected)"); + } + } else { + if (log.isDebugEnabled()) { + log.debug(klass + " : keep validator " + valitator); + } + } + return valitator; + } + + protected File getClassDir(File sourceRoot, Class<?> clazz) { + String path = clazz.getPackage().getName(); + path = path.replaceAll("\\.", File.separator); + File dir = new File(sourceRoot, path); + return dir; + } + + protected String[] getContexts(Class<?> clazz, File dir) { + Set<String> result = new TreeSet<String>(); + ValidatorFilenameFilter filter = new ValidatorFilenameFilter(clazz); + if (log.isDebugEnabled()) { + log.debug("dir : " + dir); + } + String[] files = dir.list(filter); + for (String file : files) { + if (log.isDebugEnabled()) { + log.debug("file " + file); + } + String context = file.substring( + filter.prefix.length(), + file.length() - ValidatorFilenameFilter.SUFFIX.length() + ); + if (log.isDebugEnabled()) { + log.debug("detect " + clazz.getSimpleName() + + " context [" + context + "]"); + } + result.add(context); + } + return result.toArray(new String[result.size()]); + } + + protected String[] getContextsWithoutScopes(String[] contexts) { + Set<String> result = new TreeSet<String>(); + NuitonValidatorScope[] scopes = NuitonValidatorScope.values(); + for (String context : contexts) { + for (NuitonValidatorScope scope : scopes) { + String scopeName = scope.name().toLowerCase(); + if (!context.endsWith(scopeName)) { + // pas concerne par ce scope + continue; + } + if (log.isDebugEnabled()) { + log.debug("detect context : " + context); + } + String realContext = context.substring( + 0, + context.length() - scopeName.length() + ); + if (realContext.endsWith("-")) { + realContext = realContext.substring( + 0, + realContext.length() - 1 + ); + } + result.add(realContext); + } + } + return result.toArray(new String[result.size()]); + } + + protected String[] getFilterContexts(Pattern contextFilter, + String[] realContexts) { + List<String> result = new ArrayList<String>(); + for (String c : realContexts) { + Matcher m = contextFilter.matcher(c); + if (m.matches()) { + result.add(c); + } + } + return result.toArray(new String[result.size()]); + } + + protected static class ValidatorFilenameFilter implements FilenameFilter { + + protected static final String SUFFIX = "-validation.xml"; + + protected Class<?> clazz; + + protected String prefix; + + public ValidatorFilenameFilter(Class<?> clazz) { + this.clazz = clazz; + prefix = clazz.getSimpleName() + "-"; + } + + @Override + public boolean accept(File dir, String name) { + boolean result = name.endsWith(SUFFIX); + if (result) { + result = name.startsWith(prefix); + } + return result; + } + } + + protected static class ValidatorComparator implements Comparator<NuitonValidator<?>> { + + @Override + public int compare(NuitonValidator<?> o1, NuitonValidator<?> o2) { + + NuitonValidatorModel<?> model1 = ((XWork2NuitonValidator<?>) o1).getModel(); + NuitonValidatorModel<?> model2 = ((XWork2NuitonValidator<?>) o2).getModel(); + + String contextName1 = + model1.getType().getSimpleName() + "-" + + (model1.getContext() == null ? "" : model1.getContext()); + String contextName2 = + model2.getType().getSimpleName() + "-" + + (model2.getContext() == null ? "" : model2.getContext()); + return contextName1.compareTo(contextName2); + } + } + } Modified: trunk/nuiton-validator/src/test/java/org/nuiton/validator/ValidatorTestHelper.java =================================================================== --- trunk/nuiton-validator/src/test/java/org/nuiton/validator/ValidatorTestHelper.java 2011-01-22 13:30:57 UTC (rev 2031) +++ trunk/nuiton-validator/src/test/java/org/nuiton/validator/ValidatorTestHelper.java 2011-01-22 13:32:05 UTC (rev 2032) @@ -28,6 +28,7 @@ import org.nuiton.validator.model.Person; import org.nuiton.validator.model.Pet; +import java.io.File; import java.util.List; /** @@ -37,6 +38,19 @@ * @since 2.0 */ public class ValidatorTestHelper { + + public static File getBasedir() { + + // Search basedir from maven environment + String basedirPath = System.getenv("basedir"); + if (basedirPath == null) { + + // hope the tests are running from the root of the module :) + basedirPath = new File("").getAbsolutePath(); + } + return new File(basedirPath); + } + public static void testPerson(NuitonValidator<Person> validator) { Assert.assertNotNull(validator); @@ -46,7 +60,6 @@ result = validator.validate(person); - // two errors : no name, no firstname // one warning : no pet Assert.assertFalse(result.isValid()); Modified: trunk/nuiton-validator/src/test/java/org/nuiton/validator/xwork2/XWork2NuitonValidatorProviderTest.java =================================================================== --- trunk/nuiton-validator/src/test/java/org/nuiton/validator/xwork2/XWork2NuitonValidatorProviderTest.java 2011-01-22 13:30:57 UTC (rev 2031) +++ trunk/nuiton-validator/src/test/java/org/nuiton/validator/xwork2/XWork2NuitonValidatorProviderTest.java 2011-01-22 13:32:05 UTC (rev 2032) @@ -24,17 +24,25 @@ */ package org.nuiton.validator.xwork2; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.nuiton.validator.NuitonValidator; import org.nuiton.validator.NuitonValidatorModel; import org.nuiton.validator.NuitonValidatorScope; +import org.nuiton.validator.ValidatorTestHelper; import org.nuiton.validator.model.Person; +import org.nuiton.validator.model.Pet; +import java.io.File; import java.util.Arrays; import java.util.HashSet; +import java.util.Iterator; import java.util.Set; +import java.util.SortedSet; +import java.util.regex.Pattern; /** * To test {@link XWork2NuitonValidatorProvider}. @@ -44,6 +52,11 @@ */ public class XWork2NuitonValidatorProviderTest { + + /** Logger. */ + static private final Log log = + LogFactory.getLog(XWork2NuitonValidatorProviderTest.class); + protected XWork2NuitonValidatorProvider provider; @Before @@ -77,4 +90,138 @@ Assert.assertNotNull(validator); } + @Test + public void testDetectValidators() { + + String context = "context"; + + File basedir = ValidatorTestHelper.getBasedir(); + File testResourcesDir = new File(basedir, "src" + File.separator + "test" + File.separator + "resources"); + + SortedSet<NuitonValidator<?>> result; + NuitonValidator<?> validator; + NuitonValidatorModel<?> model; + Set<NuitonValidatorScope> effectiveScopes; + Iterator<NuitonValidator<?>> iterator; + + // test with all context and all scopes : two validators (Person + Pet + Pet (context)) + + result = provider.detectValidators(testResourcesDir, null, null, Person.class, Pet.class); + + Assert.assertNotNull(result); + Assert.assertEquals(3, result.size()); + + + iterator = result.iterator(); + validator = iterator.next(); + Assert.assertNotNull(validator); + Assert.assertTrue(validator instanceof XWork2NuitonValidator<?>); + model = validator.getModel(); + Assert.assertEquals(Person.class, model.getType()); + Assert.assertNull(model.getContext()); + + + effectiveScopes = validator.getEffectiveScopes(); + log.info("effective scopes : " + effectiveScopes); + + Assert.assertNotNull(effectiveScopes); + Assert.assertEquals(2, effectiveScopes.size()); + Assert.assertTrue(effectiveScopes.contains(NuitonValidatorScope.ERROR)); + Assert.assertTrue(effectiveScopes.contains(NuitonValidatorScope.WARNING)); + + validator = iterator.next(); + Assert.assertNotNull(validator); + Assert.assertTrue(validator instanceof XWork2NuitonValidator<?>); + model = validator.getModel(); + Assert.assertEquals(Pet.class, model.getType()); + Assert.assertNull(model.getContext()); + + effectiveScopes = validator.getEffectiveScopes(); + log.info("effective scopes : " + effectiveScopes); + + Assert.assertNotNull(effectiveScopes); + Assert.assertEquals(1, effectiveScopes.size()); + Assert.assertTrue(effectiveScopes.contains(NuitonValidatorScope.ERROR)); + + validator = iterator.next(); + Assert.assertNotNull(validator); + Assert.assertTrue(validator instanceof XWork2NuitonValidator<?>); + model = validator.getModel(); + Assert.assertEquals(Pet.class, model.getType()); + + + Assert.assertEquals(context, model.getContext()); + + + effectiveScopes = validator.getEffectiveScopes(); + log.info("effective scopes : " + effectiveScopes); + + Assert.assertNotNull(effectiveScopes); + Assert.assertEquals(1, effectiveScopes.size()); + Assert.assertTrue(effectiveScopes.contains(NuitonValidatorScope.INFO)); + + // test with no context and only scope warning : one validator (Person) + + result = provider.detectValidators(testResourcesDir, null, new NuitonValidatorScope[]{NuitonValidatorScope.WARNING}, Person.class, Pet.class); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.size()); + + iterator = result.iterator(); + + validator = iterator.next(); + Assert.assertNotNull(validator); + Assert.assertTrue(validator instanceof XWork2NuitonValidator<?>); + + model = validator.getModel(); + Assert.assertEquals(Person.class, model.getType()); + Assert.assertNull(model.getContext()); + + effectiveScopes = validator.getEffectiveScopes(); + log.info("effective scopes : " + effectiveScopes); + + Assert.assertNotNull(effectiveScopes); + Assert.assertEquals(1, effectiveScopes.size()); + Assert.assertTrue(effectiveScopes.contains(NuitonValidatorScope.WARNING)); + + // test with context 'context' and all scopes : one validator (Pet) + + result = provider.detectValidators(testResourcesDir, Pattern.compile(context), null, Person.class, Pet.class); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.size()); + + iterator = result.iterator(); + + validator = iterator.next(); + Assert.assertNotNull(validator); + Assert.assertTrue(validator instanceof XWork2NuitonValidator<?>); + + model = validator.getModel(); + Assert.assertEquals(Pet.class, model.getType()); + Assert.assertEquals(context, model.getContext()); + + effectiveScopes = validator.getEffectiveScopes(); + log.info("effective scopes : " + effectiveScopes); + + Assert.assertNotNull(effectiveScopes); + Assert.assertEquals(1, effectiveScopes.size()); + Assert.assertTrue(effectiveScopes.contains(NuitonValidatorScope.INFO)); + + // test with no context and only scope fatal : no validator + result = provider.detectValidators(testResourcesDir, null, + new NuitonValidatorScope[]{NuitonValidatorScope.FATAL}, Person.class, Pet.class); + + Assert.assertNotNull(result); + Assert.assertTrue(result.isEmpty()); + + // test with specific context fake and all scopes : no validator + + result = provider.detectValidators(testResourcesDir, Pattern.compile(".*-fake"), + null, Person.class, Pet.class); + + Assert.assertNotNull(result); + Assert.assertTrue(result.isEmpty()); + } + } Added: trunk/nuiton-validator/src/test/resources/org/nuiton/validator/model/Pet-context-info-validation.xml =================================================================== --- trunk/nuiton-validator/src/test/resources/org/nuiton/validator/model/Pet-context-info-validation.xml (rev 0) +++ trunk/nuiton-validator/src/test/resources/org/nuiton/validator/model/Pet-context-info-validation.xml 2011-01-22 13:32:05 UTC (rev 2032) @@ -0,0 +1,36 @@ +<!-- + #%L + Nuiton Utils :: Nuiton Validator + + $Id$ + $HeadURL: http://svn.nuiton.org/svn/nuiton-utils/trunk/nuiton-validator/src/test/resou... $ + %% + Copyright (C) 2011 CodeLutin + %% + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Lesser Public License for more details. + + You should have received a copy of the GNU General Lesser Public + License along with this program. If not, see + <http://www.gnu.org/licenses/lgpl-3.0.html>. + #L% + --> +<!DOCTYPE validators PUBLIC + "-//OpenSymphony Group//XWork Validator 1.0.2//EN" + "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd"> +<validators> + + <field name="name"> + <field-validator type="requiredstring"> + <message>pet.name.required</message> + </field-validator> + </field> + +</validators> \ No newline at end of file Property changes on: trunk/nuiton-validator/src/test/resources/org/nuiton/validator/model/Pet-context-info-validation.xml ___________________________________________________________________ Added: svn:keywords + Author Date Id Revision Added: svn:eol-style + native Added: trunk/nuiton-validator/src/test/resources/org/nuiton/validator/model/Pet-error-validation.xml =================================================================== --- trunk/nuiton-validator/src/test/resources/org/nuiton/validator/model/Pet-error-validation.xml (rev 0) +++ trunk/nuiton-validator/src/test/resources/org/nuiton/validator/model/Pet-error-validation.xml 2011-01-22 13:32:05 UTC (rev 2032) @@ -0,0 +1,36 @@ +<!-- + #%L + Nuiton Utils :: Nuiton Validator + + $Id$ + $HeadURL: http://svn.nuiton.org/svn/nuiton-utils/trunk/nuiton-validator/src/test/resou... $ + %% + Copyright (C) 2011 CodeLutin + %% + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Lesser Public License for more details. + + You should have received a copy of the GNU General Lesser Public + License along with this program. If not, see + <http://www.gnu.org/licenses/lgpl-3.0.html>. + #L% + --> +<!DOCTYPE validators PUBLIC + "-//OpenSymphony Group//XWork Validator 1.0.2//EN" + "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd"> +<validators> + + <field name="name"> + <field-validator type="requiredstring"> + <message>pet.name.required</message> + </field-validator> + </field> + +</validators> \ No newline at end of file Property changes on: trunk/nuiton-validator/src/test/resources/org/nuiton/validator/model/Pet-error-validation.xml ___________________________________________________________________ Added: svn:keywords + Author Date Id Revision Added: svn:eol-style + native
participants (1)
-
tchemit@users.nuiton.org