HHH-8973 - Fix tracking entity modifications to detached entities with the modified flags feature.
This commit is contained in:
parent
ad0cd4fc18
commit
ebf4803a33
|
@ -18,6 +18,7 @@ import org.hibernate.envers.event.spi.EnversPostInsertEventListenerImpl;
|
||||||
import org.hibernate.envers.event.spi.EnversPostUpdateEventListenerImpl;
|
import org.hibernate.envers.event.spi.EnversPostUpdateEventListenerImpl;
|
||||||
import org.hibernate.envers.event.spi.EnversPreCollectionRemoveEventListenerImpl;
|
import org.hibernate.envers.event.spi.EnversPreCollectionRemoveEventListenerImpl;
|
||||||
import org.hibernate.envers.event.spi.EnversPreCollectionUpdateEventListenerImpl;
|
import org.hibernate.envers.event.spi.EnversPreCollectionUpdateEventListenerImpl;
|
||||||
|
import org.hibernate.envers.event.spi.EnversPreUpdateEventListenerImpl;
|
||||||
import org.hibernate.event.service.spi.EventListenerRegistry;
|
import org.hibernate.event.service.spi.EventListenerRegistry;
|
||||||
import org.hibernate.event.spi.EventType;
|
import org.hibernate.event.spi.EventType;
|
||||||
import org.hibernate.integrator.spi.Integrator;
|
import org.hibernate.integrator.spi.Integrator;
|
||||||
|
@ -29,6 +30,7 @@ import org.jboss.logging.Logger;
|
||||||
* Hooks up Envers event listeners.
|
* Hooks up Envers event listeners.
|
||||||
*
|
*
|
||||||
* @author Steve Ebersole
|
* @author Steve Ebersole
|
||||||
|
* @author Chris Cranford
|
||||||
*/
|
*/
|
||||||
public class EnversIntegrator implements Integrator {
|
public class EnversIntegrator implements Integrator {
|
||||||
private static final Logger log = Logger.getLogger( EnversIntegrator.class );
|
private static final Logger log = Logger.getLogger( EnversIntegrator.class );
|
||||||
|
@ -89,6 +91,10 @@ public class EnversIntegrator implements Integrator {
|
||||||
EventType.POST_INSERT,
|
EventType.POST_INSERT,
|
||||||
new EnversPostInsertEventListenerImpl( enversService )
|
new EnversPostInsertEventListenerImpl( enversService )
|
||||||
);
|
);
|
||||||
|
listenerRegistry.appendListeners(
|
||||||
|
EventType.PRE_UPDATE,
|
||||||
|
new EnversPreUpdateEventListenerImpl( enversService )
|
||||||
|
);
|
||||||
listenerRegistry.appendListeners(
|
listenerRegistry.appendListeners(
|
||||||
EventType.POST_UPDATE,
|
EventType.POST_UPDATE,
|
||||||
new EnversPostUpdateEventListenerImpl( enversService )
|
new EnversPostUpdateEventListenerImpl( enversService )
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* Hibernate, Relational Persistence for Idiomatic Java
|
||||||
|
*
|
||||||
|
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
|
||||||
|
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
|
||||||
|
*/
|
||||||
|
package org.hibernate.envers.event.spi;
|
||||||
|
|
||||||
|
import org.hibernate.envers.boot.internal.EnversService;
|
||||||
|
import org.hibernate.envers.internal.entities.EntityConfiguration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Chris Cranford
|
||||||
|
*/
|
||||||
|
public abstract class BaseEnversUpdateEventListener extends BaseEnversEventListener {
|
||||||
|
public BaseEnversUpdateEventListener(EnversService enversService) {
|
||||||
|
super( enversService );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the entity has {@code withModifiedFlag} features and has no old state, most likely implying
|
||||||
|
* it was updated in a detached entity state.
|
||||||
|
*
|
||||||
|
* @param entityName The associated entity name.
|
||||||
|
* @param oldState The event old (likely detached) entity state.
|
||||||
|
* @return {@code true} if the entity is/has been updated in detached state, otherwise {@code false}.
|
||||||
|
*/
|
||||||
|
protected boolean isDetachedEntityUpdate(String entityName, Object[] oldState) {
|
||||||
|
final EntityConfiguration configuration = getEnversService().getEntitiesConfigurations().get( entityName );
|
||||||
|
if ( configuration.getPropertyMapper() != null && oldState == null ) {
|
||||||
|
return configuration.getPropertyMapper().hasPropertiesWithModifiedFlag();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,8 +20,9 @@ import org.hibernate.persister.entity.EntityPersister;
|
||||||
* @author Adam Warski (adam at warski dot org)
|
* @author Adam Warski (adam at warski dot org)
|
||||||
* @author HernпїЅn Chanfreau
|
* @author HernпїЅn Chanfreau
|
||||||
* @author Steve Ebersole
|
* @author Steve Ebersole
|
||||||
|
* @author Chris Cranford
|
||||||
*/
|
*/
|
||||||
public class EnversPostUpdateEventListenerImpl extends BaseEnversEventListener implements PostUpdateEventListener {
|
public class EnversPostUpdateEventListenerImpl extends BaseEnversUpdateEventListener implements PostUpdateEventListener {
|
||||||
public EnversPostUpdateEventListenerImpl(EnversService enversService) {
|
public EnversPostUpdateEventListenerImpl(EnversService enversService) {
|
||||||
super( enversService );
|
super( enversService );
|
||||||
}
|
}
|
||||||
|
@ -34,6 +35,8 @@ public class EnversPostUpdateEventListenerImpl extends BaseEnversEventListener i
|
||||||
checkIfTransactionInProgress( event.getSession() );
|
checkIfTransactionInProgress( event.getSession() );
|
||||||
|
|
||||||
final AuditProcess auditProcess = getEnversService().getAuditProcessManager().get( event.getSession() );
|
final AuditProcess auditProcess = getEnversService().getAuditProcessManager().get( event.getSession() );
|
||||||
|
|
||||||
|
Object[] oldState = getOldDBState( auditProcess, entityName, event );
|
||||||
final Object[] newDbState = postUpdateDBState( event );
|
final Object[] newDbState = postUpdateDBState( event );
|
||||||
final AuditWorkUnit workUnit = new ModWorkUnit(
|
final AuditWorkUnit workUnit = new ModWorkUnit(
|
||||||
event.getSession(),
|
event.getSession(),
|
||||||
|
@ -42,7 +45,7 @@ public class EnversPostUpdateEventListenerImpl extends BaseEnversEventListener i
|
||||||
event.getId(),
|
event.getId(),
|
||||||
event.getPersister(),
|
event.getPersister(),
|
||||||
newDbState,
|
newDbState,
|
||||||
event.getOldState()
|
oldState
|
||||||
);
|
);
|
||||||
auditProcess.addWorkUnit( workUnit );
|
auditProcess.addWorkUnit( workUnit );
|
||||||
|
|
||||||
|
@ -52,13 +55,20 @@ public class EnversPostUpdateEventListenerImpl extends BaseEnversEventListener i
|
||||||
event.getPersister(),
|
event.getPersister(),
|
||||||
entityName,
|
entityName,
|
||||||
newDbState,
|
newDbState,
|
||||||
event.getOldState(),
|
oldState,
|
||||||
event.getSession()
|
event.getSession()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Object[] getOldDBState(AuditProcess auditProcess, String entityName, PostUpdateEvent event) {
|
||||||
|
if ( isDetachedEntityUpdate( entityName, event.getOldState() ) ) {
|
||||||
|
return auditProcess.getCachedEntityState( event.getId(), entityName );
|
||||||
|
}
|
||||||
|
return event.getOldState();
|
||||||
|
}
|
||||||
|
|
||||||
private Object[] postUpdateDBState(PostUpdateEvent event) {
|
private Object[] postUpdateDBState(PostUpdateEvent event) {
|
||||||
final Object[] newDbState = event.getState().clone();
|
final Object[] newDbState = event.getState().clone();
|
||||||
if ( event.getOldState() != null ) {
|
if ( event.getOldState() != null ) {
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Hibernate, Relational Persistence for Idiomatic Java
|
||||||
|
*
|
||||||
|
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
|
||||||
|
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
|
||||||
|
*/
|
||||||
|
package org.hibernate.envers.event.spi;
|
||||||
|
|
||||||
|
import org.hibernate.envers.boot.internal.EnversService;
|
||||||
|
import org.hibernate.envers.internal.synchronization.AuditProcess;
|
||||||
|
import org.hibernate.event.spi.PreUpdateEvent;
|
||||||
|
import org.hibernate.event.spi.PreUpdateEventListener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envers-specific entity (pre) update event listener.
|
||||||
|
*
|
||||||
|
* @author Chris Cranford
|
||||||
|
*/
|
||||||
|
public class EnversPreUpdateEventListenerImpl extends BaseEnversUpdateEventListener implements PreUpdateEventListener {
|
||||||
|
public EnversPreUpdateEventListenerImpl(EnversService enversService) {
|
||||||
|
super( enversService );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onPreUpdate(PreUpdateEvent event) {
|
||||||
|
final String entityName = event.getPersister().getEntityName();
|
||||||
|
if ( getEnversService().getEntitiesConfigurations().isVersioned( entityName ) ) {
|
||||||
|
checkIfTransactionInProgress( event.getSession() );
|
||||||
|
if ( isDetachedEntityUpdate( entityName, event.getOldState() ) ) {
|
||||||
|
final AuditProcess auditProcess = getEnversService().getAuditProcessManager().get( event.getSession() );
|
||||||
|
auditProcess.cacheEntityState(
|
||||||
|
event.getId(),
|
||||||
|
entityName,
|
||||||
|
event.getPersister().getDatabaseSnapshot( event.getId(), event.getSession() )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ import org.hibernate.property.access.spi.Setter;
|
||||||
* @author Adam Warski (adam at warski dot org)
|
* @author Adam Warski (adam at warski dot org)
|
||||||
* @author Michal Skowronek (mskowr at o2 dot pl)
|
* @author Michal Skowronek (mskowr at o2 dot pl)
|
||||||
* @author Lukasz Zuchowski (author at zuchos dot com)
|
* @author Lukasz Zuchowski (author at zuchos dot com)
|
||||||
|
* @author Chris Cranford
|
||||||
*/
|
*/
|
||||||
public class ComponentPropertyMapper implements PropertyMapper, CompositeMapperBuilder {
|
public class ComponentPropertyMapper implements PropertyMapper, CompositeMapperBuilder {
|
||||||
private final PropertyData propertyData;
|
private final PropertyData propertyData;
|
||||||
|
@ -158,4 +159,9 @@ public class ComponentPropertyMapper implements PropertyMapper, CompositeMapperB
|
||||||
public Map<PropertyData, PropertyMapper> getProperties() {
|
public Map<PropertyData, PropertyMapper> getProperties() {
|
||||||
return delegate.getProperties();
|
return delegate.getProperties();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPropertiesWithModifiedFlag() {
|
||||||
|
return delegate.hasPropertiesWithModifiedFlag();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Hibernate, Relational Persistence for Idiomatic Java
|
||||||
|
*
|
||||||
|
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
|
||||||
|
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
|
||||||
|
*/
|
||||||
|
package org.hibernate.envers.internal.entities.mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contract for {@link PropertyMapper} implementations to expose whether they contain any property
|
||||||
|
* that uses {@link org.hibernate.envers.internal.entities.PropertyData#isUsingModifiedFlag()}.
|
||||||
|
*
|
||||||
|
* @author Chris Cranford
|
||||||
|
*/
|
||||||
|
public interface ModifiedFlagMapperSupport {
|
||||||
|
/**
|
||||||
|
* Returns whether the associated {@link PropertyMapper} has any properties that use
|
||||||
|
* the {@code witModifiedFlag} feature.
|
||||||
|
*
|
||||||
|
* @return {@code true} if a property uses {@code withModifiedFlag}, otherwise {@code false}.
|
||||||
|
*/
|
||||||
|
boolean hasPropertiesWithModifiedFlag();
|
||||||
|
}
|
|
@ -233,4 +233,14 @@ public class MultiPropertyMapper implements ExtendedPropertyMapper {
|
||||||
public Map<String, PropertyData> getPropertyDatas() {
|
public Map<String, PropertyData> getPropertyDatas() {
|
||||||
return propertyDatas;
|
return propertyDatas;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPropertiesWithModifiedFlag() {
|
||||||
|
for ( PropertyData property : getProperties().keySet() ) {
|
||||||
|
if ( property.isUsingModifiedFlag() ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,9 @@ import org.hibernate.envers.internal.reader.AuditReaderImplementor;
|
||||||
/**
|
/**
|
||||||
* @author Adam Warski (adam at warski dot org)
|
* @author Adam Warski (adam at warski dot org)
|
||||||
* @author Michal Skowronek (mskowr at o2 dot pl)
|
* @author Michal Skowronek (mskowr at o2 dot pl)
|
||||||
|
* @author Chris Cranford
|
||||||
*/
|
*/
|
||||||
public interface PropertyMapper {
|
public interface PropertyMapper extends ModifiedFlagMapperSupport {
|
||||||
/**
|
/**
|
||||||
* Maps properties to the given map, basing on differences between properties of new and old objects.
|
* Maps properties to the given map, basing on differences between properties of new and old objects.
|
||||||
*
|
*
|
||||||
|
|
|
@ -133,4 +133,8 @@ public class SinglePropertyMapper implements PropertyMapper, SimpleMapperBuilder
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPropertiesWithModifiedFlag() {
|
||||||
|
return propertyData != null && propertyData.isUsingModifiedFlag();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.hibernate.envers.internal.reader.AuditReaderImplementor;
|
||||||
*
|
*
|
||||||
* @author Adam Warski (adam at warski dot org)
|
* @author Adam Warski (adam at warski dot org)
|
||||||
* @author Michal Skowronek (mskowr at o2 dot pl)
|
* @author Michal Skowronek (mskowr at o2 dot pl)
|
||||||
|
* @author Chris Cranford
|
||||||
*/
|
*/
|
||||||
public class SubclassPropertyMapper implements ExtendedPropertyMapper {
|
public class SubclassPropertyMapper implements ExtendedPropertyMapper {
|
||||||
private ExtendedPropertyMapper main;
|
private ExtendedPropertyMapper main;
|
||||||
|
@ -140,4 +141,16 @@ public class SubclassPropertyMapper implements ExtendedPropertyMapper {
|
||||||
joinedProperties.putAll( main.getProperties() );
|
joinedProperties.putAll( main.getProperties() );
|
||||||
return joinedProperties;
|
return joinedProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPropertiesWithModifiedFlag() {
|
||||||
|
// checks all properties, exposed both by the main mapper and parent mapper.
|
||||||
|
for ( PropertyData property : getProperties().keySet() ) {
|
||||||
|
if ( property.isUsingModifiedFlag() ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ import org.hibernate.property.access.spi.Setter;
|
||||||
/**
|
/**
|
||||||
* @author Adam Warski (adam at warski dot org)
|
* @author Adam Warski (adam at warski dot org)
|
||||||
* @author Michal Skowronek (mskowr at o2 dot pl)
|
* @author Michal Skowronek (mskowr at o2 dot pl)
|
||||||
|
* @author Chris Cranford
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractCollectionMapper<T> implements PropertyMapper {
|
public abstract class AbstractCollectionMapper<T> implements PropertyMapper {
|
||||||
protected final CommonCollectionMapperData commonCollectionMapperData;
|
protected final CommonCollectionMapperData commonCollectionMapperData;
|
||||||
|
@ -395,4 +396,13 @@ public abstract class AbstractCollectionMapper<T> implements PropertyMapper {
|
||||||
|
|
||||||
return collectionChanges;
|
return collectionChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPropertiesWithModifiedFlag() {
|
||||||
|
if ( commonCollectionMapperData != null ) {
|
||||||
|
final PropertyData propertyData = commonCollectionMapperData.getCollectionReferencingPropertyData();
|
||||||
|
return propertyData != null && propertyData.isUsingModifiedFlag();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import org.hibernate.service.ServiceRegistry;
|
||||||
* Base class for property mappers that manage to-one relation.
|
* Base class for property mappers that manage to-one relation.
|
||||||
*
|
*
|
||||||
* @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
|
* @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
|
||||||
|
* @author Chris Cranford
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractToOneMapper implements PropertyMapper {
|
public abstract class AbstractToOneMapper implements PropertyMapper {
|
||||||
private final ServiceRegistry serviceRegistry;
|
private final ServiceRegistry serviceRegistry;
|
||||||
|
@ -135,4 +136,9 @@ public abstract class AbstractToOneMapper implements PropertyMapper {
|
||||||
return audited;
|
return audited;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPropertiesWithModifiedFlag() {
|
||||||
|
return propertyData != null && propertyData.isUsingModifiedFlag();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import javax.persistence.FlushModeType;
|
||||||
import org.hibernate.Session;
|
import org.hibernate.Session;
|
||||||
import org.hibernate.action.spi.BeforeTransactionCompletionProcess;
|
import org.hibernate.action.spi.BeforeTransactionCompletionProcess;
|
||||||
import org.hibernate.engine.spi.SessionImplementor;
|
import org.hibernate.engine.spi.SessionImplementor;
|
||||||
|
import org.hibernate.envers.exception.AuditException;
|
||||||
import org.hibernate.envers.internal.revisioninfo.RevisionInfoGenerator;
|
import org.hibernate.envers.internal.revisioninfo.RevisionInfoGenerator;
|
||||||
import org.hibernate.envers.internal.synchronization.work.AuditWorkUnit;
|
import org.hibernate.envers.internal.synchronization.work.AuditWorkUnit;
|
||||||
import org.hibernate.envers.tools.Pair;
|
import org.hibernate.envers.tools.Pair;
|
||||||
|
@ -24,6 +25,7 @@ import org.jboss.logging.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Adam Warski (adam at warski dot org)
|
* @author Adam Warski (adam at warski dot org)
|
||||||
|
* @author Chris Cranford
|
||||||
*/
|
*/
|
||||||
public class AuditProcess implements BeforeTransactionCompletionProcess {
|
public class AuditProcess implements BeforeTransactionCompletionProcess {
|
||||||
private static final Logger log = Logger.getLogger( AuditProcess.class );
|
private static final Logger log = Logger.getLogger( AuditProcess.class );
|
||||||
|
@ -34,8 +36,8 @@ public class AuditProcess implements BeforeTransactionCompletionProcess {
|
||||||
private final LinkedList<AuditWorkUnit> workUnits;
|
private final LinkedList<AuditWorkUnit> workUnits;
|
||||||
private final Queue<AuditWorkUnit> undoQueue;
|
private final Queue<AuditWorkUnit> undoQueue;
|
||||||
private final Map<Pair<String, Object>, AuditWorkUnit> usedIds;
|
private final Map<Pair<String, Object>, AuditWorkUnit> usedIds;
|
||||||
|
private final Map<Pair<String, Object>, Object[]> entityStateCache;
|
||||||
private final EntityChangeNotifier entityChangeNotifier;
|
private final EntityChangeNotifier entityChangeNotifier;
|
||||||
|
|
||||||
private Object revisionData;
|
private Object revisionData;
|
||||||
|
|
||||||
public AuditProcess(RevisionInfoGenerator revisionInfoGenerator, SessionImplementor session) {
|
public AuditProcess(RevisionInfoGenerator revisionInfoGenerator, SessionImplementor session) {
|
||||||
|
@ -45,9 +47,27 @@ public class AuditProcess implements BeforeTransactionCompletionProcess {
|
||||||
workUnits = new LinkedList<>();
|
workUnits = new LinkedList<>();
|
||||||
undoQueue = new LinkedList<>();
|
undoQueue = new LinkedList<>();
|
||||||
usedIds = new HashMap<>();
|
usedIds = new HashMap<>();
|
||||||
|
entityStateCache = new HashMap<>();
|
||||||
entityChangeNotifier = new EntityChangeNotifier( revisionInfoGenerator, session );
|
entityChangeNotifier = new EntityChangeNotifier( revisionInfoGenerator, session );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void cacheEntityState(Object id, String entityName, Object[] snapshot) {
|
||||||
|
final Pair<String, Object> key = new Pair<>( entityName, id );
|
||||||
|
if ( entityStateCache.containsKey( key ) ) {
|
||||||
|
throw new AuditException( "The entity [" + entityName + "] with id [" + id + "] is already cached." );
|
||||||
|
}
|
||||||
|
entityStateCache.put( key, snapshot );
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object[] getCachedEntityState(Object id, String entityName) {
|
||||||
|
final Pair<String, Object> key = new Pair<>( entityName, id );
|
||||||
|
final Object[] entityState = entityStateCache.get( key );
|
||||||
|
if ( entityState != null ) {
|
||||||
|
entityStateCache.remove( key );
|
||||||
|
}
|
||||||
|
return entityState;
|
||||||
|
}
|
||||||
|
|
||||||
private void removeWorkUnit(AuditWorkUnit vwu) {
|
private void removeWorkUnit(AuditWorkUnit vwu) {
|
||||||
workUnits.remove( vwu );
|
workUnits.remove( vwu );
|
||||||
if ( vwu.isPerformed() ) {
|
if ( vwu.isPerformed() ) {
|
||||||
|
|
Loading…
Reference in New Issue