Addressing several rollback-related issues (#1146)

Addresses several JPA rollback-related issues.

Signed-off-by: Laird Nelson <laird.nelson@oracle.com>
This commit is contained in:
Laird Nelson
2019-12-03 13:19:37 -08:00
committed by GitHub
parent eb999f5c56
commit f5e1cc51e3
30 changed files with 2580 additions and 178 deletions

View File

@@ -108,12 +108,44 @@ public final class ReferenceCountedContext implements AlterableContext {
* @see Contextual#destroy(Object, CreationalContext)
*
* @see #get(Contextual, CreationalContext)
*/
public int decrementReferenceCount(final Contextual<?> c) {
return this.decrementReferenceCount(c, 1);
}
/**
* Decrements the reference count of the contextual instance, if
* any, associated with the combination of the current thread and
* the supplied {@link Contextual}, {@linkplain
* Contextual#destroy(Object, CreationalContext) destroying} the
* instance if and only if the reference count becomes less than
* or equal to zero, and returns the resulting reference count.
*
* <p>Most users will never have to call this method.</p>
*
* @param c the {@link Contextual} whose instance's reference
* count should be decremented; may be {@code null} in which case
* no action will be taken and {@code 0} will be returned
*
* @param amount the amount by which to decrement; must be greater
* than or (trivially) equal to {@code 0}
*
* @return the resulting reference count
*
* @exception IllegalArgumentException if {@code amount} is less
* than {@code 0}
*
* @see Contextual#destroy(Object, CreationalContext)
*
* @see #get(Contextual, CreationalContext)
*
* @see #decrementReferenceCount(Contextual)
*/
public int decrementReferenceCount(final Contextual<?> c) {
public int decrementReferenceCount(final Contextual<?> c, final int amount) {
final int returnValue;
if (c == null) {
if (amount < 0) {
throw new IllegalArgumentException("amount < 0: " + amount);
} else if (c == null) {
returnValue = 0;
} else {
final Map<?, ? extends Instance<?>> instances = ALL_INSTANCES.get().get(this);
@@ -128,7 +160,7 @@ public final class ReferenceCountedContext implements AlterableContext {
// will cause c.destroy(theObject,
// creationalContext) to be called if needed; no
// need to do it explicitly here.
returnValue = instance.decrementReferenceCount();
returnValue = instance.decrementReferenceCount(amount);
if (returnValue <= 0) {
instances.remove(c);
}
@@ -138,6 +170,42 @@ public final class ReferenceCountedContext implements AlterableContext {
return returnValue;
}
/**
* Returns the reference count of the contextual instance, if
* any, associated with the combination of the current thread and
* the supplied {@link Contextual}.
*
* <p>Most users will never have to call this method.</p>
*
* <p>This method never returns a negative number.</p>
*
* @param c the {@link Contextual} whose instance's reference
* count should be returned; may be {@code null} in which case
* {@code 0} will be returned
*
* @return the reference count in question; never a negative
* number
*/
public int getReferenceCount(final Contextual<?> c) {
final int returnValue;
if (c == null) {
returnValue = 0;
} else {
final Map<?, ? extends Instance<?>> instances = ALL_INSTANCES.get().get(this);
if (instances == null) {
returnValue = 0;
} else {
final Instance<?> instance = instances.get(c);
if (instance == null) {
returnValue = 0;
} else {
returnValue = Math.max(0, instance.getReferenceCount());
}
}
}
return returnValue;
}
/**
* Calls the {@link #decrementReferenceCount(Contextual)} method
* with the supplied {@link Contextual}, destroying it if and only
@@ -350,7 +418,7 @@ public final class ReferenceCountedContext implements AlterableContext {
* #get()} invocations will throw {@link
* IllegalStateException}.</p>
*
* @see #decrementReferenceCount()
* @see #decrementReferenceCount(int)
*
* @see #get()
*/
@@ -423,7 +491,7 @@ public final class ReferenceCountedContext implements AlterableContext {
*
* @exception IllegalStateException if the reference count
* internally is less than zero, such as when an invocation of
* {@link #decrementReferenceCount()} has resulted in
* {@link #decrementReferenceCount(int)} has resulted in
* destruction
*/
private T get() {
@@ -435,26 +503,42 @@ public final class ReferenceCountedContext implements AlterableContext {
return this.object;
}
private int getReferenceCount() {
return this.referenceCount;
}
/**
* Decrements this {@link Instance}'s reference count, unless
* it is {@code 0}, and returns the result.
* Decrements this {@link Instance}'s reference count by the
* supplied amount, unless it is already {@code 0}, and
* returns the result.
*
* <p>If the result of decrementing the reference count is
* {@code 0}, then this {@link Instance}'s contextual instance
* is {@linkplain Contextual#destroy(Object,
* CreationalContext) destroyed}, and its internal object
* reference is set to {@code null}. Subsequent invocations
* of {@link #get()} will throw {@link
* less than or equal to {@code 0}, then this {@link
* Instance}'s contextual instance is {@linkplain
* Contextual#destroy(Object, CreationalContext) destroyed},
* and its internal object reference is set to {@code null}.
* Subsequent invocations of {@link #get()} will throw {@link
* IllegalStateException}.</p>
*
* <p>Internally, if a reference count ever drops below {@code
* 0}, it is set to {@code 0}.</p>
*
* @param amount the amount to decrement by; must be greater
* than or (trivially) equal to {@code 0}
*
* @return the resulting decremented reference count
*
* @exception IllegalArgumentException if {@code amount} is
* less than {@code 0}
*
* @exception IllegalStateException if the internal reference
* count is already less than or equal to zero
*/
private int decrementReferenceCount() {
if (this.referenceCount > 0) {
--this.referenceCount;
private int decrementReferenceCount(final int amount) {
if (amount < 0) {
throw new IllegalArgumentException("amount < 0: " + amount);
} else if (this.referenceCount > 0) {
this.referenceCount = Math.max(0, this.referenceCount - amount);
if (this.referenceCount == 0) {
this.contextual.destroy(this.object, this.creationalContext);
if (this.creationalContext != null) {

View File

@@ -15,6 +15,8 @@
*/
package io.helidon.integrations.cdi.referencecountedcontext;
import java.util.Set;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.Initialized;
import javax.enterprise.context.RequestScoped;
@@ -22,6 +24,7 @@ import javax.enterprise.context.control.RequestContextController;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.se.SeContainer;
import javax.enterprise.inject.se.SeContainerInitializer;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.inject.Inject;
@@ -31,6 +34,7 @@ import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@ApplicationScoped
@@ -73,11 +77,21 @@ final class TestReferenceCountedContext {
assertNotNull(beanManager);
assertNotNull(gorpAsParameter);
System.out.println("*** let's go");
final Set<Bean<?>> gorpBeans = beanManager.getBeans(Gorp.class);
assertNotNull(gorpBeans);
assertEquals(1, gorpBeans.size());
@SuppressWarnings("unchecked")
final Bean<Gorp> gorpBean = (Bean<Gorp>)gorpBeans.iterator().next();
assertNotNull(gorpBean);
final ReferenceCountedContext context = ReferenceCountedContext.getInstanceFrom(beanManager);
assertNotNull(context);
assertTrue(context.isActive());
// Gorp is @ReferenceCounted. It is proxied. Note that we do
// not dereference the gorpAsParameter proxy yet, so no
// underlying instance is created.
assertEquals(0, context.getReferenceCount(gorpBean));
Gorp gorp = null;
try {
@@ -86,14 +100,19 @@ final class TestReferenceCountedContext {
// this.bean is @RequestScoped. It houses an instance of
// Gorp.
assertNotNull(this.bean);
assertEquals(0, context.getReferenceCount(gorpBean));
// Here, the gorp acquired is a client proxy. No
// contextual instance has been created yet.
gorp = this.bean.getGorp();
assertNotNull(gorp);
assertEquals(0, context.getReferenceCount(gorpBean));
// This should cause the underlying instance to be
// created. It will be the first Gorp instance created
// anywhere.
// This will cause the underlying instance to be created.
// It will be the first Gorp instance created anywhere.
// The reference count is 1.
assertEquals(1, gorp.getId());
assertEquals(1, context.getReferenceCount(gorpBean));
} finally {
@@ -103,11 +122,13 @@ final class TestReferenceCountedContext {
// reference count of 0. This Gorp reference is therefore
// destroyed.
controller.deactivate();
assertEquals(0, context.getReferenceCount(gorpBean));
}
// Now we kick the proxy. This will cause another instance to
// be created. The reference count will bump to 1.
assertEquals(2, gorpAsParameter.getId());
assertEquals(1, context.getReferenceCount(gorpBean));
// Next we refer to the Gorp instance that was referred to by
// the request-scoped HousingBean. If Gorp had been
@@ -117,6 +138,7 @@ final class TestReferenceCountedContext {
// reference count to jump, THIS Gorp instance will now be the
// same one as gorpAsParameter.
assertEquals(2, gorp.getId());
assertEquals(2, context.getReferenceCount(gorpBean));
}

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
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.
-->
<FindBugsFilter
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://github.com/spotbugs/filter/3.1.0"
xsi:schemaLocation="https://github.com/spotbugs/filter/3.1.0
https://raw.githubusercontent.com/spotbugs/spotbugs/3.1.0/spotbugs/etc/findbugsfilter.xsd">
<Match>
<Class name="io.helidon.integrations.cdi.jpa.DelegatingQuery"/>
<Bug pattern="RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT"/>
</Match>
<Match>
<Class name="io.helidon.integrations.cdi.jpa.DelegatingStoredProcedureQuery"/>
<Bug pattern="RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT"/>
</Match>
<Match>
<Class name="io.helidon.integrations.cdi.jpa.DelegatingTypedQuery"/>
<Bug pattern="RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT"/>
</Match>
</FindBugsFilter>

View File

@@ -31,6 +31,7 @@
<properties>
<doclint>-syntax</doclint>
<spotbugs.exclude>etc/spotbugs/exclude.xml</spotbugs.exclude>
</properties>
<dependencies>

View File

@@ -21,12 +21,37 @@ import java.util.Objects;
import java.util.Set;
import javax.enterprise.inject.Instance;
import javax.enterprise.inject.Vetoed;
import javax.persistence.EntityManager;
/**
* A {@link DelegatingEntityManager} created in certain very specific
* JPA-mandated transaction-related scenarios.
*
* <p>Instances of this class are never directly seen by the end user.
* Specifically, instances of this class are themselves returned by a
* {@link DelegatingEntityManager} implementation's {@link
* DelegatingEntityManager#acquireDelegate()} method and only under
* appropriate circumstances.</p>
*
* <p>This class is added as a synthetic bean by the {@link
* JpaExtension} class.</p>
*
* <h2>Implementation Notes</h2>
*
* <p>Because instances of this class are typically placed in <a
* href="https://jakarta.ee/specifications/cdi/2.0/cdi-spec-2.0.html#normal_scope"
* target="_parent"><em>normal scopes</em></a>, this class is not
* declared {@code final}, but must be treated as if it were.</p>
*
* <h2>Thread Safety</h2>
*
* <p>As with all {@link EntityManager} implementations, instances of
* this class are not safe for concurrent use by multiple threads.</p>
*
* @see JpaExtension
*/
@Vetoed
class CdiTransactionScopedEntityManager extends DelegatingEntityManager {
@@ -39,6 +64,8 @@ class CdiTransactionScopedEntityManager extends DelegatingEntityManager {
private final Set<? extends Annotation> suppliedQualifiers;
private final TransactionSupport transactionSupport;
private EntityManager delegate;
private boolean closeDelegate;
@@ -66,6 +93,7 @@ class CdiTransactionScopedEntityManager extends DelegatingEntityManager {
super();
this.closeDelegate = true;
this.instance = null;
this.transactionSupport = null;
this.suppliedQualifiers = Collections.emptySet();
}
@@ -85,6 +113,7 @@ class CdiTransactionScopedEntityManager extends DelegatingEntityManager {
final Set<? extends Annotation> suppliedQualifiers) {
super();
this.instance = Objects.requireNonNull(instance);
this.transactionSupport = instance.select(TransactionSupport.class).get();
this.suppliedQualifiers = Objects.requireNonNull(suppliedQualifiers);
}
@@ -94,6 +123,37 @@ class CdiTransactionScopedEntityManager extends DelegatingEntityManager {
*/
/**
* Disposes of this {@link CdiTransactionScopedEntityManager} by
* calling the {@link #close()} method.
*
* <p>If the {@link javax.transaction.TransactionScoped} scope is
* behaving the way it should, then this method will be invoked
* only when the underlying transaction has finished committing or
* rolling back, and in no other situations. It follows that the
* current transaction status must be either {@link
* TransactionSupport#STATUS_COMMITTED} or {@link
* TransactionSupport#STATUS_ROLLEDBACK}.</p>
*
* <p>This method must not be overridden. It is not {@code final}
* only due to CDI restrictions.</p>
*
* <h2>Thread Safety</h2>
*
* <p>This method may be (and often is) called by a thread that is
* not the CDI container thread, since transactions may roll back
* on such a thread.</p>
*
* @param ignoredInstance the {@link Instance} supplied by the CDI
* bean configuration machinery when the transaction scope is
* going away; ignored by this method; may be {@code null}
*
* @see #close()
*/
void dispose(final Instance<Object> ignoredInstance) {
this.close();
}
@Override
protected EntityManager acquireDelegate() {
if (this.delegate == null) {

View File

@@ -0,0 +1,52 @@
/*
* Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
*
* 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.
*/
package io.helidon.integrations.cdi.jpa;
import java.util.List;
import java.util.Objects;
import javax.persistence.EntityManager;
import javax.persistence.Query;
final class ClearingQuery extends DelegatingQuery {
private final EntityManager entityManager;
ClearingQuery(final EntityManager entityManager, final Query delegate) {
super(delegate);
this.entityManager = Objects.requireNonNull(entityManager);
}
@Override
@SuppressWarnings("rawtypes")
public List getResultList() {
try {
return super.getResultList();
} finally {
this.entityManager.clear();
}
}
@Override
public Object getSingleResult() {
try {
return super.getSingleResult();
} finally {
this.entityManager.clear();
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
*
* 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.
*/
package io.helidon.integrations.cdi.jpa;
import java.util.List;
import java.util.Objects;
import javax.persistence.EntityManager;
import javax.persistence.StoredProcedureQuery;
final class ClearingStoredProcedureQuery extends DelegatingStoredProcedureQuery {
private final EntityManager entityManager;
ClearingStoredProcedureQuery(final EntityManager entityManager, final StoredProcedureQuery delegate) {
super(delegate);
this.entityManager = Objects.requireNonNull(entityManager);
}
@Override
@SuppressWarnings("rawtypes")
public List getResultList() {
try {
return super.getResultList();
} finally {
this.entityManager.clear();
}
}
@Override
public Object getSingleResult() {
try {
return super.getSingleResult();
} finally {
this.entityManager.clear();
}
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
*
* 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.
*/
package io.helidon.integrations.cdi.jpa;
import java.util.List;
import java.util.Objects;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
final class ClearingTypedQuery<X> extends DelegatingTypedQuery<X> {
private final EntityManager entityManager;
ClearingTypedQuery(final EntityManager entityManager, final TypedQuery<X> delegate) {
super(delegate);
this.entityManager = Objects.requireNonNull(entityManager);
}
@Override
public List<X> getResultList() {
try {
return super.getResultList();
} finally {
this.entityManager.clear();
}
}
@Override
public X getSingleResult() {
try {
return super.getSingleResult();
} finally {
this.entityManager.clear();
}
}
}

View File

@@ -36,8 +36,13 @@ import javax.persistence.metamodel.Metamodel;
/**
* A partial {@link EntityManager} implementation that forwards all
* calls to an underlying {@link EntityManager}.
*
* <h2>Thread Safety</h2>
*
* <p>As with all {@link EntityManager} implementations, instances of
* this class are not safe for concurrent use by multiple threads.</p>
*/
abstract class DelegatingEntityManager implements EntityManager {
abstract class DelegatingEntityManager implements EntityManager, AutoCloseable {
/*
@@ -49,6 +54,8 @@ abstract class DelegatingEntityManager implements EntityManager {
* The {@link EntityManager} to which all operations will be
* forwarded if it is non-{@code null}.
*
* <p>This field may be {@code null}.</p>
*
* @see #DelegatingEntityManager(EntityManager)
*
* @see #delegate()
@@ -64,9 +71,13 @@ abstract class DelegatingEntityManager implements EntityManager {
/**
* Creates a new {@link DelegatingEntityManager}.
* Creates a new {@link DelegatingEntityManager} that will
* indirectly invoke the {@link #acquireDelegate()} method as part
* of each method invocation to acquire its delegate.
*
* @see #delegate()
*
* @see #acquireDelegate()
*/
DelegatingEntityManager() {
this(null);
@@ -135,7 +146,8 @@ abstract class DelegatingEntityManager implements EntityManager {
*
* <p>This method is called by the {@link #delegate()} method and
* potentially on every method invocation of instances of this
* class so implementations of it must be fast.</p>
* class so implementations of it should be as fast as
* possible.</p>
*
* <p>Implementations of this method must not call the {@link
* #delegate()} method.</p>

View File

@@ -0,0 +1,234 @@
/*
* Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
*
* 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.
*/
package io.helidon.integrations.cdi.jpa;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.Parameter;
import javax.persistence.Query;
import javax.persistence.TemporalType;
abstract class DelegatingQuery implements Query {
private final Query delegate;
DelegatingQuery(final Query delegate) {
super();
this.delegate = Objects.requireNonNull(delegate);
}
@Override
@SuppressWarnings("rawtypes")
public List getResultList() {
return this.delegate.getResultList();
}
@Override
public Object getSingleResult() {
return this.delegate.getSingleResult();
}
@Override
public int getMaxResults() {
return this.delegate.getMaxResults();
}
@Override
public Query setMaxResults(final int maxResults) {
this.delegate.setMaxResults(maxResults);
return this;
}
@Override
public int getFirstResult() {
return this.delegate.getFirstResult();
}
@Override
public Query setFirstResult(final int firstResult) {
this.delegate.setFirstResult(firstResult);
return this;
}
@Override
public Map<String, Object> getHints() {
return this.delegate.getHints();
}
@Override
public Query setHint(final String hintName, final Object value) {
this.delegate.setHint(hintName, value);
return this;
}
@Override
public Parameter<?> getParameter(final int position) {
return this.delegate.getParameter(position);
}
@Override
public <T> Parameter<T> getParameter(final int position, final Class<T> type) {
return this.delegate.getParameter(position, type);
}
@Override
public Parameter<?> getParameter(final String name) {
return this.delegate.getParameter(name);
}
@Override
public <T> Parameter<T> getParameter(final String name, final Class<T> type) {
return this.delegate.getParameter(name, type);
}
@Override
public Set<Parameter<?>> getParameters() {
return this.delegate.getParameters();
}
@Override
public <T> T getParameterValue(final Parameter<T> parameter) {
return this.delegate.getParameterValue(parameter);
}
@Override
public Object getParameterValue(final int position) {
return this.delegate.getParameterValue(position);
}
@Override
public Object getParameterValue(final String name) {
return this.delegate.getParameterValue(name);
}
@Override
public boolean isBound(final Parameter<?> parameter) {
return this.delegate.isBound(parameter);
}
@Override
public <T> Query setParameter(final Parameter<T> parameter, final T value) {
this.delegate.setParameter(parameter, value);
return this;
}
@Override
public Query setParameter(final Parameter<Calendar> parameter, final Calendar value, final TemporalType temporalType) {
this.delegate.setParameter(parameter, value, temporalType);
return this;
}
@Override
public Query setParameter(final Parameter<Date> parameter, final Date value, final TemporalType temporalType) {
this.delegate.setParameter(parameter, value, temporalType);
return this;
}
@Override
public Query setParameter(final int position, final Calendar value, final TemporalType temporalType) {
this.delegate.setParameter(position, value, temporalType);
return this;
}
@Override
public Query setParameter(final String name, final Calendar value, final TemporalType temporalType) {
this.delegate.setParameter(name, value, temporalType);
return this;
}
@Override
public Query setParameter(final int position, final Date value, final TemporalType temporalType) {
this.delegate.setParameter(position, value, temporalType);
return this;
}
@Override
public Query setParameter(final String name, final Date value, final TemporalType temporalType) {
this.delegate.setParameter(name, value, temporalType);
return this;
}
@Override
public Query setParameter(final int position, final Object value) {
this.delegate.setParameter(position, value);
return this;
}
@Override
public Query setParameter(final String name, final Object value) {
this.delegate.setParameter(name, value);
return this;
}
@Override
public FlushModeType getFlushMode() {
return this.delegate.getFlushMode();
}
@Override
public Query setFlushMode(final FlushModeType flushMode) {
this.delegate.setFlushMode(flushMode);
return this;
}
@Override
public LockModeType getLockMode() {
return this.delegate.getLockMode();
}
@Override
public Query setLockMode(final LockModeType lockMode) {
this.delegate.setLockMode(lockMode);
return this;
}
@Override
public int executeUpdate() {
return this.delegate.executeUpdate();
}
@Override
public <T> T unwrap(final Class<T> cls) {
return this.delegate.unwrap(cls);
}
}

View File

@@ -0,0 +1,161 @@
/*
* Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
*
* 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.
*/
package io.helidon.integrations.cdi.jpa;
import java.util.Calendar;
import java.util.Date;
import javax.persistence.FlushModeType;
import javax.persistence.Parameter;
import javax.persistence.ParameterMode;
import javax.persistence.StoredProcedureQuery;
import javax.persistence.TemporalType;
abstract class DelegatingStoredProcedureQuery extends DelegatingQuery implements StoredProcedureQuery {
private final StoredProcedureQuery delegate;
DelegatingStoredProcedureQuery(final StoredProcedureQuery delegate) {
super(delegate);
this.delegate = delegate;
}
@Override
public Object getOutputParameterValue(final int position) {
return this.delegate.getOutputParameterValue(position);
}
@Override
public Object getOutputParameterValue(final String parameterName) {
return this.delegate.getOutputParameterValue(parameterName);
}
@Override
public boolean hasMoreResults() {
return this.delegate.hasMoreResults();
}
@Override
public int getUpdateCount() {
return this.delegate.getUpdateCount();
}
@Override
@SuppressWarnings("rawtypes")
public DelegatingStoredProcedureQuery registerStoredProcedureParameter(final int position,
final Class type,
final ParameterMode mode) {
this.delegate.registerStoredProcedureParameter(position, type, mode);
return this;
}
@Override
@SuppressWarnings("rawtypes")
public DelegatingStoredProcedureQuery registerStoredProcedureParameter(final String parameterName,
final Class type,
final ParameterMode mode) {
this.delegate.registerStoredProcedureParameter(parameterName, type, mode);
return this;
}
@Override
public DelegatingStoredProcedureQuery setFlushMode(FlushModeType flushMode) {
return (DelegatingStoredProcedureQuery) super.setFlushMode(flushMode);
}
@Override
public DelegatingStoredProcedureQuery setHint(final String hintName,
final Object value) {
return (DelegatingStoredProcedureQuery) super.setHint(hintName, value);
}
@Override
public <T> DelegatingStoredProcedureQuery setParameter(final Parameter<T> parameter,
final T value) {
return (DelegatingStoredProcedureQuery) super.setParameter(parameter, value);
}
@Override
public DelegatingStoredProcedureQuery setParameter(final Parameter<Calendar> parameter,
final Calendar value,
final TemporalType temporalType) {
return (DelegatingStoredProcedureQuery) super.setParameter(parameter, value, temporalType);
}
@Override
public DelegatingStoredProcedureQuery setParameter(final Parameter<Date> parameter,
final Date value,
final TemporalType temporalType) {
return (DelegatingStoredProcedureQuery) super.setParameter(parameter, value, temporalType);
}
@Override
public DelegatingStoredProcedureQuery setParameter(final int position,
final Object value) {
return (DelegatingStoredProcedureQuery) super.setParameter(position, value);
}
@Override
public DelegatingStoredProcedureQuery setParameter(final int position,
final Calendar value,
final TemporalType temporalType) {
return (DelegatingStoredProcedureQuery) super.setParameter(position, value, temporalType);
}
@Override
public DelegatingStoredProcedureQuery setParameter(final int position,
final Date value,
final TemporalType temporalType) {
return (DelegatingStoredProcedureQuery) super.setParameter(position, value, temporalType);
}
@Override
public DelegatingStoredProcedureQuery setParameter(final String name,
final Object value) {
return (DelegatingStoredProcedureQuery) super.setParameter(name, value);
}
@Override
public DelegatingStoredProcedureQuery setParameter(final String name,
final Calendar value,
final TemporalType temporalType) {
return (DelegatingStoredProcedureQuery) super.setParameter(name, value, temporalType);
}
@Override
public DelegatingStoredProcedureQuery setParameter(final String name,
final Date value,
final TemporalType temporalType) {
return (DelegatingStoredProcedureQuery) super.setParameter(name, value, temporalType);
}
@Override
public boolean execute() {
return this.delegate.execute();
}
}

View File

@@ -0,0 +1,242 @@
/*
* Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
*
* 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.
*/
package io.helidon.integrations.cdi.jpa;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.Parameter;
import javax.persistence.TemporalType;
import javax.persistence.TypedQuery;
abstract class DelegatingTypedQuery<X> implements TypedQuery<X> {
private final TypedQuery<X> delegate;
DelegatingTypedQuery(final TypedQuery<X> delegate) {
super();
this.delegate = Objects.requireNonNull(delegate);
}
@Override
public List<X> getResultList() {
return this.delegate.getResultList();
}
@Override
public X getSingleResult() {
return this.delegate.getSingleResult();
}
@Override
public int getMaxResults() {
return this.delegate.getMaxResults();
}
@Override
public TypedQuery<X> setMaxResults(final int maxResults) {
this.delegate.setMaxResults(maxResults);
return this;
}
@Override
public int getFirstResult() {
return this.delegate.getFirstResult();
}
@Override
public TypedQuery<X> setFirstResult(final int startPosition) {
this.delegate.setFirstResult(startPosition);
return this;
}
@Override
public Map<String, Object> getHints() {
return this.delegate.getHints();
}
@Override
public TypedQuery<X> setHint(final String hintName, final Object value) {
this.delegate.setHint(hintName, value);
return this;
}
@Override
public Set<Parameter<?>> getParameters() {
return this.delegate.getParameters();
}
@Override
public Parameter<?> getParameter(final String name) {
return this.delegate.getParameter(name);
}
@Override
public <T> Parameter<T> getParameter(final String name, final Class<T> type) {
return this.delegate.getParameter(name, type);
}
@Override
public Parameter<?> getParameter(final int position) {
return this.delegate.getParameter(position);
}
@Override
public <T> Parameter<T> getParameter(final int position, final Class<T> type) {
return this.delegate.getParameter(position, type);
}
@Override
public <T> T getParameterValue(final Parameter<T> parameter) {
return this.delegate.getParameterValue(parameter);
}
@Override
public Object getParameterValue(final String name) {
return this.delegate.getParameterValue(name);
}
@Override
public Object getParameterValue(final int position) {
return this.delegate.getParameterValue(position);
}
@Override
public boolean isBound(final Parameter<?> parameter) {
return this.delegate.isBound(parameter);
}
@Override
public <T> TypedQuery<X> setParameter(final Parameter<T> parameter,
final T value) {
this.delegate.setParameter(parameter, value);
return this;
}
@Override
public TypedQuery<X> setParameter(final Parameter<Calendar> parameter,
final Calendar value,
final TemporalType temporalType) {
this.delegate.setParameter(parameter, value, temporalType);
return this;
}
@Override
public TypedQuery<X> setParameter(final Parameter<Date> parameter,
final Date value,
final TemporalType temporalType) {
this.delegate.setParameter(parameter, value, temporalType);
return this;
}
@Override
public TypedQuery<X> setParameter(final int position,
final Object value) {
this.delegate.setParameter(position, value);
return this;
}
@Override
public TypedQuery<X> setParameter(final int position,
final Calendar value,
final TemporalType temporalType) {
this.delegate.setParameter(position, value, temporalType);
return this;
}
@Override
public TypedQuery<X> setParameter(final int position,
final Date value,
final TemporalType temporalType) {
this.delegate.setParameter(position, value, temporalType);
return this;
}
@Override
public TypedQuery<X> setParameter(final String name,
final Object value) {
this.delegate.setParameter(name, value);
return this;
}
@Override
public TypedQuery<X> setParameter(final String name,
final Calendar value,
final TemporalType temporalType) {
this.delegate.setParameter(name, value, temporalType);
return this;
}
@Override
public TypedQuery<X> setParameter(final String name,
final Date value,
final TemporalType temporalType) {
this.delegate.setParameter(name, value, temporalType);
return this;
}
@Override
public FlushModeType getFlushMode() {
return this.delegate.getFlushMode();
}
@Override
public TypedQuery<X> setFlushMode(final FlushModeType flushMode) {
this.delegate.setFlushMode(flushMode);
return this;
}
@Override
public LockModeType getLockMode() {
return this.delegate.getLockMode();
}
@Override
public TypedQuery<X> setLockMode(final LockModeType lockMode) {
this.delegate.setLockMode(lockMode);
return this;
}
@Override
public int executeUpdate() {
return this.delegate.executeUpdate();
}
@Override
public <T> T unwrap(final Class<T> cls) {
return this.delegate.unwrap(cls);
}
}

View File

@@ -20,7 +20,6 @@ import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import javax.enterprise.context.Dependent;
import javax.enterprise.context.spi.Context;
import javax.enterprise.inject.CreationException;
import javax.enterprise.inject.Instance;
@@ -68,8 +67,8 @@ final class ExtendedEntityManager extends DelegatingEntityManager {
this.suppliedQualifiers = Objects.requireNonNull(suppliedQualifiers);
this.beanManager = Objects.requireNonNull(beanManager);
this.transactionSupport = Objects.requireNonNull(instance.select(TransactionSupport.class).get());
if (!transactionSupport.isActive()) {
throw new IllegalArgumentException("!transactionSupport.isActive()");
if (!transactionSupport.isEnabled()) {
throw new IllegalArgumentException("!transactionSupport.isEnabled()");
}
this.isSynchronized = !suppliedQualifiers.contains(Unsynchronized.Literal.INSTANCE);
assert
@@ -252,7 +251,7 @@ final class ExtendedEntityManager extends DelegatingEntityManager {
final Bean<?> cdiTransactionScopedEntityManagerBean =
(Bean<CdiTransactionScopedEntityManager>) this.beanManager.resolve(cdiTransactionScopedEntityManagerBeans);
assert cdiTransactionScopedEntityManagerBean != null;
assert !Dependent.class.equals(cdiTransactionScopedEntityManagerBean.getScope());
assert context.getScope().equals(cdiTransactionScopedEntityManagerBean.getScope());
// Using that bean, check the Context to see if there's
// already a container-managed EntityManager enrolled in

View File

@@ -1024,7 +1024,8 @@ public class JpaExtension implements Extension {
* #addJpaTransactionScopedEntityManagerBeans(AfterBeanDiscovery,
* Set)
*/
private void addContainerManagedJpaBeans(final AfterBeanDiscovery event, final BeanManager beanManager) {
private void addContainerManagedJpaBeans(final AfterBeanDiscovery event, final BeanManager beanManager)
throws ReflectiveOperationException {
final String cn = JpaExtension.class.getName();
final String mn = "addContainerManagedJpaBeans";
if (LOGGER.isLoggable(Level.FINER)) {
@@ -1110,7 +1111,8 @@ public class JpaExtension implements Extension {
}
private void addCdiTransactionScopedEntityManagerBeans(final AfterBeanDiscovery event,
final Set<Annotation> suppliedQualifiers) {
final Set<Annotation> suppliedQualifiers)
throws ReflectiveOperationException {
final String cn = JpaExtension.class.getName();
final String mn = "addCdiTransactionScopedEntityManagerBeans";
if (LOGGER.isLoggable(Level.FINER)) {
@@ -1127,15 +1129,16 @@ public class JpaExtension implements Extension {
// @Inject
// @ContainerManaged
// @CdiTransactionScoped
// @Synchronized
// @Synchronized // <-- NOTE
// @Named("test")
// private final EntityManager cdiTransactionScopedEm;
//
// ...AND:
//
// @Inject
// @ContainerManaged
// @CdiTransactionScoped
// @Unynchronized
// @Unsynchronized // <-- NOTE
// @Named("test")
// private final EntityManager cdiTransactionScopedEm;
final Set<Annotation> qualifiers = new HashSet<>(suppliedQualifiers);
@@ -1171,7 +1174,7 @@ public class JpaExtension implements Extension {
qualifiers.add(Synchronized.Literal.INSTANCE);
final Set<Annotation> synchronizedQualifiers = new HashSet<>(qualifiers);
event.addBean()
event.<CdiTransactionScopedEntityManager>addBean()
.addTransitiveTypeClosure(CdiTransactionScopedEntityManager.class)
.scope(scope)
.addQualifiers(synchronizedQualifiers)
@@ -1179,12 +1182,12 @@ public class JpaExtension implements Extension {
// On its own line to ease debugging.
return new CdiTransactionScopedEntityManager(instance, synchronizedQualifiers);
})
.disposeWith((em, instance) -> em.close());
.disposeWith(CdiTransactionScopedEntityManager::dispose);
qualifiers.remove(Synchronized.Literal.INSTANCE);
qualifiers.add(Unsynchronized.Literal.INSTANCE);
final Set<Annotation> unsynchronizedQualifiers = new HashSet<>(qualifiers);
event.addBean()
event.<CdiTransactionScopedEntityManager>addBean()
.addTransitiveTypeClosure(CdiTransactionScopedEntityManager.class)
.scope(scope)
.addQualifiers(unsynchronizedQualifiers)
@@ -1192,7 +1195,7 @@ public class JpaExtension implements Extension {
// On its own line to ease debugging.
return new CdiTransactionScopedEntityManager(instance, unsynchronizedQualifiers);
})
.disposeWith((em, instance) -> em.close());
.disposeWith(CdiTransactionScopedEntityManager::dispose);
}
if (LOGGER.isLoggable(Level.FINER)) {
@@ -1237,8 +1240,9 @@ public class JpaExtension implements Extension {
.produceWith(instance -> {
// On its own line to ease debugging.
return new JpaTransactionScopedEntityManager(instance, suppliedQualifiers);
});
// (deliberately no disposeWith())
})
.disposeWith(JpaTransactionScopedEntityManager::dispose);
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.exiting(cn, mn);
@@ -1275,7 +1279,15 @@ public class JpaExtension implements Extension {
// On its own line to ease debugging.
return new NonTransactionalEntityManager(instance, suppliedQualifiers);
})
.disposeWith((em, instance) -> em.close());
// Revisit: ReferenceCountedContext does not
// automatically pick up synthetic beans like this
// one. So we have to tell it somehow to "work on"
// this bean. Right now this bean is in what amounts
// to a thread-specific singleton scope. As it
// happens, this might actually be OK.
.disposeWith((em, instance) -> {
em.close();
});
}
if (LOGGER.isLoggable(Level.FINER)) {

View File

@@ -17,24 +17,48 @@ package io.helidon.integrations.cdi.jpa;
import java.lang.annotation.Annotation;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.enterprise.context.ContextNotActiveException;
import javax.enterprise.context.spi.Context;
import javax.enterprise.inject.Any;
import javax.enterprise.inject.Default;
import javax.enterprise.inject.Instance;
import javax.enterprise.inject.Vetoed;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.inject.Provider;
import javax.persistence.EntityGraph;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import javax.persistence.StoredProcedureQuery;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaDelete;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.CriteriaUpdate;
import javax.persistence.metamodel.Metamodel;
import io.helidon.integrations.cdi.referencecountedcontext.ReferenceCountedContext;
/**
* A {@link DelegatingEntityManager} that adheres to the JPA
* specification's rules for transaction-scoped {@link
* EntityManager}s.
*
* <h2>Thread Safety</h2>
*
* <p>As with all {@link EntityManager} implementations, instances of
* this class are not safe for concurrent use by multiple threads.</p>
*/
@Vetoed
final class JpaTransactionScopedEntityManager extends DelegatingEntityManager {
@@ -42,19 +66,22 @@ final class JpaTransactionScopedEntityManager extends DelegatingEntityManager {
* Instance fields.
*/
private final BeanManager beanManager;
private final Instance<Object> instance;
private final TransactionSupport transactionSupport;
private final Annotation[] cdiTransactionScopedEntityManagerSelectionQualifiersArray;
private final Bean<NonTransactionalEntityManager> nonTransactionalEntityManagerBean;
private final Provider<EntityManager> nonTransactionalEntityManagerProvider;
private final NonTransactionalEntityManager nonTransactionalEntityManager;
private final boolean isUnsynchronized;
private final int startingReferenceCount;
private final Bean<?> cdiTransactionScopedEntityManagerOppositeSynchronizationBean;
private final Bean<?> oppositeSynchronizationBean;
private final CdiTransactionScopedEntityManager cdiTransactionScopedEntityManager;
/*
@@ -76,14 +103,14 @@ final class JpaTransactionScopedEntityManager extends DelegatingEntityManager {
super();
Objects.requireNonNull(instance);
Objects.requireNonNull(suppliedQualifiers);
this.transactionSupport = Objects.requireNonNull(instance.select(TransactionSupport.class).get());
if (!transactionSupport.isActive()) {
throw new IllegalArgumentException("!instance.select(TransacitonSupport.class).get().isActive()");
this.transactionSupport = instance.select(TransactionSupport.class).get();
if (!transactionSupport.isEnabled()) {
throw new IllegalArgumentException("!instance.select(TransactionSupport.class).get().isEnabled()");
}
this.beanManager = instance.select(BeanManager.class).get();
this.instance = instance;
this.isUnsynchronized = suppliedQualifiers.contains(Unsynchronized.Literal.INSTANCE);
// This large block is devoted to honoring the slightly odd
// provision in the JPA specification that there be only one
@@ -93,10 +120,10 @@ final class JpaTransactionScopedEntityManager extends DelegatingEntityManager {
// SynchronizationType.UNSYNCHRONIZED. Which one it is is
// determined by who "gets there first". That is, by spec, if
// someone requests a synchronized transaction scoped entity
// manager, then the entity manager that is established for
// the transaction will happen to be synchronized. If someone
// manager, then the EntityManager that is established for the
// transaction will happen to be synchronized. If someone
// else somehow wants to get their hands on an unsynchronized
// transaction scoped entity manager, we have to detect this
// transaction scoped EntityManager, we have to detect this
// mixed synchronization case and throw an error.
//
// The mixed synchronization detection has to happen on each
@@ -117,44 +144,73 @@ final class JpaTransactionScopedEntityManager extends DelegatingEntityManager {
selectionQualifiers.remove(NonTransactional.Literal.INSTANCE);
selectionQualifiers.add(CdiTransactionScoped.Literal.INSTANCE);
selectionQualifiers.add(ContainerManaged.Literal.INSTANCE);
if (this.isUnsynchronized) {
final Annotation[] cdiTransactionScopedEntityManagerSelectionQualifiersArray;
if (suppliedQualifiers.contains(Unsynchronized.Literal.INSTANCE)) {
selectionQualifiers.remove(Synchronized.Literal.INSTANCE);
selectionQualifiers.add(Unsynchronized.Literal.INSTANCE);
this.cdiTransactionScopedEntityManagerSelectionQualifiersArray =
cdiTransactionScopedEntityManagerSelectionQualifiersArray =
selectionQualifiers.toArray(new Annotation[selectionQualifiers.size()]);
selectionQualifiers.remove(Unsynchronized.Literal.INSTANCE);
selectionQualifiers.add(Synchronized.Literal.INSTANCE);
} else {
selectionQualifiers.remove(Unsynchronized.Literal.INSTANCE);
selectionQualifiers.add(Synchronized.Literal.INSTANCE);
this.cdiTransactionScopedEntityManagerSelectionQualifiersArray =
cdiTransactionScopedEntityManagerSelectionQualifiersArray =
selectionQualifiers.toArray(new Annotation[selectionQualifiers.size()]);
selectionQualifiers.remove(Synchronized.Literal.INSTANCE);
selectionQualifiers.add(Unsynchronized.Literal.INSTANCE);
}
final Set<Bean<?>> beans =
Set<Bean<?>> beans =
this.beanManager.getBeans(EntityManager.class,
selectionQualifiers.toArray(new Annotation[selectionQualifiers.size()]));
assert beans != null;
assert beans.size() == 1 : "beans.size() != 1: " + beans;
this.cdiTransactionScopedEntityManagerOppositeSynchronizationBean = this.beanManager.resolve(beans);
assert this.cdiTransactionScopedEntityManagerOppositeSynchronizationBean != null;
this.oppositeSynchronizationBean = Objects.requireNonNull(this.beanManager.resolve(beans));
// This is a proxy whose scope will be
// javax.transaction.TransactionScoped, and therefore will be
// lazily "inflated". In other words, the acquisition of this
// reference here does not cause a contextual instance to be
// created. Invoking any method on it will cause the
// contextual instance to be created at that point, including
// toString(), so debug with care.
this.cdiTransactionScopedEntityManager =
instance.select(CdiTransactionScopedEntityManager.class,
cdiTransactionScopedEntityManagerSelectionQualifiersArray).get();
assert this.cdiTransactionScopedEntityManager.getClass().isSynthetic();
selectionQualifiers.remove(CdiTransactionScoped.Literal.INSTANCE);
selectionQualifiers.remove(ContainerManaged.Literal.INSTANCE);
selectionQualifiers.remove(Synchronized.Literal.INSTANCE);
selectionQualifiers.remove(Unsynchronized.Literal.INSTANCE);
selectionQualifiers.add(NonTransactional.Literal.INSTANCE);
this.nonTransactionalEntityManagerProvider =
Objects.requireNonNull(instance.select(EntityManager.class,
selectionQualifiers.toArray(new Annotation[selectionQualifiers.size()])));
assert this.nonTransactionalEntityManagerProvider != null;
beans = this.beanManager.getBeans(NonTransactionalEntityManager.class,
selectionQualifiers.toArray(new Annotation[selectionQualifiers.size()]));
assert beans != null;
assert beans.size() == 1 : "beans.size() != 1: " + beans;
this.nonTransactionalEntityManagerBean = (Bean<NonTransactionalEntityManager>) this.beanManager.resolve(beans);
assert this.nonTransactionalEntityManagerBean != null;
beans = null;
final ReferenceCountedContext context = ReferenceCountedContext.getInstanceFrom(this.beanManager);
assert context != null;
assert context.isActive(); // it's always active
this.startingReferenceCount = context.getReferenceCount(this.nonTransactionalEntityManagerBean);
this.nonTransactionalEntityManager =
(NonTransactionalEntityManager) this.beanManager.getReference(this.nonTransactionalEntityManagerBean,
NonTransactionalEntityManager.class,
this.beanManager.createCreationalContext(null)); // fix
}
/**
* Acquires and returns a delegate {@link EntityManager}, adhering
* to the rules spelled out by the JPA specification around
* transaction-scoped entity managers.
* transaction-scoped {@link EntityManager}s.
*
* <p>This method never returns {@code null}.</p>
*
@@ -173,45 +229,575 @@ final class JpaTransactionScopedEntityManager extends DelegatingEntityManager {
@Override
protected EntityManager acquireDelegate() {
final EntityManager returnValue;
if (this.transactionSupport.inTransaction()) {
// If we're in a transaction, then we're obligated to see
// if there's a transaction-scoped entity manager already
// affiliated with the current transaction. If there is,
// and its synchronization type doesn't match ours, we're
// supposed to throw an exception.
final Context context =
this.beanManager.getContext(this.cdiTransactionScopedEntityManagerOppositeSynchronizationBean.getScope());
assert context != null;
assert context.isActive();
final Object contextualInstance =
context.get(this.cdiTransactionScopedEntityManagerOppositeSynchronizationBean);
if (contextualInstance != null) {
// The Context in question reported that it has
// already created (and is therefore storing) an
// instance with the "wrong" synchronization type. We
// must throw an exception.
throw new PersistenceException(Messages.format("mixedSynchronizationTypes",
this.cdiTransactionScopedEntityManagerOppositeSynchronizationBean,
contextualInstance));
final int status = this.transactionSupport.getStatus();
switch (status) {
case TransactionSupport.STATUS_ACTIVE:
// If we are or were just in a transaction, then we're
// obligated to see if there's a transaction-scoped
// EntityManager already affiliated with the current
// transaction. If there is, and its synchronization type
// doesn't match ours, we're supposed to throw an
// exception. Remember that the transaction-scoped
// context can go inactive at any point due to a rollback
// on another thread, and may already be inactive at
// *this* point. This also means the status that dropped
// us into this case statement may be stale.
final Context transactionScopedContext = this.transactionSupport.getContext();
if (transactionScopedContext == null || !transactionScopedContext.isActive()) {
returnValue = this.nonTransactionalEntityManager;
} else {
EntityManager candidateReturnValue = this.cdiTransactionScopedEntityManager;
try {
final Object existingContextualInstance = transactionScopedContext.get(this.oppositeSynchronizationBean);
if (existingContextualInstance != null) {
// The Context in question reported that it
// has already created (and is therefore
// storing) an instance with the "wrong"
// synchronization type. We must throw an
// exception.
throw new PersistenceException(Messages.format("mixedSynchronizationTypes",
this.oppositeSynchronizationBean,
existingContextualInstance));
}
} catch (final ContextNotActiveException contextNotActiveException) {
candidateReturnValue = this.nonTransactionalEntityManager;
} finally {
returnValue = candidateReturnValue;
}
}
returnValue =
this.instance.select(EntityManager.class,
this.cdiTransactionScopedEntityManagerSelectionQualifiersArray).get();
} else {
returnValue = this.nonTransactionalEntityManagerProvider.get();
break;
default:
returnValue = this.nonTransactionalEntityManager;
break;
}
assert returnValue != null;
return returnValue;
}
/**
* Throws an {@link IllegalStateException} when invoked.
*
* @return nothing
*
* @exception IllegalStateException when invoked
*
* @see EntityManager#getTransaction()
*/
@Override
public EntityTransaction getTransaction() {
throw new IllegalStateException(Messages.format("jpaTransactionScopedEntityManagerGetTransaction"));
}
/**
* Throws an {@link IllegalStateException} when invoked.
*
* @exception IllegalStateException when invoked
*/
@Override
public void close() {
// Revisit: Wildfly allows end users to close UNSYNCHRONIZED
// Wildfly allows end users to close UNSYNCHRONIZED
// container-managed EntityManagers:
// https://github.com/wildfly/wildfly/blob/7f80f0150297bbc418a38e7e23da7cf0431f7c28/jpa/subsystem/src/main/java/org/jboss/as/jpa/container/UnsynchronizedEntityManagerWrapper.java#L75-L78
// I don't know why. Glassfish does not:
// I don't know why. Glassfish does not; it's the reference
// application; we follow suit:
// https://github.com/javaee/glassfish/blob/f9e1f6361dcc7998cacccb574feef5b70bf84e23/appserver/common/container-common/src/main/java/com/sun/enterprise/container/common/impl/EntityManagerWrapper.java#L752-L761
throw new IllegalStateException();
throw new IllegalStateException(Messages.format("jpaTransactionScopedEntityManagerClose"));
}
void dispose(final Instance<Object> instance) {
final ReferenceCountedContext context = ReferenceCountedContext.getInstanceFrom(this.beanManager);
assert context != null;
assert context.isActive();
final int finalReferenceCount = context.getReferenceCount(this.nonTransactionalEntityManagerBean);
context.decrementReferenceCount(this.nonTransactionalEntityManagerBean,
finalReferenceCount - this.startingReferenceCount);
}
/*
* Overrides that translate ContextNotActiveException.
*
* Because the underlying delegate is often a
* CdiTransactionScopedEntityManager in
* javax.transaction.TransactionScoped scope, and because that
* scope's lifetime is equal to the current transaction's
* lifetime, and because a transaction can roll back at any point
* due to timeout on a background thread, it is *always* possible
* that the delegate returned by the delegate() method (and
* therefore indirectly by the acquireDelegate() method above) is
* essentially invalid: you can take delivery of it but as soon as
* you call a method on it it may turn out that the transaction
* has rolled back. Your method invocation will thus result in a
* ContextNotActiveException.
*
* In this case we must revert to a non-transactional delegate,
* i.e. one whose underlying persistence context is always empty.
* The overrides that follow perform this translation.
*/
@Override
public void persist(final Object entity) {
try {
super.persist(entity);
} catch (final ContextNotActiveException contextNotActiveException) {
this.nonTransactionalEntityManager.persist(entity);
}
}
@Override
public <T> T merge(final T entity) {
try {
return super.merge(entity);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.merge(entity);
}
}
@Override
public void remove(final Object entity) {
try {
super.remove(entity);
} catch (final ContextNotActiveException contextNotActiveException) {
this.nonTransactionalEntityManager.remove(entity);
}
}
@Override
public <T> T find(final Class<T> entityClass,
final Object primaryKey) {
try {
return super.find(entityClass, primaryKey);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.find(entityClass, primaryKey);
}
}
@Override
public <T> T find(final Class<T> entityClass,
final Object primaryKey,
final Map<String, Object> properties) {
try {
return super.find(entityClass, primaryKey, properties);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.find(entityClass, primaryKey, properties);
}
}
@Override
public <T> T find(final Class<T> entityClass,
final Object primaryKey,
final LockModeType lockMode) {
try {
return super.find(entityClass, primaryKey, lockMode);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.find(entityClass, primaryKey, lockMode);
}
}
@Override
public <T> T find(final Class<T> entityClass,
final Object primaryKey,
final LockModeType lockMode,
final Map<String, Object> properties) {
try {
return super.find(entityClass, primaryKey, lockMode, properties);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.find(entityClass, primaryKey, lockMode, properties);
}
}
@Override
public <T> T getReference(final Class<T> entityClass,
final Object primaryKey) {
try {
return super.getReference(entityClass, primaryKey);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.getReference(entityClass, primaryKey);
}
}
@Override
public void flush() {
try {
super.flush();
} catch (final ContextNotActiveException contextNotActiveException) {
this.nonTransactionalEntityManager.flush();
}
}
@Override
public void setFlushMode(final FlushModeType flushMode) {
try {
super.setFlushMode(flushMode);
} catch (final ContextNotActiveException contextNotActiveException) {
this.nonTransactionalEntityManager.setFlushMode(flushMode);
}
}
@Override
public FlushModeType getFlushMode() {
try {
return super.getFlushMode();
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.getFlushMode();
}
}
@Override
public void lock(final Object entity,
final LockModeType lockMode) {
try {
super.lock(entity, lockMode);
} catch (final ContextNotActiveException contextNotActiveException) {
this.nonTransactionalEntityManager.lock(entity, lockMode);
}
}
@Override
public void lock(final Object entity,
final LockModeType lockMode,
final Map<String, Object> properties) {
try {
super.lock(entity, lockMode, properties);
} catch (final ContextNotActiveException contextNotActiveException) {
this.nonTransactionalEntityManager.lock(entity, lockMode, properties);
}
}
@Override
public void refresh(final Object entity) {
try {
super.refresh(entity);
} catch (final ContextNotActiveException contextNotActiveException) {
this.nonTransactionalEntityManager.refresh(entity);
}
}
@Override
public void refresh(final Object entity,
final Map<String, Object> properties) {
try {
super.refresh(entity, properties);
} catch (final ContextNotActiveException contextNotActiveException) {
this.nonTransactionalEntityManager.refresh(entity, properties);
}
}
@Override
public void refresh(final Object entity,
final LockModeType lockMode) {
try {
super.refresh(entity, lockMode);
} catch (final ContextNotActiveException contextNotActiveException) {
this.nonTransactionalEntityManager.refresh(entity, lockMode);
}
}
@Override
public void refresh(final Object entity,
final LockModeType lockMode,
final Map<String, Object> properties) {
try {
super.refresh(entity, lockMode, properties);
} catch (final ContextNotActiveException contextNotActiveException) {
this.nonTransactionalEntityManager.refresh(entity, lockMode, properties);
}
}
@Override
public void clear() {
try {
super.clear();
} catch (final ContextNotActiveException contextNotActiveException) {
this.nonTransactionalEntityManager.clear();
}
}
@Override
public void detach(final Object entity) {
try {
super.detach(entity);
} catch (final ContextNotActiveException contextNotActiveException) {
this.nonTransactionalEntityManager.detach(entity);
}
}
@Override
public boolean contains(final Object entity) {
try {
return super.contains(entity);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.contains(entity);
}
}
@Override
public LockModeType getLockMode(final Object entity) {
try {
return super.getLockMode(entity);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.getLockMode(entity);
}
}
@Override
public Map<String, Object> getProperties() {
try {
return super.getProperties();
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.getProperties();
}
}
@Override
public void setProperty(final String propertyName,
final Object propertyValue) {
try {
super.setProperty(propertyName, propertyValue);
} catch (final ContextNotActiveException contextNotActiveException) {
this.nonTransactionalEntityManager.setProperty(propertyName, propertyValue);
}
}
@Override
public Query createQuery(final String jpql) {
try {
return super.createQuery(jpql);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.createQuery(jpql);
}
}
@Override
public <T> TypedQuery<T> createQuery(final CriteriaQuery<T> criteriaQuery) {
try {
return super.createQuery(criteriaQuery);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.createQuery(criteriaQuery);
}
}
@Override
@SuppressWarnings("rawtypes")
public Query createQuery(final CriteriaUpdate criteriaUpdate) {
try {
return super.createQuery(criteriaUpdate);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.createQuery(criteriaUpdate);
}
}
@Override
@SuppressWarnings("rawtypes")
public Query createQuery(final CriteriaDelete criteriaDelete) {
try {
return super.createQuery(criteriaDelete);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.createQuery(criteriaDelete);
}
}
@Override
public <T> TypedQuery<T> createQuery(final String jpql, final Class<T> resultClass) {
try {
return super.createQuery(jpql, resultClass);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.createQuery(jpql, resultClass);
}
}
@Override
public Query createNamedQuery(final String sql) {
try {
return super.createNamedQuery(sql);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.createNamedQuery(sql);
}
}
@Override
public <T> TypedQuery<T> createNamedQuery(final String sql, final Class<T> resultClass) {
try {
return super.createNamedQuery(sql, resultClass);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.createNamedQuery(sql, resultClass);
}
}
@Override
public Query createNativeQuery(final String sql) {
try {
return super.createNativeQuery(sql);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.createNativeQuery(sql);
}
}
@Override
@SuppressWarnings("rawtypes")
public Query createNativeQuery(final String sql, final Class resultClass) {
try {
return super.createNativeQuery(sql, resultClass);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.createNativeQuery(sql, resultClass);
}
}
@Override
public Query createNativeQuery(final String sql, final String resultSetMapping) {
try {
return super.createNativeQuery(sql, resultSetMapping);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.createNativeQuery(sql, resultSetMapping);
}
}
@Override
public StoredProcedureQuery createNamedStoredProcedureQuery(final String procedureName) {
try {
return super.createNamedStoredProcedureQuery(procedureName);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.createNamedStoredProcedureQuery(procedureName);
}
}
@Override
public StoredProcedureQuery createStoredProcedureQuery(final String procedureName) {
try {
return super.createStoredProcedureQuery(procedureName);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.createStoredProcedureQuery(procedureName);
}
}
@Override
@SuppressWarnings("rawtypes")
public StoredProcedureQuery createStoredProcedureQuery(final String procedureName,
final Class... resultClasses) {
try {
return super.createStoredProcedureQuery(procedureName, resultClasses);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.createStoredProcedureQuery(procedureName, resultClasses);
}
}
@Override
public StoredProcedureQuery createStoredProcedureQuery(final String procedureName,
final String... resultSetMappings) {
try {
return super.createStoredProcedureQuery(procedureName, resultSetMappings);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.createStoredProcedureQuery(procedureName, resultSetMappings);
}
}
@Override
public void joinTransaction() {
try {
super.joinTransaction();
} catch (final ContextNotActiveException contextNotActiveException) {
this.nonTransactionalEntityManager.joinTransaction();
}
}
@Override
public boolean isJoinedToTransaction() {
try {
return super.isJoinedToTransaction();
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.isJoinedToTransaction();
}
}
@Override
public <T> T unwrap(final Class<T> c) {
try {
return super.unwrap(c);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.unwrap(c);
}
}
@Override
public Object getDelegate() {
try {
return super.getDelegate();
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.getDelegate();
}
}
@Override
public boolean isOpen() {
try {
return super.isOpen();
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.isOpen();
}
}
@Override
public EntityManagerFactory getEntityManagerFactory() {
try {
return super.getEntityManagerFactory();
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.getEntityManagerFactory();
}
}
@Override
public CriteriaBuilder getCriteriaBuilder() {
try {
return super.getCriteriaBuilder();
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.getCriteriaBuilder();
}
}
@Override
public Metamodel getMetamodel() {
try {
return super.getMetamodel();
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.getMetamodel();
}
}
@Override
public <T> EntityGraph<T> createEntityGraph(final Class<T> rootType) {
try {
return super.createEntityGraph(rootType);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.createEntityGraph(rootType);
}
}
@Override
public EntityGraph<?> createEntityGraph(final String graphName) {
try {
return super.createEntityGraph(graphName);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.createEntityGraph(graphName);
}
}
@Override
public EntityGraph<?> getEntityGraph(final String graphName) {
try {
return super.getEntityGraph(graphName);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.getEntityGraph(graphName);
}
}
@Override
public <T> List<EntityGraph<? super T>> getEntityGraphs(final Class<T> entityClass) {
try {
return super.getEntityGraphs(entityClass);
} catch (final ContextNotActiveException contextNotActiveException) {
return this.nonTransactionalEntityManager.getEntityGraphs(entityClass);
}
}
}

View File

@@ -22,7 +22,8 @@ import javax.enterprise.context.ContextNotActiveException;
import javax.enterprise.context.spi.Context;
import javax.enterprise.inject.spi.BeanManager;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.transaction.SystemException;
import javax.transaction.TransactionManager;
import javax.transaction.TransactionScoped;
/**
@@ -31,9 +32,18 @@ import javax.transaction.TransactionScoped;
*
* <p>See the exclusion stanzas in {@code META-INF/beans.xml} for more
* details.</p>
*
* <h2>Thread Safety</h2>
*
* <p>Instances of this class are not safe for concurrent use by
* multiple threads.</p>
*
* @see TransactionSupport
*
* @see NoTransactionSupport
*/
@ApplicationScoped
class JtaTransactionSupport implements TransactionSupport {
final class JtaTransactionSupport implements TransactionSupport {
/*
@@ -41,17 +51,11 @@ class JtaTransactionSupport implements TransactionSupport {
*/
/**
* A {@link Provider} of {@link BeanManager} instances.
*
* <p>This field will normally not be {@code null} but if the
* {@link #JtaSupport()} constructor is used, then it will be
* {@code null} (and the {@link JtaTransactionSupport} instance so
* constructed will be non-functional.</p>
*
* @see #JtaSupport()
*/
private final Provider<BeanManager> beanManagerProvider;
private final BeanManager beanManager;
private Context transactionScopedContext;
private final TransactionManager transactionManager;
/*
@@ -59,42 +63,24 @@ class JtaTransactionSupport implements TransactionSupport {
*/
/**
* Creates a new, <strong>nonfunctional</strong> {@link
* JtaTransactionSupport}.
*
* <p>This constructor exists only to conform with section 3.15 of
* the CDI specification.</p>
*
* @deprecated This constructor exists only to conform with
* section 3.15 of the CDI specification; please use the {@link
* #JtaTransactionSupport(Provider)} constructor instead.
*
* @see
* #JtaTransactionSupport(Provider)
*
* @see <a
* href="http://docs.jboss.org/cdi/spec/1.2/cdi-spec.html#unproxyable">Section
* 3.15 of the CDI 2.0 specification</a>
*/
@Deprecated
JtaTransactionSupport() {
super();
this.beanManagerProvider = null;
}
/**
* Creates a new {@link JtaTransactionSupport}.
*
* @param beanManagerProvider a {@link Provider} of {@link
* BeanManager} instances; must not be {@code null}
* @param beanManager a {@link BeanManager}; must not be {@code
* null}
*
* @exception NullPointerException
* @param transactionManager a {@link TransactionManager}; must
* not be {@code null}
*
* @exception NullPointerException if either {@code beanManager}
* or {@code transactionManager} is {@code null}
*/
@Inject
JtaTransactionSupport(final Provider<BeanManager> beanManagerProvider) {
private JtaTransactionSupport(final BeanManager beanManager,
final TransactionManager transactionManager) {
super();
this.beanManagerProvider = Objects.requireNonNull(beanManagerProvider);
this.beanManager = Objects.requireNonNull(beanManager);
this.transactionManager = Objects.requireNonNull(transactionManager);
}
@@ -109,13 +95,13 @@ class JtaTransactionSupport implements TransactionSupport {
* @return {@code true} when invoked
*/
@Override
public boolean isActive() {
public boolean isEnabled() {
return true;
}
/**
* Returns the {@link Context} that supports JTA transactions, if
* there is one and it is active.
* there is one and it is active at the moment of invocation.
*
* <p>This method may return {@code null}.</p>
*
@@ -126,32 +112,39 @@ class JtaTransactionSupport implements TransactionSupport {
@Override
public Context getContext() {
final Context returnValue;
final BeanManager beanManager = this.beanManagerProvider.get();
if (beanManager == null) {
returnValue = null;
} else {
Context temp = null;
if (this.transactionScopedContext == null) {
try {
temp = beanManager.getContext(TransactionScoped.class);
this.transactionScopedContext = this.beanManager.getContext(TransactionScoped.class);
} catch (final ContextNotActiveException contextNotActiveException) {
temp = null;
this.transactionScopedContext = null;
} finally {
returnValue = temp;
returnValue = this.transactionScopedContext;
}
} else if (this.transactionScopedContext.isActive()) {
returnValue = this.transactionScopedContext;
} else {
returnValue = null;
}
return returnValue;
}
/**
* Returns {@code true} if the return value of {@link
* #getContext()} is non-{@code null}.
* Returns the {@linkplain TransactionManager#getStatus() current
* status} of the current transaction, if available.
*
* @return {@code true} if the return value of {@link
* #getContext()} is non-{@code null}; {@code false} otherwise
* @return the {@linkplain TransactionManager#getStatus() current
* status} of the current transaction, if available
*
* @exception IllegalStateException if there was a problem
* acquiring status
*/
@Override
public boolean inTransaction() {
return this.getContext() != null;
public int getStatus() {
try {
return this.transactionManager.getStatus();
} catch (final SystemException systemException) {
throw new IllegalStateException(systemException.getMessage(), systemException);
}
}
}

View File

@@ -27,6 +27,10 @@ import javax.enterprise.context.spi.Context;
*
* <p>See the exclusion stanzas in {@code META-INF/beans.xml} for more
* details.</p>
*
* @see TransactionSupport
*
* @see JtaTransactionSupport
*/
@ApplicationScoped
final class NoTransactionSupport implements TransactionSupport {
@@ -60,7 +64,7 @@ final class NoTransactionSupport implements TransactionSupport {
* @return {@code false} when invoked
*/
@Override
public boolean isActive() {
public boolean isEnabled() {
return false;
}
@@ -75,13 +79,15 @@ final class NoTransactionSupport implements TransactionSupport {
}
/**
* Returns {@code false} when invoked.
* Returns {@link TransactionSupport#STATUS_NO_TRANSACTION} when
* invoked.
*
* @return {@code false} when invoked
* @return {@link TransactionSupport#STATUS_NO_TRANSACTION} when
* invoked
*/
@Override
public boolean inTransaction() {
return false;
public int getStatus() {
return STATUS_NO_TRANSACTION;
}
}

View File

@@ -20,22 +20,49 @@ import java.util.Map;
import java.util.Set;
import javax.enterprise.inject.Instance;
import javax.enterprise.inject.Vetoed;
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import javax.persistence.StoredProcedureQuery;
import javax.persistence.TransactionRequiredException;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaQuery;
/**
* A {@link DelegatingEntityManager} that is definitionally not backed
* by an extended persistence context and that assumes there is no JTA
* transaction in effect.
*
* <p>This class obeys the requirements imposed upon it by sections
* 3.3.2, 3.3.3 and 7.6.2 of the JPA 2.2 specification.</p>
*
* <p>Instances of this class are never directly seen by the end user.
* Specifically, instances of this class are themselves returned by a
* {@link DelegatingEntityManager} implementation's {@link
* #acquireDelegate()} method.</p>
* #acquireDelegate()} method and only under appropriate
* circumstances.</p>
*
* <p>This class is added as a synthetic bean by the {@link
* JpaExtension} class.</p>
*
* <h2>Implementation Notes</h2>
*
* <p>Because instances of this class are typically placed in <a
* href="https://jakarta.ee/specifications/cdi/2.0/cdi-spec-2.0.html#normal_scope"
* target="_parent"><em>normal scopes</em></a>, this class is not
* declared {@code final}, but must be treated as if it were.</p>
*
* <h2>Thread Safety</h2>
*
* <p>As with all {@link EntityManager} implementations, instances of
* this class are not safe for concurrent use by multiple threads.</p>
*
* @see JpaExtension
*/
final class NonTransactionalEntityManager extends DelegatingEntityManager {
@Vetoed
class NonTransactionalEntityManager extends DelegatingEntityManager {
/*
@@ -67,19 +94,134 @@ final class NonTransactionalEntityManager extends DelegatingEntityManager {
/**
* Throws a {@link PersistenceException} when invoked, because it
* will never be invoked in the normal course of events.
* Throws a {@link PersistenceException} when invoked, because
* this method will never be invoked in the normal course of
* events.
*
* @return a non-{@code null} {@link EntityManager}, but this will
* never happen
*
* @exception PersistenceException when invoked
*
* @see #delegate()
*/
@Override
protected EntityManager acquireDelegate() {
throw new PersistenceException();
}
@Override
public <T> TypedQuery<T> createNamedQuery(final String name,
final Class<T> resultClass) {
return new ClearingTypedQuery<>(this, super.createNamedQuery(name, resultClass));
}
@Override
public <T> TypedQuery<T> createQuery(final CriteriaQuery<T> criteriaQuery) {
return new ClearingTypedQuery<>(this, super.createQuery(criteriaQuery));
}
@Override
public <T> TypedQuery<T> createQuery(final String jpql,
final Class<T> resultClass) {
return new ClearingTypedQuery<>(this, super.createQuery(jpql, resultClass));
}
@Override
public <T> T find(final Class<T> entityClass,
final Object primaryKey,
final Map<String, Object> properties) {
final T returnValue = super.find(entityClass, primaryKey, properties);
this.clear();
return returnValue;
}
@Override
public <T> T find(final Class<T> entityClass,
final Object primaryKey) {
final T returnValue = super.find(entityClass, primaryKey);
this.clear();
return returnValue;
}
@Override
public Query createNamedQuery(final String name) {
return new ClearingQuery(this, super.createNamedQuery(name));
}
@Override
@SuppressWarnings("rawtypes")
public Query createNativeQuery(final String sql,
final Class resultClass) {
return new ClearingQuery(this, super.createNativeQuery(sql, resultClass));
}
@Override
public Query createNativeQuery(final String sql,
final String resultSetMapping) {
return new ClearingQuery(this, super.createNativeQuery(sql, resultSetMapping));
}
@Override
public Query createNativeQuery(final String sql) {
return new ClearingQuery(this, super.createNativeQuery(sql));
}
@Override
public Query createQuery(final String jpql) {
return new ClearingQuery(this, super.createQuery(jpql));
}
@Override
public <T> T getReference(final Class<T> entityClass,
final Object primaryKey) {
final T returnValue = super.getReference(entityClass, primaryKey);
this.clear();
return returnValue;
}
@Override
public StoredProcedureQuery createNamedStoredProcedureQuery(final String name) {
return new ClearingStoredProcedureQuery(this, super.createNamedStoredProcedureQuery(name));
}
@Override
public StoredProcedureQuery createStoredProcedureQuery(final String procedureName) {
return new ClearingStoredProcedureQuery(this, super.createStoredProcedureQuery(procedureName));
}
@Override
@SuppressWarnings("rawtypes")
public StoredProcedureQuery createStoredProcedureQuery(final String procedureName,
final Class... resultClasses) {
return new ClearingStoredProcedureQuery(this, super.createStoredProcedureQuery(procedureName, resultClasses));
}
@Override
public StoredProcedureQuery createStoredProcedureQuery(final String procedureName,
final String... resultSetMappings) {
return new ClearingStoredProcedureQuery(this, super.createStoredProcedureQuery(procedureName, resultSetMappings));
}
/**
* Returns {@code false} when invoked.
*
* @return {@code false} in all cases
*/
public boolean isJoinedToTransaction() {
return false;
}
/**
* Throws a {@link TransactionRequiredException} when invoked.
*
* @exception TransactionRequiredException when invoked
*/
@Override
public void joinTransaction() {
throw new TransactionRequiredException();
}
/**
* Throws a {@link TransactionRequiredException} when invoked.
*
@@ -176,6 +318,78 @@ final class NonTransactionalEntityManager extends DelegatingEntityManager {
throw new TransactionRequiredException();
}
@Override
public <T> T find(final Class<T> entityClass,
final Object primaryKey,
final LockModeType lockMode) {
if (lockMode != null && !lockMode.equals(LockModeType.NONE)) {
throw new TransactionRequiredException();
}
final T returnValue = super.find(entityClass, primaryKey, lockMode);
this.clear();
return returnValue;
}
@Override
public <T> T find(final Class<T> entityClass,
final Object primaryKey,
final LockModeType lockMode,
final Map<String, Object> properties) {
if (lockMode != null && !lockMode.equals(LockModeType.NONE)) {
throw new TransactionRequiredException();
}
final T returnValue = super.find(entityClass, primaryKey, lockMode, properties);
this.clear();
return returnValue;
}
/**
* Throws a {@link TransactionRequiredException} when invoked.
*
* @param entity ignored
*
* @param lockMode ignored
*
* @exception TransactionRequiredException when invoked
*/
@Override
public void lock(final Object entity,
final LockModeType lockMode) {
throw new TransactionRequiredException();
}
/**
* Throws a {@link TransactionRequiredException} when invoked.
*
* @param entity ignored
*
* @param lockMode ignored
*
* @param properties ignored
*
* @exception TransactionRequiredException when invoked
*/
@Override
public void lock(final Object entity,
final LockModeType lockMode,
final Map<String, Object> properties) {
throw new TransactionRequiredException();
}
/**
* Throws a {@link TransactionRequiredException} when invoked.
*
* @param entity ignored
*
* @return nothing
*
* @exception TransactionRequiredException when invoked
*/
@Override
public LockModeType getLockMode(final Object entity) {
throw new TransactionRequiredException();
}
/**
* Throws a {@link TransactionRequiredException} when invoked.
*

View File

@@ -27,32 +27,113 @@ import javax.enterprise.context.spi.Context;
*/
interface TransactionSupport {
/*
* Static fields.
*/
/**
* Equal in value and meaning to {@link
* javax.transaction.Status#STATUS_ACTIVE}.
*/
int STATUS_ACTIVE = 0;
/**
* Equal in value and meaning to {@link
* javax.transaction.Status#STATUS_MARKED_ROLLBACK}.
*/
int STATUS_MARKED_ROLLBACK = 1;
/**
* Equal in value and meaning to {@link
* javax.transaction.Status#STATUS_PREPARED}.
*/
int STATUS_PREPARED = 2;
/**
* Equal in value and meaning to {@link
* javax.transaction.Status#STATUS_COMMITTED}.
*/
int STATUS_COMMITTED = 3;
/**
* Equal in value and meaning to {@link
* javax.transaction.Status#STATUS_ROLLEDBACK}.
*/
int STATUS_ROLLEDBACK = 4;
/**
* Equal in value and meaning to {@link
* javax.transaction.Status#STATUS_UNKNOWN}.
*/
int STATUS_UNKNOWN = 5;
/**
* Equal in value and meaning to {@link
* javax.transaction.Status#STATUS_NO_TRANSACTION}.
*/
int STATUS_NO_TRANSACTION = 6;
/**
* Equal in value and meaning to {@link
* javax.transaction.Status#STATUS_PREPARING}.
*/
int STATUS_PREPARING = 7;
/**
* Equal in value and meaning to {@link
* javax.transaction.Status#STATUS_COMMITTING}.
*/
int STATUS_COMMITTING = 8;
/**
* Equal in value and meaning to {@link
* javax.transaction.Status#STATUS_ROLLING_BACK}.
*/
int STATUS_ROLLING_BACK = 9;
/*
* Method signatures.
*/
/**
* Returns {@code true} if JTA facilities are available.
*
* @return {@code true} if JTA facilities are available; {@code
* false} otherwise
*/
boolean isActive();
boolean isEnabled();
/**
* Returns the {@linkplain Context#isActive() active} {@link
* Context}, if any, that supports JTA facilities, or {@code null}
* Context}, if any, that supports JTA facilities at the moment of
* invocation, or {@code null}.
*
* <p>Implementations of this method may return {@code null}.</p>
* <p>Implementations of this method may, and often do, return
* {@code null}.</p>
*
* <p>The {@link Context} returned by implementations of this
* method may become active or inactive at any moment.</p>
*
* @return the {@link Context}, if any, that supports JTA
* facilities, or {@code null}
*
* @see Context#isActive()
*/
Context getContext();
/**
* Returns {@code true} if a JTA transaction is currently in
* effect.
* Returns a constant indicating the current transaction status.
*
* @return {@code true} if a JTA transaction is currently in
* effect; {@code false} otherwise
* <p>Implementations of this method must return {@link
* #STATUS_NO_TRANSACTION} ({@code 6}) if JTA is not supported.</p>
*
* @return a JTA {@link javax.transaction.Status} constant
* indicating the current transaction status
*/
boolean inTransaction();
int getStatus();
}

View File

@@ -16,6 +16,14 @@
ambiguousPersistenceUnitInfo = \
There was at least one @PersistenceContext- or @PersistenceUnit-annotated injection point that did not specify a value for \
its unitName element, and the following persistence units were found: {0}
closedStatus = \
closed (status: {0})
closedNotActive = \
closedNotActive ({0})
jpaTransactionScopedEntityManagerClose = \
You cannot close a container-managed EntityManager.
jpaTransactionScopedEntityManagerGetTransaction = \
You cannot call getTransaction() on a container-managed EntityManager.
mixedSynchronizationTypes = \
An instance of CdiTransactionScopedEntityManager is already affiliated with the current transaction, but has a different \
synchronization type than that desired. Instigating bean: {0}; existing contextual instance: {1}
@@ -33,3 +41,7 @@ preexistingExtendedEntityManager = \
resourceLocalPersistenceUnitDisallowed = \
The persistence unit {0} has a PersistenceUnitTransactionType of PersistenceUnitTransactionType.RESOURCE_LOCAL, but \
container-managed persistence units must have a PersistenceUnitTransactionType of PersistenceUnitTransactionType.JTA.
unexpectedCurrentStatus = \
Unexpected status: {0}
unexpectedPriorStatus = \
Unexpected status: {0}

View File

@@ -52,7 +52,7 @@ import static org.junit.jupiter.api.Assertions.fail;
@DataSourceDefinition(
name = "test",
className = "org.h2.jdbcx.JdbcDataSource",
url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
url = "jdbc:h2:mem:test",
serverName = "",
properties = {
"user=sa"

View File

@@ -47,6 +47,7 @@ public class Author implements Serializable {
@Column(name = "NAME",
insertable = true,
nullable = false,
unique = true,
updatable = true)
private String name;
@@ -71,5 +72,31 @@ public class Author implements Serializable {
public void setName(final String name) {
this.name = Objects.requireNonNull(name);
}
@Override
public int hashCode() {
final Object name = this.getName();
return name == null ? 0 : name.hashCode();
}
@Override
public boolean equals(final Object other) {
if (other == this) {
return true;
} else if (other instanceof Author) {
final Author her = (Author) other;
final Object name = this.getName();
if (name == null) {
if (her.getName() != null) {
return false;
}
} else if (!name.equals(her.getName())) {
return false;
}
return true;
} else {
return false;
}
}
}

View File

@@ -50,7 +50,7 @@ import static org.junit.jupiter.api.Assertions.fail;
@DataSourceDefinition(
name = "chirp",
className = "org.h2.jdbcx.JdbcDataSource",
url = "jdbc:h2:mem:chirp;INIT=RUNSCRIPT FROM 'classpath:chirp.ddl'",
url = "jdbc:h2:mem:chirp;INIT=SET TRACE_LEVEL_FILE=4\\;RUNSCRIPT FROM 'classpath:chirp.ddl'",
serverName = "",
properties = {
"user=sa"

View File

@@ -50,7 +50,7 @@ import static org.junit.jupiter.api.Assertions.fail;
@DataSourceDefinition(
name = "chirp",
className = "org.h2.jdbcx.JdbcDataSource",
url = "jdbc:h2:mem:chirp;INIT=RUNSCRIPT FROM 'classpath:chirp.ddl'",
url = "jdbc:h2:mem:chirp;INIT=SET TRACE_LEVEL_FILE=4\\;RUNSCRIPT FROM 'classpath:chirp.ddl'",
serverName = "",
properties = {
"user=sa"

View File

@@ -15,20 +15,31 @@
*/
package io.helidon.integrations.cdi.jpa.chirp;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Objects;
import javax.annotation.sql.DataSourceDefinition;
import javax.inject.Inject;
import javax.inject.Named;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.BeforeDestroyed;
import javax.enterprise.context.ContextNotActiveException;
import javax.enterprise.context.spi.Context;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.se.SeContainer;
import javax.enterprise.inject.se.SeContainerInitializer;
import javax.enterprise.inject.spi.BeanManager;
import javax.persistence.EntityExistsException;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import javax.persistence.SynchronizationType;
import javax.persistence.TransactionRequiredException;
import javax.sql.DataSource;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.NotSupportedException;
@@ -36,13 +47,16 @@ import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.TransactionManager;
import javax.transaction.TransactionScoped;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@@ -50,7 +64,7 @@ import static org.junit.jupiter.api.Assertions.fail;
@DataSourceDefinition(
name = "chirp",
className = "org.h2.jdbcx.JdbcDataSource",
url = "jdbc:h2:mem:chirp;INIT=RUNSCRIPT FROM 'classpath:chirp.ddl'",
url = "jdbc:h2:mem:chirp;INIT=SET TRACE_LEVEL_FILE=4\\;RUNSCRIPT FROM 'classpath:chirp.ddl'",
serverName = "",
properties = {
"user=sa"
@@ -66,6 +80,10 @@ class TestJpaTransactionScopedSynchronizedEntityManager {
@Inject
private TransactionManager transactionManager;
@Inject
@Named("chirp")
private DataSource dataSource;
@PersistenceContext(
type = PersistenceContextType.TRANSACTION,
@@ -126,6 +144,10 @@ class TestJpaTransactionScopedSynchronizedEntityManager {
return this.transactionManager;
}
DataSource getDataSource() {
return this.dataSource;
}
private void onShutdown(@Observes @BeforeDestroyed(ApplicationScoped.class) final Object event,
final TransactionManager tm) throws SystemException {
// If an assertion fails, or some other error happens in the
@@ -155,9 +177,14 @@ class TestJpaTransactionScopedSynchronizedEntityManager {
HeuristicRollbackException,
NotSupportedException,
RollbackException,
SQLException,
SystemException
{
// Get a BeanManager for later use.
final BeanManager beanManager = this.cdiContainer.getBeanManager();
assertNotNull(beanManager);
// Get a CDI contextual reference to this test instance. It
// is important to use "self" in this test instead of "this".
final TestJpaTransactionScopedSynchronizedEntityManager self =
@@ -170,6 +197,10 @@ class TestJpaTransactionScopedSynchronizedEntityManager {
assertNotNull(em);
assertTrue(em.isOpen());
// Get a DataSource for JPA-independent testing and assertions.
final DataSource dataSource = self.getDataSource();
assertNotNull(dataSource);
// We haven't started any kind of transaction yet and we
// aren't testing anything using
// the @javax.transaction.Transactional annotation so there is
@@ -180,7 +211,7 @@ class TestJpaTransactionScopedSynchronizedEntityManager {
// Create a JPA entity and try to insert it. This should fail
// because according to JPA a TransactionRequiredException
// will be thrown.
final Author author = new Author("Abraham Lincoln");
Author author = new Author("Abraham Lincoln");
try {
em.persist(author);
fail("A TransactionRequiredException should have been thrown");
@@ -192,8 +223,16 @@ class TestJpaTransactionScopedSynchronizedEntityManager {
// scenes and use it to start a Transaction.
final TransactionManager tm = self.getTransactionManager();
assertNotNull(tm);
tm.setTransactionTimeout(60 * 20); // TODO: set to 20 minutes for debugging purposes only
tm.begin();
// Grab the TransactionScoped context while the transaction is
// active. We want to make sure it's active at various
// points.
final Context transactionScopedContext = beanManager.getContext(TransactionScoped.class);
assertNotNull(transactionScopedContext);
assertTrue(transactionScopedContext.isActive());
// Now magically our EntityManager should be joined to it.
assertTrue(em.isJoinedToTransaction());
@@ -201,16 +240,26 @@ class TestJpaTransactionScopedSynchronizedEntityManager {
// is no longer joined to it.
tm.rollback();
assertFalse(em.isJoinedToTransaction());
assertFalse(transactionScopedContext.isActive());
// Start another transaction and persist our Author.
// Start another transaction.
tm.begin();
assertTrue(transactionScopedContext.isActive());
// Persist our Author.
assertNull(author.getId());
em.persist(author);
assertTrue(em.contains(author));
tm.commit();
// After the transaction commits, a flush should happen, and
// the author is managed, so we should see his ID.
assertEquals(Integer.valueOf(1), author.getId());
// The transaction is over, so our EntityManager is not joined
// to one anymore.
assertFalse(em.isJoinedToTransaction());
assertFalse(transactionScopedContext.isActive());
// Our PersistenceContextType is TRANSACTION, not EXTENDED, so
// the underlying persistence context dies with the
@@ -218,6 +267,82 @@ class TestJpaTransactionScopedSynchronizedEntityManager {
// should be empty, so the contains() operation should return
// false.
assertFalse(em.contains(author));
// Start a transaction.
tm.begin();
assertTrue(transactionScopedContext.isActive());
// Remove the Author we successfully committed before. We
// have to merge because the author became detached a few
// lines above.
author = em.merge(author);
em.remove(author);
assertFalse(em.contains(author));
tm.commit();
assertFalse(em.isJoinedToTransaction());
assertFalse(transactionScopedContext.isActive());
// Note that its ID is still 1.
assertEquals(Integer.valueOf(1), author.getId());
assertDatabaseIsEmpty(dataSource);
tm.begin();
try {
assertTrue(em.isJoinedToTransaction());
assertTrue(transactionScopedContext.isActive());
// This is interesting. author is now detached, but it
// still has a persistent identifier set to 1.
em.persist(author);
// The act of persisting doesn't flush anything, so our id
// is still 1.
assertEquals(Integer.valueOf(1), author.getId());
assertTrue(transactionScopedContext.isActive());
assertTrue(em.contains(author));
assertTrue(em.isJoinedToTransaction());
// Persisting the same thing again is a no-op.
em.persist(author);
// The act of persisting doesn't flush anything, so our id
// is still 1.
assertEquals(Integer.valueOf(1), author.getId());
// Make sure the TransactionContext is active.
assertTrue(transactionScopedContext.isActive());
assertTrue(em.contains(author));
assertTrue(em.isJoinedToTransaction());
tm.commit();
// Make sure the TransactionContext is NOT active.
assertFalse(em.isJoinedToTransaction());
assertFalse(em.contains(author));
assertFalse(transactionScopedContext.isActive());
// Now that the commit and accompanying flush have
// happened, our author's ID has changed.
assertEquals(Integer.valueOf(2), author.getId());
} catch (final EntityExistsException expected) {
} catch (final Exception somethingUnexpected) {
fail(somethingUnexpected);
}
}
private static final void assertDatabaseIsEmpty(final DataSource dataSource) throws SQLException {
try (final Connection connection = dataSource.getConnection();
final Statement statement = connection.createStatement();
final ResultSet resultSet = statement.executeQuery("SELECT COUNT(a.id) FROM AUTHOR a");) {
assertNotNull(resultSet);
assertTrue(resultSet.next());
assertEquals(0, resultSet.getInt(1));
}
}
}

View File

@@ -50,7 +50,7 @@ import static org.junit.jupiter.api.Assertions.fail;
@DataSourceDefinition(
name = "chirp",
className = "org.h2.jdbcx.JdbcDataSource",
url = "jdbc:h2:mem:chirp;INIT=RUNSCRIPT FROM 'classpath:chirp.ddl'",
url = "jdbc:h2:mem:chirp;INIT=SET TRACE_LEVEL_FILE=4\\;RUNSCRIPT FROM 'classpath:chirp.ddl'",
serverName = "",
properties = {
"user=sa"

View File

@@ -0,0 +1,313 @@
/*
* Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
*
* 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.
*/
package io.helidon.integrations.cdi.jpa.chirp;
import java.util.Objects;
import javax.annotation.sql.DataSourceDefinition;
import javax.inject.Inject;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.BeforeDestroyed;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.se.SeContainer;
import javax.enterprise.inject.se.SeContainerInitializer;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import javax.persistence.SynchronizationType;
import javax.persistence.TransactionRequiredException;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.TransactionManager;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@ApplicationScoped
@DataSourceDefinition(
name = "chirp",
className = "org.h2.jdbcx.JdbcDataSource",
url = "jdbc:h2:mem:chirp;INIT=SET TRACE_LEVEL_FILE=4\\;RUNSCRIPT FROM 'classpath:chirp.ddl'",
serverName = "",
properties = {
"user=sa"
}
)
class TestRollbackScenarios {
private SeContainer cdiContainer;
@Inject
private TransactionManager transactionManager;
@PersistenceContext(
type = PersistenceContextType.TRANSACTION,
synchronization = SynchronizationType.SYNCHRONIZED,
unitName = "chirp"
)
private EntityManager jpaTransactionScopedSynchronizedEntityManager;
/*
* Constructors.
*/
TestRollbackScenarios() {
super();
}
/*
* Setup and teardown methods.
*/
@BeforeEach
void startCdiContainer() {
final SeContainerInitializer initializer = SeContainerInitializer.newInstance()
.addBeanClasses(this.getClass());
assertNotNull(initializer);
this.cdiContainer = initializer.initialize();
}
@AfterEach
void shutDownCdiContainer() {
if (this.cdiContainer != null) {
this.cdiContainer.close();
}
}
/*
* Support methods.
*/
TestRollbackScenarios self() {
return this.cdiContainer.select(TestRollbackScenarios.class).get();
}
/**
* A "business method" providing access to one of this {@link
* TestRollbackScenarios}' {@link EntityManager}
* instances for use by {@link Test}-annotated methods.
*
* @return a non-{@code null} {@link EntityManager}
*/
EntityManager getJpaTransactionScopedSynchronizedEntityManager() {
return this.jpaTransactionScopedSynchronizedEntityManager;
}
TransactionManager getTransactionManager() {
return this.transactionManager;
}
private void onShutdown(@Observes @BeforeDestroyed(ApplicationScoped.class) final Object event,
final TransactionManager tm) throws SystemException {
// If an assertion fails, or some other error happens in the
// CDI container, there may be a current transaction that has
// neither been committed nor rolled back. Because the
// Narayana transaction engine is fundamentally static, this
// means that a transaction affiliation with the main thread
// may "leak" into another JUnit test (since JUnit, by
// default, forks once, and then runs all tests in the same
// JVM). CDI, thankfully, will fire an event for the
// application context shutting down, even in the case of
// errors.
if (tm.getStatus() != Status.STATUS_NO_TRANSACTION) {
tm.rollback();
}
}
/*
* Test methods.
*/
@Test
void testRollbackScenarios()
throws HeuristicMixedException,
HeuristicRollbackException,
InterruptedException,
NotSupportedException,
RollbackException,
SystemException
{
// Get a CDI contextual reference to this test instance. It
// is important to use "self" in this test instead of "this".
final TestRollbackScenarios self = self();
assertNotNull(self);
// Get the EntityManager that is synchronized with and scoped
// to a JTA transaction.
final EntityManager em = self.getJpaTransactionScopedSynchronizedEntityManager();
assertNotNull(em);
assertTrue(em.isOpen());
// We haven't started any kind of transaction yet and we
// aren't testing anything using
// the @javax.transaction.Transactional annotation so there is
// no transaction in effect so the EntityManager cannot be
// joined to one.
assertFalse(em.isJoinedToTransaction());
// Get the TransactionManager that normally is behind the
// scenes and use it to start a Transaction.
final TransactionManager tm = self.getTransactionManager();
assertNotNull(tm);
tm.begin();
assertEquals(Status.STATUS_ACTIVE, tm.getStatus());
// Now magically our EntityManager should be joined to it.
assertTrue(em.isJoinedToTransaction());
// Create a JPA entity and insert it.
Author author = new Author("Abraham Lincoln");
em.persist(author);
// No trip to the database has happened yet, so the author's
// identifier isn't set yet.
assertNull(author.getId());
// Commit the transaction. Because we're relying on the
// default flush mode, this will cause a flush to the
// database, which, in turn, will result in author identifier
// generation.
tm.commit();
assertEquals(Status.STATUS_NO_TRANSACTION, tm.getStatus());
assertEquals(Integer.valueOf(1), author.getId());
// We're no longer in a transaction.
assertFalse(em.isJoinedToTransaction());
// The persistence context should be cleared.
assertFalse(em.contains(author));
// Ensure transaction statuses are what we think they are.
tm.begin();
tm.setRollbackOnly();
try {
assertEquals(Status.STATUS_MARKED_ROLLBACK, tm.getStatus());
} finally {
tm.rollback();
}
assertEquals(Status.STATUS_NO_TRANSACTION, tm.getStatus());
// We can do non-transactional things.
assertTrue(em.isOpen());
author = em.find(Author.class, Integer.valueOf(1));
assertNotNull(author);
// Note that because we've invoked this somehow outside of a
// transaction everything it touches is detached, per section
// 7.6.2 of the JPA 2.2 specification.
assertFalse(em.contains(author));
// Remove everything.
tm.begin();
author = em.merge(author);
assertNotNull(author);
assertTrue(em.contains(author));
em.remove(author);
tm.commit();
assertFalse(em.contains(author));
// Perform a rollback "in the middle" of a sequence of
// operations and observe that the EntityManager is in the
// proper state throughout.
author = new Author("John Kennedy");
tm.begin();
em.persist(author);
assertTrue(em.contains(author));
tm.rollback();
assertEquals(Status.STATUS_NO_TRANSACTION, tm.getStatus());
assertFalse(em.contains(author));
try {
em.remove(author);
fail("remove() was allowed to complete without a transaction");
} catch (final IllegalArgumentException | TransactionRequiredException expected) {
// The javadocs say only that either of these exceptions may
// be thrown in this case but do not indicate which one is
// preferred. EclipseLink 2.7.4 throws a
// TransactionRequiredException here. It probably should
// throw an IllegalArgumentException; see
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=553117
// which is related.
}
// author is detached; prove it.
tm.begin();
assertEquals(Status.STATUS_ACTIVE, tm.getStatus());
assertTrue(em.isJoinedToTransaction());
assertFalse(em.contains(author));
em.detach(author); // redundant; just making a point
assertFalse(em.contains(author));
try {
em.remove(author);
// We shouldn't get here because author is detached but with
// EclipseLink 2.7.4 we do. See
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=553117.
} catch (final IllegalArgumentException expected) {
}
tm.rollback();
// Remove the author properly.
tm.begin();
assertEquals(Status.STATUS_ACTIVE, tm.getStatus());
assertTrue(em.isJoinedToTransaction());
assertFalse(em.contains(author));
author = em.merge(author);
em.remove(author);
tm.commit();
assertFalse(em.contains(author));
// Cause a timeout-tripped rollback.
tm.setTransactionTimeout(1); // 1 second
author = new Author("Woodrow Wilson");
tm.begin();
assertEquals(Status.STATUS_ACTIVE, tm.getStatus());
Thread.sleep(1500L); // 1.5 seconds (arbitrarily greater than 1 second)
assertEquals(Status.STATUS_ROLLEDBACK, tm.getStatus());
try {
em.persist(author);
fail("Transaction rolled back but persist still happened");
} catch (final TransactionRequiredException expected) {
}
tm.rollback();
assertEquals(Status.STATUS_NO_TRANSACTION, tm.getStatus());
tm.setTransactionTimeout(60); // 60 seconds; the usual default
}
}

View File

@@ -14,4 +14,6 @@
.level=INFO
handlers=io.helidon.common.HelidonConsoleHandler
io.helidon.integrations.cdi.jpa.level=FINER
io.helidon.integrations.cdi.jpa.level=INFO
org.eclipse.persistence.level=INFO
h2database.level=INFO

View File

@@ -1,16 +1,16 @@
CREATE TABLE AUTHOR (
CREATE TABLE IF NOT EXISTS AUTHOR (
ID INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(64) NOT NULL
NAME VARCHAR(64) NOT NULL UNIQUE
);
CREATE TABLE MICROBLOG (
CREATE TABLE IF NOT EXISTS MICROBLOG (
ID INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
AUTHOR_ID INT NOT NULL,
NAME VARCHAR(64) NOT NULL,
FOREIGN KEY (AUTHOR_ID) REFERENCES AUTHOR(ID)
);
CREATE TABLE CHIRP (
CREATE TABLE IF NOT EXISTS CHIRP (
ID INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
MICROBLOG_ID INT NOT NULL,
CONTENT VARCHAR(140) NOT NULL,

View File

@@ -16,6 +16,7 @@
package io.helidon.integrations.jta.cdi;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.BeforeDestroyed;
import javax.enterprise.context.Destroyed;
import javax.enterprise.context.Initialized;
import javax.enterprise.event.Event;
@@ -58,6 +59,15 @@ class NarayanaTransactionManager extends DelegatingTransactionManager {
*/
private final Event<Transaction> transactionScopeInitializedBroadcaster;
/**
* An {@link Event} capable of {@linkplain Event#fire(Object)
* firing} an {@link Object} just before {@linkplain TransactionScoped
* transaction scope} is about to end.
*
* <p>This field may be {@code null}.</p>
*/
private final Event<Object> transactionScopeBeforeDestroyedBroadcaster;
/**
* An {@link Event} capable of {@linkplain Event#fire(Object)
* firing} an {@link Object} when {@linkplain TransactionScoped
@@ -91,7 +101,7 @@ class NarayanaTransactionManager extends DelegatingTransactionManager {
*/
@Deprecated
NarayanaTransactionManager() {
this(null, null, null);
this(null, null, null, null);
}
/**
@@ -120,10 +130,13 @@ class NarayanaTransactionManager extends DelegatingTransactionManager {
NarayanaTransactionManager(final JTAEnvironmentBean jtaEnvironmentBean,
@Initialized(TransactionScoped.class)
final Event<Transaction> transactionScopeInitializedBroadcaster,
@BeforeDestroyed(TransactionScoped.class)
final Event<Object> transactionScopeBeforeDestroyedBroadcaster,
@Destroyed(TransactionScoped.class)
final Event<Object> transactionScopeDestroyedBroadcaster) {
super(jtaEnvironmentBean == null ? null : jtaEnvironmentBean.getTransactionManager());
this.transactionScopeInitializedBroadcaster = transactionScopeInitializedBroadcaster;
this.transactionScopeBeforeDestroyedBroadcaster = transactionScopeBeforeDestroyedBroadcaster;
this.transactionScopeDestroyedBroadcaster = transactionScopeDestroyedBroadcaster;
}
@@ -200,7 +213,13 @@ class NarayanaTransactionManager extends DelegatingTransactionManager {
@Override
public void commit() throws HeuristicMixedException, HeuristicRollbackException, RollbackException, SystemException {
try {
super.commit();
try {
if (this.transactionScopeBeforeDestroyedBroadcaster != null) {
this.transactionScopeBeforeDestroyedBroadcaster.fire(this.toString());
}
} finally {
super.commit();
}
} finally {
if (this.transactionScopeDestroyedBroadcaster != null) {
this.transactionScopeDestroyedBroadcaster.fire(this.toString());
@@ -234,7 +253,13 @@ class NarayanaTransactionManager extends DelegatingTransactionManager {
@Override
public void rollback() throws SystemException {
try {
super.rollback();
try {
if (this.transactionScopeBeforeDestroyedBroadcaster != null) {
this.transactionScopeBeforeDestroyedBroadcaster.fire(this.toString());
}
} finally {
super.rollback();
}
} finally {
if (this.transactionScopeDestroyedBroadcaster != null) {
this.transactionScopeDestroyedBroadcaster.fire(this.toString());