All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.smooks.cartridges.validation.Validator Maven / Gradle / Ivy

Go to download

The Smooks Validation Cartridge builds on the functionality provided by the Rules Cartridge to provide rules-based fragment validation

The newest version!
/*-
 * ========================LICENSE_START=================================
 * smooks-validation-cartridge
 * %%
 * Copyright (C) 2020 Smooks
 * %%
 * Licensed under the terms of the Apache License Version 2.0, or
 * the GNU Lesser General Public License version 3.0 or later.
 *
 * SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-or-later
 *
 * ======================================================================
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * ======================================================================
 *
 * 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 * =========================LICENSE_END==================================
 */
package org.smooks.cartridges.validation;

import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.smooks.api.ApplicationContext;
import org.smooks.api.ExecutionContext;
import org.smooks.api.SmooksConfigException;
import org.smooks.api.SmooksException;
import org.smooks.api.io.Sink;
import org.smooks.api.resource.config.ResourceConfig;
import org.smooks.api.resource.visitor.VisitAfterReport;
import org.smooks.api.resource.visitor.VisitBeforeReport;
import org.smooks.api.resource.visitor.sax.ng.AfterVisitor;
import org.smooks.api.resource.visitor.sax.ng.ChildrenVisitor;
import org.smooks.cartridges.rules.RuleEvalResult;
import org.smooks.cartridges.rules.RuleProvider;
import org.smooks.cartridges.rules.RuleProviderAccessor;
import org.smooks.engine.delivery.fragment.NodeFragment;
import org.smooks.engine.memento.TextAccumulatorMemento;
import org.smooks.engine.memento.TextAccumulatorVisitorMemento;
import org.smooks.engine.resource.config.xpath.IndexedSelectorPath;
import org.smooks.engine.resource.config.xpath.step.AttributeSelectorStep;
import org.smooks.resource.URIResourceLocator;
import org.smooks.support.DomUtils;
import org.smooks.support.FreeMarkerTemplate;
import org.w3c.dom.CharacterData;
import org.w3c.dom.Element;

import javax.inject.Inject;
import javax.inject.Named;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Optional;
import java.util.ResourceBundle;

/**
 * 

* A Validator uses a predefined Rule that performs the actual validator for a Validator. This way a Validator does not know * about the technology used for the validation and users can mix and max different rules as appropriate to the use case they * have. For example, one problem might be solve nicely with a regular expression but another might be easier to sovle using * a MVEL expression. *

* Example configuration: *

{@code
 * 
 *    
 * 
 *
 * 
 *
 * }
* Options: *
    *
  • on * The fragement that the validation will be performed upon.
  • * *
  • rule * Is the name of a previously defined in a rules element. The rule itself is identified by ruleProviderName.ruleName. * So taking the above example addressing is the ruleProviderName and email is the rule name. In this case email * identifies a regular expression but if you were to change the provider that might change and a differnet technology * could be used to validate an email address.
  • * *
  • onFail * The onFail attribute in the validation configuration specified what action should be taken when a rule matches. * This is all about reporting back valdiation failures. *
  • * *
* * @author Daniel Bevenius */ @VisitBeforeReport(condition = "false") @VisitAfterReport(summary = "Applied validation rule '${resource.parameters.name}'.") public final class Validator implements ChildrenVisitor, AfterVisitor { private static final Logger LOGGER = LoggerFactory.getLogger(Validator.class); /** * The name of the rule that will be used by this validator. */ private String compositRuleName; /** * Rule provider name. */ private String ruleProviderName; /** * Rule name. */ private String ruleName; /** * Rule provider for this validator. */ private RuleProvider ruleProvider; /** * The validation failure level. Default is OnFail.ERROR. */ private OnFail onFail = OnFail.ERROR; /** * The Smooks {@link ApplicationContext}. */ @Inject private ApplicationContext appContext; /** * Config. */ @Inject private ResourceConfig resourceConfig; /** * Attribute name if the validation target is an attribute, otherwise null. */ private String targetAttribute; /** * Message bundle name for the ruleset. */ private String messageBundleBaseName; /** * The maximum number of failures permitted per {@link ValidationSink} instance.. */ private int maxFails; /** * No-args constructor required by Smooks. */ public Validator() { } /** * Initialize the visitor instance. */ @PostConstruct public void postConstruct() { if (resourceConfig.getSelectorPath() instanceof IndexedSelectorPath && ((IndexedSelectorPath) resourceConfig.getSelectorPath()).getTargetSelectorStep() instanceof AttributeSelectorStep) { targetAttribute = ((AttributeSelectorStep) ((IndexedSelectorPath) resourceConfig.getSelectorPath()). getTargetSelectorStep()).getQName().getLocalPart(); } else { targetAttribute = null; } } /** * Public constructor. * * @param compositeRuleName The name of the rule that will be used by this validator. * @param onFail The failure level. */ public Validator(final String compositeRuleName, final OnFail onFail) { setCompositRuleName(compositeRuleName); this.onFail = onFail; } @Override public void visitAfter(final Element element, final ExecutionContext executionContext) throws SmooksException { if (targetAttribute != null) { OnFailResultImpl result = _validate(element.getAttribute(targetAttribute), executionContext); if (result != null) { result.setFailFragmentPath(DomUtils.getXPath(element) + "/@" + targetAttribute); assertValidationException(result, executionContext); } } else { TextAccumulatorMemento textAccumulatorMemento = new TextAccumulatorVisitorMemento(new NodeFragment(element), this); executionContext.getMementoCaretaker().restore(textAccumulatorMemento); OnFailResultImpl result = _validate(textAccumulatorMemento.getText(), executionContext); if (result != null) { result.setFailFragmentPath(DomUtils.getXPath(element)); assertValidationException(result, executionContext); } } } private void assertValidationException(OnFailResultImpl result, ExecutionContext executionContext) { if (onFail == OnFail.FATAL) { throw new ValidationException("A FATAL validation failure has occured " + result, result); } ValidationSink validationResult = getValidationResult(executionContext); if (validationResult.getNumFailures() > maxFails) { throw new ValidationException("The maximum number of allowed validation failures (" + maxFails + ") has been exceeded.", result); } } /** * Validate will lookup the configured RuleProvider and validate the text against the * rule specfied by the composite rule name. * * @param text The selected data to perform the evaluation on. * @param executionContext The Smooks {@link org.smooks.api.ExecutionContext}. * @throws ValidationException A FATAL Validation failure has occured, or the maximum number of * allowed failures has been exceeded. */ void validate(final String text, final ExecutionContext executionContext) throws ValidationException { OnFailResultImpl result = _validate(text, executionContext); if (result != null) { assertValidationException(result, executionContext); } } /** * Validate will lookup the configured RuleProvider and validate the text against the * rule specfied by the composite rule name. * * @param text The selected data to perform the evaluation on. * @param executionContext The Smooks {@link org.smooks.api.ExecutionContext}. * @throws ValidationException A FATAL Validation failure has occured, or the maximum number of * allowed failures has been exceeded. */ private OnFailResultImpl _validate(final String text, final ExecutionContext executionContext) throws ValidationException { if (ruleProvider == null) { setRuleProvider(executionContext); } final RuleEvalResult result = ruleProvider.evaluate(ruleName, text, executionContext); if (LOGGER.isDebugEnabled()) { LOGGER.debug(result.toString()); } if (!result.matched()) { ValidationSink validationResult = getValidationResult(executionContext); OnFailResultImpl onFailResult = new OnFailResultImpl(); onFailResult.setRuleResult(result); onFailResult.setBeanContext(executionContext.getBeanContext().getBeanMap()); validationResult.addResult(onFailResult, onFail); return onFailResult; } return null; } private ValidationSink getValidationResult(ExecutionContext executionContext) { Optional validationSink = executionContext.getOrDefault(Sink.SINKS_TYPED_KEY, Collections.emptyList()).stream().filter(s -> ValidationSink.class.isAssignableFrom(s.getClass())).findFirst(); // Create a new ValidationSink if one was not available in the execution context. // This would be the case for example if one as not specified to Smooks filter method. return validationSink.map(sink -> (ValidationSink) sink).orElseGet(ValidationSink::new); } private synchronized void setRuleProvider(ExecutionContext executionContext) { if (ruleProvider != null) { return; } ruleProvider = RuleProviderAccessor.get(appContext, ruleProviderName); if (ruleProvider == null) { throw new SmooksException("Unknown rule provider '" + ruleProviderName + "'."); } // Configure the base bundle name for validation failure messages... setMessageBundleBaseName(); // Configure the maxFails per ValidationResult instance... String maxFailsConfig = executionContext.getConfigParameter(OnFailResult.MAX_FAILS); if (maxFailsConfig != null) { try { maxFails = Integer.parseInt(maxFailsConfig.trim()); } catch (NumberFormatException e) { throw new SmooksConfigException("Invalid config value '" + maxFailsConfig.trim() + "' for global parameter '" + OnFailResult.MAX_FAILS + "'. Must be a valid Integer value."); } } else { maxFails = Integer.MAX_VALUE; } } private void setMessageBundleBaseName() { String ruleSource = ruleProvider.getSrc(); File srcFile = new File(ruleSource); String srcFileName = srcFile.getName(); int indexOfExt = srcFileName.lastIndexOf('.'); File parentFolder = srcFile.getParentFile(); if (indexOfExt != -1) { messageBundleBaseName = srcFileName.substring(0, indexOfExt); } else { messageBundleBaseName = ruleSource; } if (parentFolder != null) { messageBundleBaseName = parentFolder.getPath() + "/i18n/" + messageBundleBaseName; } else { messageBundleBaseName = "i18n/" + messageBundleBaseName; } messageBundleBaseName = messageBundleBaseName.replace('\\', '/'); } @Override public String toString() { return String.format("%s [rule=%s, onFail=%s]", getClass().getSimpleName(), compositRuleName, onFail); } @Inject public void setCompositRuleName(@Named("name") final String compositRuleName) { this.compositRuleName = compositRuleName; this.ruleProviderName = RuleProviderAccessor.parseRuleProviderName(compositRuleName); this.ruleName = RuleProviderAccessor.parseRuleName(compositRuleName); } public String getCompositRuleName() { return compositRuleName; } @Inject public void setOnFail(final Optional onFail) { this.onFail = onFail.orElse(OnFail.ERROR); } public OnFail getOnFail() { return onFail; } public Validator setAppContext(ApplicationContext appContext) { this.appContext = appContext; return this; } @Override public void visitChildText(CharacterData characterData, ExecutionContext executionContext) { if (targetAttribute == null) { // The selected text is not an attribute, which means it's the element text, // which means we need to turn on text accumulation for SAX... TextAccumulatorMemento textAccumulatorMemento = new TextAccumulatorVisitorMemento(new NodeFragment(characterData.getParentNode()), this); executionContext.getMementoCaretaker().restore(textAccumulatorMemento); textAccumulatorMemento.accumulateText(characterData.getTextContent()); executionContext.getMementoCaretaker().capture(textAccumulatorMemento); } } @Override public void visitChildElement(Element childElement, ExecutionContext executionContext) { } private class OnFailResultImpl implements OnFailResult { private String failFragmentPath; private RuleEvalResult ruleResult; public Map beanContext; public void setFailFragmentPath(String failFragmentPath) { this.failFragmentPath = failFragmentPath; } public String getFailFragmentPath() { return failFragmentPath; } public void setRuleResult(RuleEvalResult ruleResult) { this.ruleResult = ruleResult; } public RuleEvalResult getFailRuleResult() { return ruleResult; } public void setBeanContext(Map beanContext) { // Need to create a shallow copy as the context data may change. // Even this is not foolproof, as internal bean data can also be // overwritten by the bean context!! this.beanContext = new HashMap(); this.beanContext.putAll(beanContext); } public String getMessage() { return getMessage(Locale.getDefault()); } public String getMessage(Locale locale) { if (ruleResult.getEvalException() != null) { return ruleResult.getEvalException().getMessage(); } String message = getMessage(locale, ruleName); // If no ResouceBundle was configured then use this instances toString if (message == null) { return toString(); } if (message.startsWith("ftl:")) { // TODO: Is there a way to optimize this e.g. attach the compiled template // to the bundle as an object and then get back using ResourceBundle.getObject?? // I timed it and it was able to create and apply 10000 templates in about 2500 ms // on an "average" spec machine, so it's not toooooo bad, and it's only done on demand :) FreeMarkerTemplate template = new FreeMarkerTemplate(message.substring("ftl:".length())); beanContext.put("ruleResult", ruleResult); beanContext.put("path", failFragmentPath); message = template.apply(beanContext); } return message; } private String getMessage(final Locale locale, final String messageName) { final ResourceBundle bundle = getMessageBundle(locale); if (messageName == null || bundle == null) return null; return bundle.getString(messageName); } /** * @param locale The Locale to look up. * @return {@link ResourceBundle} for the Locale and message bundle base name. Or null if no bundle exists. */ private ResourceBundle getMessageBundle(final Locale locale) { try { return ResourceBundle.getBundle(messageBundleBaseName, locale, new ResourceBundleClassLoader(appContext.getClassLoader())); } catch (final MissingResourceException e) { LOGGER.warn("Failed to load Validation rule message bundle '" + messageBundleBaseName + "'. This resource must be on the classpath!", e); } return null; } @Override public String toString() { return "[" + failFragmentPath + "] " + ruleResult.toString(); } } private static class ResourceBundleClassLoader extends ClassLoader { private final ClassLoader classLoader; public ResourceBundleClassLoader(ClassLoader classLoader) { this.classLoader = classLoader; } @Override public Class loadClass(String name) throws ClassNotFoundException { return classLoader.loadClass(name); } @Override protected Class findClass(String name) throws ClassNotFoundException { return super.findClass(name); } @Override public URL getResource(String name) { return classLoader.getResource(name); } @Override public Enumeration getResources(String name) throws IOException { return classLoader.getResources(name); } @Override public void setDefaultAssertionStatus(boolean enabled) { classLoader.setDefaultAssertionStatus(enabled); } @Override public void setPackageAssertionStatus(String packageName, boolean enabled) { classLoader.setPackageAssertionStatus(packageName, enabled); } @Override public void setClassAssertionStatus(String className, boolean enabled) { classLoader.setClassAssertionStatus(className, enabled); } @Override public void clearAssertionStatus() { classLoader.clearAssertionStatus(); } @Override public InputStream getResourceAsStream(String name) { try { return new URIResourceLocator().getResource(name); } catch (IOException e) { return null; } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy