com.autobizlogic.abl.rule.ConstraintRule.java Source code

Java tutorial

Introduction

Here is the source code for com.autobizlogic.abl.rule.ConstraintRule.java

Source

package com.autobizlogic.abl.rule;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.apache.commons.jexl2.Expression;
import org.apache.commons.jexl2.JexlContext;
import org.apache.commons.jexl2.JexlEngine;

import com.autobizlogic.abl.data.PersistentBean;
import com.autobizlogic.abl.engine.ConstraintFailure;
import com.autobizlogic.abl.engine.LogicRunner;
import com.autobizlogic.abl.engine.InternalConstraintException;
import com.autobizlogic.abl.logic.LogicContext;
import com.autobizlogic.abl.perf.PerformanceMonitor;
import com.autobizlogic.abl.engine.LogicException;
import com.autobizlogic.abl.event.LogicAfterConstraintEvent;
import com.autobizlogic.abl.session.LogicTransactionContext;
import com.autobizlogic.abl.text.LogicMessageFormatter;
import com.autobizlogic.abl.text.MessageName;
import com.autobizlogic.abl.util.MethodInvocationUtil;
import com.autobizlogic.abl.util.NodalPathUtil;

/**
 * Define and execute a constraint in the business logic.
 */
public class ConstraintRule extends AbstractDependsOnWithVerbsRule {

    protected String[] problemAttributes;

    private String expression = null;

    private String errorMessage = null;

    protected static final JexlEngine jexlEngine = new JexlEngine();
    static {
        jexlEngine.setCache(512);
        jexlEngine.setSilent(false);
    }

    //////////////////////////////////////////////////////////////////////

    protected ConstraintRule(LogicGroup logicGroup, String logicMethodName) {
        this.logicGroup = logicGroup;
        this.logicMethodName = logicMethodName;
    }

    /**
     * Get the names of the attributes
     * @return
     */
    public String[] getProblemAttributes() {
        return problemAttributes;
    }

    /**
     * Get the Jexl expression defined in the annotation, if present.
     */
    public String getExpression() {
        return expression;
    }

    protected void setExpression(String s) {
        expression = s;
    }

    /**
     * Get the error message defined in the annotation, if present.
     */
    public String getErrorMessage() {
        return errorMessage;
    }

    protected void setErrorMessage(String s) {
        errorMessage = s;
    }

    /**
     * Translate the expression into a valid SQL expression
     */
    public String getExpressionSQL() {

        if (expression == null)
            return null;
        String sqlClause = expression.replaceAll("\\|\\|", " OR ");
        sqlClause = sqlClause.replaceAll("&&", " AND ");
        sqlClause = sqlClause.replaceAll("==null", " IS NULL ");
        sqlClause = sqlClause.replaceAll("== null", " IS NULL ");
        sqlClause = sqlClause.replaceAll("!=null", " IS NOT NULL ");
        sqlClause = sqlClause.replaceAll("!= null", " IS NOT NULL ");
        sqlClause = sqlClause.replaceAll("==", "=");
        sqlClause = sqlClause.replaceAll("!=", "<>");
        sqlClause = sqlClause.replaceAll("!", " NOT ");
        return sqlClause;
    }

    /**
     * Get the error message defined in the annotation, formatted for the given bean.
     * If no error message was defined in the annotation, a generic message is used.
     */
    public String getFormattedErrorMessage(PersistentBean bean) {
        if (errorMessage == null)
            return LogicMessageFormatter.getMessage(MessageName.rule_Constraint_genericFailure,
                    new Object[] { getBeanAttributeName(), bean });

        if (errorMessage.indexOf('{') == -1)
            return errorMessage;

        StringBuffer formattedMsg = new StringBuffer();
        int braceIdx = 0;
        int startIdx = 0;
        int closeBraceIdx = 0;
        while (braceIdx != -1 && braceIdx < errorMessage.length() - 1) {
            braceIdx = errorMessage.indexOf('{', braceIdx);
            if (braceIdx == -1)
                break;
            closeBraceIdx = errorMessage.indexOf('}', braceIdx);
            if (braceIdx == -1) {
                break;
            }
            formattedMsg.append(errorMessage.substring(startIdx, braceIdx));
            String expr = errorMessage.substring(braceIdx + 1, closeBraceIdx);
            JexlEngine jexl = new JexlEngine();
            try {
                Expression e = jexl.createExpression(expr);
                BeanMapContext ctxt = new BeanMapContext(bean, null, false);
                Object result = e.evaluate(ctxt);
                if (result != null)
                    formattedMsg.append(result.toString());
                else
                    formattedMsg.append("<null>");
            } catch (Exception ex) {
                formattedMsg.append("<error>");
            }

            startIdx = closeBraceIdx + 1;
            braceIdx++;
        }
        if (closeBraceIdx > 0 && closeBraceIdx < errorMessage.length() - 1)
            formattedMsg.append(errorMessage.substring(closeBraceIdx + 1));

        return formattedMsg.toString();
    }

    public void execute(PersistentBean bean, LogicRunner aLogicRunner) {
        // Nothing to do here
    }

    /**
     * Execute the constraint method.
     */
    public ConstraintFailure executeConstraint(LogicRunner aLogicRunner) {

        // Users must not refer to transients aggregates on delete, else we must drop delete constraints like this...
        // We do not run constraints on deleted objects, because, well, they're deleted, and therefore
        // we often don't have access to e.g. their children (because they've been deleted too).
        //if (aLogicRunner.getVerb() == Verb.DELETE)
        //   return null;

        long startTime = System.nanoTime();
        ConstraintFailure failure = null;
        try {
            if (expression != null && expression.trim().length() > 0) {
                executeDeclaredConstraint(aLogicRunner.getLogicObject(), aLogicRunner.getCurrentDomainObject(),
                        false, aLogicRunner.getLogicContext());
            } else {
                String theLogicMethodName = getLogicMethodName();
                Class<?> logicClass = aLogicRunner.getLogicObject().getClass();
                Method constraintMethod = logicClass.getMethod(theLogicMethodName, (Class<?>[]) null);
                if (constraintMethod == null)
                    throw new RuntimeException("Unable to find constraint method " + theLogicMethodName
                            + " in class " + logicClass.getName());
                constraintMethod.invoke(aLogicRunner.getLogicObject(), (Object[]) null);
            }
        } catch (NoSuchMethodException e) {
            throw new LogicException("Failure finding / executing constraint: " + logicMethodName + " on: "
                    + aLogicRunner.getLogicObject(), e);
        } catch (IllegalAccessException e) {
            throw new LogicException("Failure finding or executing constraint: " + logicMethodName + " on: "
                    + aLogicRunner.getLogicObject(), e);
        } catch (InvocationTargetException e) { // this is the exception we get for failed constraints
            Throwable cause = e.getCause();
            if (cause != null && (cause instanceof InternalConstraintException)) {
                InternalConstraintException ice = (InternalConstraintException) cause;
                String[] atts = ice.getProblemAttributes();
                if (atts == null)
                    atts = problemAttributes;
                failure = new ConstraintFailure(ice.getMessage(), atts);
                failure.setLogicClassName(aLogicRunner.getLogicObject().getClass().getName());
                failure.setConstraintName(getLogicMethodName());
                failure.setProblemPk(aLogicRunner.getCurrentDomainObject().getPk());
                if (log.isDebugEnabled()) {
                    log.debug("Constraint failure: " + failure.getLogicClassName() + "."
                            + failure.getConstraintName() + " for object [" + failure.getProblemPk() + "]");
                }
            } else {
                throw new LogicException("Failure finding or executing constraint: " + logicMethodName + " on: "
                        + aLogicRunner.getLogicObject()
                        + ". A common cause is throwing an exception in a constraint "
                        + "without using ConstraintFailure.failConstraint().", e);
            }
        } catch (Exception e) {
            throw new LogicException("Failure finding or executing constraint: " + logicMethodName + " on: "
                    + aLogicRunner.getLogicObject(), e);
        }

        firePostEvent(aLogicRunner.getLogicObject(), aLogicRunner, failure, System.nanoTime() - startTime);

        return failure;
    }

    /**
     * Execute the expression defined in the annotation.
     * @param aLogicObject The logic object for the bean
     * @param bean The bean itself
     * @param skipMethodIfPossible If true, and the constraint is an expression, do not execute the method.
     * @throws InvocationTargetException If the expression did not evaluate successfully
     */
    private void executeDeclaredConstraint(Object aLogicObject, PersistentBean bean, boolean skipMethodIfPossible,
            LogicContext logicContext) throws InvocationTargetException {

        Object result = evaluateExpression(bean, logicContext);
        if (result == null)
            throw new RuntimeException(
                    "Constraint " + this.getLogicGroup().getLogicClassName() + "." + this.getLogicMethodName()
                            + " evaluated to null. A constraint must evaluate to true or false.");
        if (!(result instanceof Boolean))
            throw new RuntimeException(
                    "Constraint " + this.getLogicGroup().getLogicClassName() + "." + this.getLogicMethodName()
                            + " evaluated to a non-boolean value. A constraint must evaluate to true or false.");

        if (!skipMethodIfPossible) {
            try { // Then call the method for debugging purposes, but ignore its return value
                MethodInvocationUtil.invokeMethodOnObject(aLogicObject, logicMethodName);
            } catch (Exception ex) {
                log.warn("Constraint method " + this.getLogicGroup().getLogicClassName() + "."
                        + this.getLogicMethodName() + " threw an exception. This is not fatal because "
                        + "the constraint has an expression in its declaration, and therefore the method itself "
                        + "is called only for debugging convenience, but this should be examined. The exception was: "
                        + ex);
            }
        }

        // If the constraint simply evaluated to false...
        if (!((Boolean) result)) {
            String msg = "Constraint expression " + getLogicGroup().getLogicClassName() + "." + getLogicMethodName()
                    + " evaluated to false.";
            if (errorMessage != null)
                msg = getFormattedErrorMessage(bean);
            throw new InvocationTargetException(new InternalConstraintException(msg));
        }
    }

    /**
     * Execute the constraint method, but without a full context. This is used for post-facto checking.
     * @param aLogicObject The LogicObject currently in use
     * @param bean The persistent bean for which to execute this constraint.
     * @return Null if the constraint succeeded, otherwise a ConstraintFailure with the relevant information.
     */
    public ConstraintFailure executeConstraintForObject(Object aLogicObject, PersistentBean bean) {
        ConstraintFailure failure = null;

        Class<?> logicClass = aLogicObject.getClass();
        String theLogicMethodName = getLogicMethodName();
        try {
            if (expression != null && expression.trim().length() > 0) {
                executeDeclaredConstraint(aLogicObject, bean, true, null);
            } else {
                Method constraintMethod = logicClass.getMethod(theLogicMethodName, (Class<?>[]) null);
                constraintMethod.invoke(aLogicObject, (Object[]) null);
            }
        } catch (NoSuchMethodException e) {
            throw new LogicException(
                    "Failure finding / executing constraint: " + theLogicMethodName + " on: " + aLogicObject, e);
        } catch (IllegalAccessException e) {
            throw new LogicException(
                    "Failure finding or executing constraint: " + theLogicMethodName + " on: " + aLogicObject, e);
        } catch (InvocationTargetException e) { // this is the exception we get for failed constraints
            Throwable cause = e.getCause();
            String causeMsg = cause == null ? "System error - unknown message" : cause.getMessage();
            failure = new ConstraintFailure(causeMsg, problemAttributes);
        } catch (Exception e) {
            throw new LogicException(
                    "Failure finding or executing constraint: " + theLogicMethodName + " on: " + aLogicObject, e);
        }

        return failure;
    }

    /**
     * If the formula is defined with an expression, this is where we evaluate the expression.
     * @param bean The PersistentBean for which the expression must be evaluated
     * @return The result of the expression evaluation.
     */
    private Object evaluateExpression(PersistentBean bean, LogicContext logicContext) {
        if (expression == null || expression.trim().length() == 0)
            return null;

        JexlContext context = new BeanMapContext(bean, null, false);
        Expression expr;
        try {
            expr = jexlEngine.createExpression(expression);
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new LogicException("Error while creating expression : " + expression + " for constraint "
                    + getLogicMethodName() + " in class " + getLogicGroup().getLogicClassName(), ex);
        }
        Object res = null;
        try {
            res = expr.evaluate(context);
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new LogicException("Error while evaluating expression : " + expression, ex);
        }

        return res;
    }

    /**
     * Fire the post event for this constraint.
     */
    protected void firePostEvent(Object aLogicObject, LogicRunner aLogicRunner, ConstraintFailure failure,
            long executionTime) {

        LogicAfterConstraintEvent evt = new LogicAfterConstraintEvent(aLogicRunner.getContext(),
                aLogicRunner.getLogicContext(), this, aLogicRunner.getCurrentDomainObject(), failure);
        evt.setExecutionTime(executionTime);
        LogicTransactionContext.fireEvent(evt);
        PerformanceMonitor.addRuleExecution(this, executionTime);
    }

    ///////////////////////////////////////////////////////////////////////////////////////

    @Override
    public String toString() {
        String s = "Constraint " + NodalPathUtil.getNodalPathLastName(logicGroup.getLogicClassName()) + "#"
                + logicMethodName;
        if (this.getExpression() != null && getExpression().trim().length() > 0)
            s += " [" + getExpression() + "]";
        if (problemAttributes != null) {
            s += " - problem attributes:";
            for (String att : problemAttributes)
                s += att + ", ";
        }
        return s;
    }

    @SuppressWarnings("unused")
    private final static String SVN_ID = "$Id: Version 2.1.5 Build 0602 Date 2012-04-28-14-13  ConstraintRule.java 1207 2012-04-19 22:33:25Z max@automatedbusinesslogic.com $";
}

/*
 * The contents of this file are subject to the Automated Business Logic Public License Version 1.0 (the "License"),
 * which is derived from the Mozilla Public License version 1.1. You may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at http://www.automatedbusinesslogic.com/license/public-license
 *
 * Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, 
 * either express or implied. See the License for the specific language governing rights and limitations under the License.
 */