parent
c59660ca86
commit
e5408884f6
8 changed files with 90 additions and 24 deletions
|
@ -46,13 +46,17 @@ public interface JpaRootEntity extends AbstractEntity, Serializable {
|
|||
* calls this method whenever the root entity or one of its children changes.
|
||||
*
|
||||
* Future versions of this method might restrict downgrading to downgrade only from the next version.
|
||||
*
|
||||
* @return <code>true</code> if the entityVersion was effectively changed, <code>false</code> otherwise.
|
||||
*/
|
||||
default void updateEntityVersion() {
|
||||
default boolean updateEntityVersion() {
|
||||
Integer ev = getEntityVersion();
|
||||
Integer currentEv = getCurrentSchemaVersion();
|
||||
if (ev != null && !Objects.equals(ev, currentEv)) {
|
||||
setEntityVersion(currentEv);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Integer getCurrentSchemaVersion();
|
||||
|
|
|
@ -37,6 +37,7 @@ import org.keycloak.models.map.authSession.MapAuthenticationSessionEntity;
|
|||
import org.keycloak.models.map.common.DeepCloner;
|
||||
import org.keycloak.models.map.common.UpdatableEntity;
|
||||
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_AUTH_SESSION;
|
||||
import org.keycloak.models.map.storage.jpa.JpaChildEntity;
|
||||
import org.keycloak.models.map.storage.jpa.JpaRootVersionedEntity;
|
||||
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
|
||||
import org.keycloak.sessions.CommonClientSessionModel;
|
||||
|
@ -47,7 +48,7 @@ import org.keycloak.sessions.CommonClientSessionModel;
|
|||
@Entity
|
||||
@Table(name = "kc_auth_session")
|
||||
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
|
||||
public class JpaAuthenticationSessionEntity extends UpdatableEntity.Impl implements MapAuthenticationSessionEntity, JpaRootVersionedEntity {
|
||||
public class JpaAuthenticationSessionEntity extends UpdatableEntity.Impl implements MapAuthenticationSessionEntity, JpaRootVersionedEntity, JpaChildEntity<JpaRootAuthenticationSessionEntity>{
|
||||
|
||||
@Id
|
||||
@Column
|
||||
|
@ -86,6 +87,11 @@ public class JpaAuthenticationSessionEntity extends UpdatableEntity.Impl impleme
|
|||
return metadata != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JpaRootAuthenticationSessionEntity getParent() {
|
||||
return root;
|
||||
}
|
||||
|
||||
public void setParent(JpaRootAuthenticationSessionEntity root) {
|
||||
this.root = root;
|
||||
}
|
||||
|
|
|
@ -30,22 +30,30 @@ import org.keycloak.models.map.storage.jpa.JpaRootEntity;
|
|||
/**
|
||||
* Listen on changes on child- and root entities and updates the current entity version of the root.
|
||||
*
|
||||
* This support a multiple level parent-child relationship, where the upmost parent needs the entity version to be updated.
|
||||
* This support a multiple level parent-child relationship, where all parents needs the entity version to be updated
|
||||
* in case it was effectively changed. The traversing is stopped at that point when it is detected that parent entity
|
||||
* version is the same one.
|
||||
*
|
||||
* It is based on an assumption that it may happen that one parent entity could be extracted into "parent A -> parent B -> child"
|
||||
* format. Then the change (insertion, deletion or update) of the child should bump the entity version of both parent B and parent A.
|
||||
*/
|
||||
public class JpaEntityVersionListener implements PreInsertEventListener, PreDeleteEventListener, PreUpdateEventListener {
|
||||
|
||||
public static final JpaEntityVersionListener INSTANCE = new JpaEntityVersionListener();
|
||||
|
||||
/**
|
||||
* Traverse from current entity up to the upmost parent, then update the entity version if it is a root entity.
|
||||
* Traverse from current entity through its parent tree and update the entity version of it.
|
||||
* Stop if non-changed parent is found.
|
||||
*/
|
||||
public void updateEntityVersion(Object entity) throws HibernateException {
|
||||
Object root = entity;
|
||||
while(root instanceof JpaChildEntity) {
|
||||
root = ((JpaChildEntity<?>) entity).getParent();
|
||||
}
|
||||
if (root instanceof JpaRootEntity) {
|
||||
((JpaRootEntity) root).updateEntityVersion();
|
||||
if (root instanceof JpaRootEntity) {
|
||||
if (!((JpaRootEntity) root).updateEntityVersion()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,26 +30,27 @@ import org.keycloak.models.map.storage.jpa.JpaRootVersionedEntity;
|
|||
|
||||
import javax.persistence.LockModeType;
|
||||
import java.util.Objects;
|
||||
import org.keycloak.models.map.storage.jpa.JpaRootEntity;
|
||||
|
||||
/**
|
||||
* Listen on changes on child entities and forces an optimistic locking increment on the topmost parent aka root.
|
||||
* Listen on changes on child entities and forces an optimistic locking increment on the closest parent aka root.
|
||||
* The assumption is that any parent of a child entity is root entity. Optimistic locking is enforced on child entity
|
||||
* which is not the child entity at the same time. This prevents {@link javax.persistence.OptimisticLockException}s
|
||||
* when different children in the same parent are being manipulated at the same time by different threads.
|
||||
*
|
||||
* This support a multiple level parent-child relationship, where only the upmost parent is locked.
|
||||
* This support a multiple level parent-child relationship, where only the closest parent is locked.
|
||||
*/
|
||||
public class JpaOptimisticLockingListener implements PreInsertEventListener, PreDeleteEventListener, PreUpdateEventListener {
|
||||
|
||||
public static final JpaOptimisticLockingListener INSTANCE = new JpaOptimisticLockingListener();
|
||||
|
||||
/**
|
||||
* Check if the entity is a child with a parent and force optimistic locking increment on the upmost parent aka root.
|
||||
* Check if the entity is a child with a parent and force optimistic locking increment on the parent aka root.
|
||||
*/
|
||||
public void lockRootEntity(Session session, Object entity) throws HibernateException {
|
||||
if(entity instanceof JpaChildEntity) {
|
||||
Object root = entity;
|
||||
while (root instanceof JpaChildEntity) {
|
||||
root = ((JpaChildEntity<?>) entity).getParent();
|
||||
Objects.requireNonNull(root, "children must always return their parent, never null");
|
||||
}
|
||||
if (entity instanceof JpaChildEntity && ! (entity instanceof JpaRootEntity)) {
|
||||
Object root = ((JpaChildEntity<?>) entity).getParent();
|
||||
Objects.requireNonNull(root, "children must always return their parent, never null");
|
||||
|
||||
// do not lock if root doesn't implement implicit optimistic locking mechanism
|
||||
if (! (root instanceof JpaRootVersionedEntity)) return;
|
||||
|
|
|
@ -39,6 +39,7 @@ import org.keycloak.models.map.common.UpdatableEntity;
|
|||
import org.keycloak.models.map.common.UuidValidator;
|
||||
import org.keycloak.models.map.realm.entity.MapComponentEntity;
|
||||
import org.keycloak.models.map.storage.jpa.Constants;
|
||||
import org.keycloak.models.map.storage.jpa.JpaChildEntity;
|
||||
import org.keycloak.models.map.storage.jpa.JpaRootVersionedEntity;
|
||||
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
|
||||
|
||||
|
@ -48,10 +49,7 @@ import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
|
|||
* to indicate that they are automatically generated from json fields. As such, these fields are non-insertable and non-updatable.
|
||||
* <p/>
|
||||
* Components are independent (i.e. a component doesn't depend on another component) and can be manipulated directly via
|
||||
* the component endpoints. Because of that, this entity implements {@link JpaRootVersionedEntity} instead of
|
||||
* {@link org.keycloak.models.map.storage.jpa.JpaChildEntity}. This prevents {@link javax.persistence.OptimisticLockException}s
|
||||
* when different components in the same realm are being manipulated at the same time - for example, when multiple components
|
||||
* are being added to the realm by different threads.
|
||||
* the component endpoints.
|
||||
* <p/>
|
||||
* By implementing {@link JpaRootVersionedEntity}, this entity will enforce optimistic locking, which can lead to
|
||||
* {@link javax.persistence.OptimisticLockException} if more than one thread attempts to modify the <b>same</b> component
|
||||
|
@ -62,7 +60,7 @@ import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
|
|||
@Entity
|
||||
@Table(name = "kc_component")
|
||||
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
|
||||
public class JpaComponentEntity extends UpdatableEntity.Impl implements MapComponentEntity, JpaRootVersionedEntity {
|
||||
public class JpaComponentEntity extends UpdatableEntity.Impl implements MapComponentEntity, JpaRootVersionedEntity, JpaChildEntity<JpaRealmEntity> {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
|
@ -100,6 +98,11 @@ public class JpaComponentEntity extends UpdatableEntity.Impl implements MapCompo
|
|||
this.metadata = new JpaComponentMetadata(cloner);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JpaRealmEntity getParent() {
|
||||
return root;
|
||||
}
|
||||
|
||||
public void setParent(JpaRealmEntity root) {
|
||||
this.root = root;
|
||||
}
|
||||
|
|
|
@ -36,7 +36,9 @@ import org.hibernate.annotations.TypeDef;
|
|||
import org.hibernate.annotations.TypeDefs;
|
||||
import org.keycloak.models.map.common.DeepCloner;
|
||||
import org.keycloak.models.map.common.UpdatableEntity;
|
||||
import org.keycloak.models.map.storage.jpa.Constants;
|
||||
import org.keycloak.models.map.storage.jpa.JpaChildEntity;
|
||||
import org.keycloak.models.map.storage.jpa.JpaRootEntity;
|
||||
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
|
||||
import org.keycloak.models.map.user.MapUserConsentEntity;
|
||||
|
||||
|
@ -52,7 +54,7 @@ import org.keycloak.models.map.user.MapUserConsentEntity;
|
|||
@UniqueConstraint(columnNames = {"clientId"})
|
||||
})
|
||||
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
|
||||
public class JpaUserConsentEntity extends UpdatableEntity.Impl implements MapUserConsentEntity, JpaChildEntity<JpaUserEntity> {
|
||||
public class JpaUserConsentEntity extends UpdatableEntity.Impl implements MapUserConsentEntity, JpaRootEntity, JpaChildEntity<JpaUserEntity> {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
|
@ -87,11 +89,13 @@ public class JpaUserConsentEntity extends UpdatableEntity.Impl implements MapUse
|
|||
return metadata != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getEntityVersion() {
|
||||
if (isMetadataInitialized()) return this.metadata.getEntityVersion();
|
||||
return entityVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEntityVersion(Integer version) {
|
||||
this.metadata.setEntityVersion(version);
|
||||
}
|
||||
|
@ -105,6 +109,21 @@ public class JpaUserConsentEntity extends UpdatableEntity.Impl implements MapUse
|
|||
this.root = root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return id == null ? null : id.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setId(String id) {
|
||||
this.id = id == null ? null : UUID.fromString(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getCurrentSchemaVersion() {
|
||||
return Constants.CURRENT_SCHEMA_VERSION_USER_CONSENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientId() {
|
||||
if (isMetadataInitialized()) return this.metadata.getClientId();
|
||||
|
|
|
@ -34,7 +34,9 @@ import org.hibernate.annotations.TypeDef;
|
|||
import org.hibernate.annotations.TypeDefs;
|
||||
import org.keycloak.models.map.common.DeepCloner;
|
||||
import org.keycloak.models.map.common.UpdatableEntity;
|
||||
import org.keycloak.models.map.storage.jpa.Constants;
|
||||
import org.keycloak.models.map.storage.jpa.JpaChildEntity;
|
||||
import org.keycloak.models.map.storage.jpa.JpaRootEntity;
|
||||
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
|
||||
import org.keycloak.models.map.user.MapUserFederatedIdentityEntity;
|
||||
|
||||
|
@ -47,7 +49,7 @@ import org.keycloak.models.map.user.MapUserFederatedIdentityEntity;
|
|||
@Entity
|
||||
@Table(name = "kc_user_federated_identity")
|
||||
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
|
||||
public class JpaUserFederatedIdentityEntity extends UpdatableEntity.Impl implements MapUserFederatedIdentityEntity, JpaChildEntity<JpaUserEntity> {
|
||||
public class JpaUserFederatedIdentityEntity extends UpdatableEntity.Impl implements MapUserFederatedIdentityEntity, JpaRootEntity, JpaChildEntity<JpaUserEntity> {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
|
@ -86,15 +88,22 @@ public class JpaUserFederatedIdentityEntity extends UpdatableEntity.Impl impleme
|
|||
return metadata != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getEntityVersion() {
|
||||
if (isMetadataInitialized()) return this.metadata.getEntityVersion();
|
||||
return entityVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEntityVersion(Integer version) {
|
||||
this.metadata.setEntityVersion(version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getCurrentSchemaVersion() {
|
||||
return Constants.CURRENT_SCHEMA_VERSION_USER_FEDERATED_IDENTITY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JpaUserEntity getParent() {
|
||||
return this.root;
|
||||
|
@ -104,6 +113,16 @@ public class JpaUserFederatedIdentityEntity extends UpdatableEntity.Impl impleme
|
|||
this.root = root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return id == null ? null : id.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setId(String id) {
|
||||
this.id = id == null ? null : UUID.fromString(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToken() {
|
||||
return this.metadata.getToken();
|
||||
|
|
|
@ -39,6 +39,7 @@ import org.hibernate.annotations.TypeDefs;
|
|||
import org.keycloak.models.map.common.DeepCloner;
|
||||
import org.keycloak.models.map.common.UuidValidator;
|
||||
import org.keycloak.models.map.storage.jpa.Constants;
|
||||
import org.keycloak.models.map.storage.jpa.JpaChildEntity;
|
||||
import org.keycloak.models.map.storage.jpa.JpaRootVersionedEntity;
|
||||
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
|
||||
import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity.AbstractAuthenticatedClientSessionEntity;
|
||||
|
@ -49,7 +50,7 @@ import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity.A
|
|||
@Entity
|
||||
@Table(name = "kc_client_session")
|
||||
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
|
||||
public class JpaClientSessionEntity extends AbstractAuthenticatedClientSessionEntity implements JpaRootVersionedEntity {
|
||||
public class JpaClientSessionEntity extends AbstractAuthenticatedClientSessionEntity implements JpaRootVersionedEntity, JpaChildEntity<JpaUserSessionEntity> {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
|
@ -94,6 +95,11 @@ public class JpaClientSessionEntity extends AbstractAuthenticatedClientSessionEn
|
|||
return metadata != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JpaUserSessionEntity getParent() {
|
||||
return root;
|
||||
}
|
||||
|
||||
public void setParent(JpaUserSessionEntity root) {
|
||||
this.root = root;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue