2

EclipsLink doesnt' seem to detect or fire JSR303 annotation constraints in a base class that is the mapped super class of an entity during a persist() operation.

For example:

public Base
{
    @NotNull
    private Integer id; 

    private String recordName; 

    //other stuff (getters etc) 
}

and then

public class MyObject
    extends Base
{
     //stuff...
}

and then:

<mapped-superclass class="Base">
    <attributes>
         <basic name="recordName">
             <column name = "NAME" />
         </basic>
    </attributes>
</mapped-superclass> 

and finally:

<entity class="MyObject">
    <table name="TheTable"/>
        <attributes>
            <id name="id">
                <column name="recordId" />
            </id>
        </attributes>
</entity>     

Some other relevant parameters:

  • using jpa 2.1 -- specifically eclipslink 2.6.2 and 2.6.3
  • I am integration testing - so java se (and spock)
  • JDK 1.8.77
  • I do have hibernate validator in my classpath (org.hibernate:hibernate-validator:5.2.4.Final)
  • If I write a test fixture and use validitor.validate() directly (no jpa or persist) hibernate validator works as expected.
  • I do NOT use JPA annotations and only use ORM xml to declare entity mappings.
  • I do use JSR303 annotations to mark attrs and props with constraints.
  • persistence.xml is marked with validation "AUTO" and many variations of properties like javax.persistence.validation.group.pre-persist with FQDN of marker interfaces have been tried.

As mentioned, calling em.persist(myObjectInst) will not fire any 303 annotations added to class 'Base'.

* Is there some tuning parameter or switch I can tinker with that will make this work? *

Note: I did a deep-dive debug on this and can see that org.eclipse.persistence.internal.jpa.metadata.beanvalidation.BeanValidationHelper.detectConstraints() does NOT look at any parent classes for JSR303 annotations. It seems to only want to look at the specific entity class. I'd hazard to guess that if I moved my JSR303 constraints to the concrete (or entity class); it may just work. But then I would loose the extension and mapped super class stuff. So what fun is that?

UPDATE Looks like issue in EclipseLink 2.6.x. See here ( https://www.eclipse.org/forums/index.php?t=msg&th=1077658&goto=1732842&#msg_1732842 ) for more details.

fwelland
  • 545
  • 5
  • 17
  • 2
    Sounds to me like a bug in org.eclipse.persistence.internal.jpa.metadata.beanvalidation.BeanValidationHelper. Did you check their bug tracker or report a bug. Bean Validation and JPA are in-depended, so it is ok to use JPA annotation, but just Bean Validation ones. Your example should work imo. – Hardy May 19 '16 at 08:26
  • Looks like issue in EclipseLink 2.6.x. See here ( https://www.eclipse.org/forums/index.php?t=msg&th=1077658&goto=1732842msg_1732842 ) for more details. – fwelland May 25 '16 at 17:44

1 Answers1

2

From what I can see, eclipse link 2.6.X up to 2.6.4 seems to have a Massive bug in terms of upholding its contract of triggering JSR 303 bean validations. Right now, eclipselink 2.6.4 only triggers these validations if your child entity is right-out flagged with constraints.

I have integration tests that work perfectly under JEE 6 library versions (e.g. eclipselink 2.4.x).

When I upgrade libraries to JEE 7 verions, in the particular case of ecliselink this means versions: 2.6.1 up to 2.6.4, they all manifest the same bug.

The broken unit tests I have analyzed so far, are validating that ConstraintViolationExceptions, such as not null, must get triggered.

So if you take an Entity A that extends abstract entity B. And abstract entity B is a @MappedSuperClass. You will have problems if your @NotNull or any other such constraints is found on your abstract entity B ... In this case, things will not go well.

No constraint violation gets triggered by eclipselink. Instead, it is the DB that stops you if you do issue the commit() or flush() in the test. Eclipse-link will rollback on the db exception.

However, as soon as you go to entity A and you pump into it a dummy field: @NotNull private String dummy;

This is sufficient to make the Validator (e.g. hibernate validator) get called.

In this case, my the test still fails because now I get twotwo @NotNull constraint validations instead of one.

In the following snippet I illustrate the relevant chucnk of stack trace on eclipselink 2.6.1.

Caused by: javax.validation.ConstraintViolationException: 
Bean Validation constraint(s) violated while executing Automatic Bean Validation on callback event:'prePersist'. 
Please refer to embedded ConstraintViolations for details.
    at org.eclipse.persistence.internal.jpa.metadata.listeners.BeanValidationListener.validateOnCallbackEvent(BeanValidationListener.java:108)
    at org.eclipse.persistence.internal.jpa.metadata.listeners.BeanValidationListener.prePersist(BeanValidationListener.java:77)
    at org.eclipse.persistence.descriptors.DescriptorEventManager.notifyListener(DescriptorEventManager.java:748)
    at org.eclipse.persistence.descriptors.DescriptorEventManager.notifyEJB30Listeners(DescriptorEventManager.java:691)
    at org.eclipse.persistence.descriptors.DescriptorEventManager.executeEvent(DescriptorEventManager.java:229)
    at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.registerNewObjectClone(UnitOfWorkImpl.java:4314)
    at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.registerNotRegisteredNewObjectForPersist(UnitOfWorkImpl.java:4291)
    at org.eclipse.persistence.internal.sessions.RepeatableWriteUnitOfWork.registerNotRegisteredNewObjectForPersist(RepeatableWriteUnitOfWork.java:521)
    at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.registerNewObjectForPersist(UnitOfWorkImpl.java:4233)
    at org.eclipse.persistence.internal.jpa.EntityManagerImpl.persist(EntityManagerImpl.java:507)
    at TEST_THAT_IS_PROBLEMATIC
    ... 25 more

In the stack trace above, you have the unit test doing an em.persist() on entity A, and this case entity A has the dummy @NotNull field. So the validation gets called.

The bug in eclipselink seems to be when the BeanValidationListener asks the BeanValidationHelper if a class is constrained or not:

The code from Eclipselink is as follows:

private void validateOnCallbackEvent(DescriptorEvent event, String callbackEventName, Class[] validationGroup) {
        Object source = event.getSource();
        boolean noOptimization = "true".equalsIgnoreCase((String) event.getSession().getProperty(PersistenceUnitProperties.BEAN_VALIDATION_NO_OPTIMISATION));
        boolean shouldValidate = noOptimization || beanValidationHelper.isConstrained(source.getClass());
        if (shouldValidate) {
            Set<ConstraintViolation<Object>> constraintViolations = getValidator(event).validate(source, validationGroup);
            if (constraintViolations.size() > 0) {
                // There were errors while call to validate above.
                // Throw a ConstrainViolationException as required by the spec.
                // The transaction would be rolled back automatically
                // TODO need to I18N this.
                throw new ConstraintViolationException(
                        "Bean Validation constraint(s) violated while executing Automatic Bean Validation on callback event:'" +
                                callbackEventName + "'. Please refer to embedded ConstraintViolations for details.",
                        (Set<ConstraintViolation<?>>) (Object) constraintViolations); /* Do not remove the explicit
                        cast. This issue is related to capture#a not being instance of capture#b. */
            }
        }
    }

And the problem is that the query:

beanValidationHelper.isConstrained(source.getClass());

Returns false, this is completely wrong.

Finally, if you check the implemation of the BeanValidationHelper, the intial part of the code looks as follows:

 private Boolean detectConstraints(Class<?> clazz) {
        for (Field f : ReflectionUtils.getDeclaredFields(clazz)) {
            for (Annotation a : f.getDeclaredAnnotations()) {
                final Class<? extends Annotation> type = a.annotationType();
                if (KNOWN_CONSTRAINTS.contains(type.getName())){
                    return true;
                }
                // Check for custom annotations on the field (+ check inheritance on class annotations).
                // Custom bean validation annotation is defined by having @Constraint annotation on its class.
                for (Annotation typesClassAnnotation : type.getAnnotations()) {
                    final Class<? extends Annotation> classAnnotationType = typesClassAnnotation.annotationType();
                    if (Constraint.class == classAnnotationType) {
                        KNOWN_CONSTRAINTS.add(type.getName());
                        return true;
                    }
                }
            }
        }

The implementation above is clearly wrong, because the method as whole is non recursive and the apis it uses from reflection are themselves non recursive. They look only at the current instance. If you see the following stack overflow thread:

What is the difference between getFields and getDeclaredFields in Java reflection

It is clearly explained by the top ranked answer that:

Field f : ReflectionUtils.getDeclaredFields(clazz)

Only returns you the fields for the current class but not the parents.

What I am seing myself force to do in the mean time is to put in place this workaround to froce the broken algorithm in the BeanValidationHelper to detect the class as one needing to be validated:

 @Transient
    @NotNull
    private final char waitForEclipseLinkToFixTheVersion264 = 'a';

By doing as above, you have your code clearly flagged with a chunk that you can in the future remove. And since the field is transient ... hey, it does not change your DB.

Please note as well that the eclipselink forum now has additional information. The bug goes deeper than than just the improper tracking of when "beanValidation" is needed in the BeanValidationListner.class. The bug has second depth. The BeanValidationListner.class provided with eclipse-link also does not register any implementation for the: PreWriteEvent and for the DescriptorEventManager.PreInsertEvent.

So when the "DeferredCachedDetectionPolocy.class is calculatinChanges(), if your entity A has JSR fields and it still does not get JSR 303 validation. This is most likely happening to you because your enitya was: T0: Persisted and validatioons wen through ok T1: you modify the peristed entity in the same transaction, and when the calculateChanges invokes the event litenters. The BeanValidationListner.class does not care about a preInsertEvent. It just assumes the validation was done a prePersist and does not invoke the validation at all.

The work-around for this, I am not yet sure. I will be looking at how to register an event listner during PreInserPhase, that does the same as the BeanValidationListner. Or, I will be locally patching the BeanValidationListner.class to subscribe to the PreINsert event.

I hade modifying code of libraries maintained by others, so I will go first for the approach of our own eventListner as a temprorary workarund for this bug.

Adding repository that allows to verify both bugs. https://github.com/99sono/EclipseLink_2_6_4_JSR_303Bug

For bug number 2 the following EventListner can server a a temporary work-around, until eclipse link 2.6.4 fixes their bean validation orchestration logic.

package jpa.eclipselink.test.bug2workaround;

import java.util.Map;

import javax.validation.Validation;
import javax.validation.ValidatorFactory;

import org.eclipse.persistence.config.PersistenceUnitProperties;
import org.eclipse.persistence.descriptors.DescriptorEvent;
import org.eclipse.persistence.descriptors.DescriptorEventAdapter;
import org.eclipse.persistence.descriptors.changetracking.DeferredChangeDetectionPolicy;
import org.eclipse.persistence.internal.jpa.deployment.BeanValidationInitializationHelper;
import org.eclipse.persistence.internal.jpa.metadata.listeners.BeanValidationListener;

/**
 * Temporary work-around for JSR 303 bean validation flow in eclipselink.
 *
 * <P>
 * Problem: <br>
 * The
 * {@link DeferredChangeDetectionPolicy#calculateChanges(Object, Object, boolean, org.eclipse.persistence.internal.sessions.UnitOfWorkChangeSet, org.eclipse.persistence.internal.sessions.UnitOfWorkImpl, org.eclipse.persistence.descriptors.ClassDescriptor, boolean)}
 * during a flush will do one of the following: <br>
 * {@code descriptor.getEventManager().executeEvent(new DescriptorEvent(DescriptorEventManager.PreInsertEvent, writeQuery)); }
 * or <br>
 *
 * {@code descriptor.getEventManager().executeEvent(new DescriptorEvent(DescriptorEventManager.PreUpdateEvent, writeQuery)); }
 *
 * <P>
 * WHe it does
 * {@code descriptor.getEventManager().executeEvent(new DescriptorEvent(DescriptorEventManager.PreInsertEvent, writeQuery)); }
 * the {@link BeanValidationListener} will not do anything. We want it to do bean validation.
 */
public class ForceBeanManagerValidationOnPreInsert extends DescriptorEventAdapter {

    private static final Class[] DUMMY_GROUP_PARAMETER = null;

    /**
     * This is is the EJB validator that eclipselink uses to do JSR 303 validations during pre-update, pre-delete,
     * pre-persist, but not pre-insert.
     *
     * Do not access this field directly. Use the {@link #getBeanValidationListener(DescriptorEvent)} api to get it, as
     * this api will initialize the tool if necessary.
     */
    BeanValidationListener beanValidationListener = null;

    final Object beanValidationListenerLock = new Object();

    /**
     *
     */
    public ForceBeanManagerValidationOnPreInsert() {
        super();

    }

    /**
     * As a work-around we want to do bean validation that the container is currently not doing.
     */
    @Override
    public void preInsert(DescriptorEvent event) {
        // (a) get for ourselves an instances of the eclipse link " Step 4 - Notify internal listeners."
        // that knows how to run JSR 303 validations on beans associated to descriptor events
        BeanValidationListener eclipseLinkBeanValidationListenerTool = getBeanValidationListener(event);

        // (b) let the validation listener run its pre-update logic on a preInsert it serves our purpose
        eclipseLinkBeanValidationListenerTool.preUpdate(event);

    }

    /**
     * Returns the BeanValidationListener that knows how to do JSR 303 validation. Creates a new instance if needed,
     * otherwise return the already created listener.
     *
     * <P>
     * We can only initialize our {@link BeanValidationListener} during runtime, to get access to the JPA persistence
     * unit properties. (e.g. to the validation factory).
     *
     * @param event
     *            This event describes an ongoing insert, updetae, delete event on an entity and for which we may want
     *            to force eclipselink to kill the transaction if a JSR bean validation fails.
     * @return the BeanValidationListener that knows how to do JSR 303 validation.
     */
    protected BeanValidationListener getBeanValidationListener(DescriptorEvent event) {
        synchronized (beanValidationListenerLock) {
            // (a) initializae our BeanValidationListener if needed
            boolean initializationNeeded = beanValidationListener == null;
            if (initializationNeeded) {
                beanValidationListener = createBeanValidationListener(event);
            }

            // (b) return the validation listerner that is normally used by eclipse link
            // for pre-persist, pre-update and pre-delete so that we can force it run on pre-insert
            return beanValidationListener;
        }

    }

    /**
     * Creates a new instance of the {@link BeanValidationListener} that comes with eclipse link.
     *
     * @param event
     *            the ongoing db event (e.g. pre-insert) where we want to trigger JSR 303 bean validation.
     *
     * @return A new a new instance of the {@link BeanValidationListener} .
     */
    protected BeanValidationListener createBeanValidationListener(DescriptorEvent event) {
        Map peristenceUnitProperties = event.getSession().getProperties();
        ValidatorFactory validatorFactory = getValidatorFactory(peristenceUnitProperties);
        return new BeanValidationListener(validatorFactory, DUMMY_GROUP_PARAMETER, DUMMY_GROUP_PARAMETER,
                DUMMY_GROUP_PARAMETER);
    }

    /**
     * Snippet of code taken out of {@link BeanValidationInitializationHelper}
     *
     * @param puProperties
     *            the persistence unit properties that may be specifying the JSR 303 validation factory.
     * @return the validation factory that can check if a bean is violating business rules. Almost everyone uses
     *         hirbernate JSR 303 validation.
     */
    protected ValidatorFactory getValidatorFactory(Map puProperties) {
        ValidatorFactory validatorFactory = (ValidatorFactory) puProperties
                .get(PersistenceUnitProperties.VALIDATOR_FACTORY);

        if (validatorFactory == null) {
            validatorFactory = Validation.buildDefaultValidatorFactory();
        }
        return validatorFactory;
    }

}

Simply add this bean validator to the class, preferably a base abstract class to ensure JSR 303 validation will happen in pre-insert. This should work-around the hole that allows us to commit dirtyu entities violating business rules to the db.

Here is an example of an entity with the work-around in place.

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DESCRIMINATOR", length = 32)
@DiscriminatorValue("Bug2WorkAround")
@Entity
@EntityListeners({ ForceBeanManagerValidationOnPreInsert.class })
public class Bug2Entity2WithWorkAround extends GenericEntity {

Kind regards.

Community
  • 1
  • 1
99Sono
  • 3,554
  • 27
  • 39