Migrate hibernate from 6.1.7.Final to 7.1.1.Final

Change-Id: Ib5855c283ce6c65401009db8b42e4aae9afa3669
diff --git a/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java b/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java
index 6c68c13..b2c09cf 100644
--- a/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java
+++ b/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java
@@ -165,7 +165,36 @@
         return new HashSet<Role>(resultList);
     }
 
-    public Role retrieveRoleByGroupIdQueryIdPrivilege (int groupId, int queryId,
+    /**
+     * Retrieve all roles associated with a given query id, including their members.
+     */
+    public List<Role> retrieveRolesByQueryIdWithMembers(int queryId) {
+        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
+        CriteriaQuery<Role> cq = cb.createQuery(Role.class);
+
+        Root<Role> role = cq.from(Role.class);
+        role.fetch(Role_.userGroupMembers, JoinType.LEFT);
+        role.fetch(Role_.userGroup, JoinType.INNER);
+        // query is optional for some roles, but we filter roles linked to the query
+        cq.select(role);
+        cq.where(cb.equal(role.get(Role_.query).get(QueryDO_.id), queryId));
+
+        TypedQuery<Role> q = entityManager.createQuery(cq);
+        return q.getResultList();
+    }
+
+    /**
+     * Bulk delete all roles associated with a given query id.
+     */
+    public void deleteRolesByQueryId(int queryId) {
+        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
+        CriteriaDelete<Role> delete = cb.createCriteriaDelete(Role.class);
+        Root<Role> role = delete.from(Role.class);
+        delete.where(cb.equal(role.get(Role_.query).get(QueryDO_.id), queryId));
+        entityManager.createQuery(delete).executeUpdate();
+    }
+
+    public Role retrieveRoleByGroupIdQueryIdPrivilege(int groupId, int queryId,
             PrivilegeType p) throws KustvaktException {
 
         CriteriaBuilder cb = entityManager.getCriteriaBuilder();
diff --git a/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java b/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
index 2545122..8627f1d 100644
--- a/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
+++ b/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
@@ -111,7 +111,23 @@
                     "groupId: " + groupId);
         }
 
-        // EM: this seems weird
+        // Before deleting the group, detach role associations from members to
+        // avoid transient role references during flush
+        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
+        CriteriaQuery<UserGroupMember> cq = cb.createQuery(UserGroupMember.class);
+        Root<UserGroupMember> memberRoot = cq.from(UserGroupMember.class);
+        cq.select(memberRoot);
+        cq.where(cb.equal(memberRoot.get(UserGroupMember_.group).get("id"), groupId));
+        @SuppressWarnings("unchecked")
+        List<UserGroupMember> members = entityManager.createQuery(cq).getResultList();
+        for (UserGroupMember m : members) {
+            if (!entityManager.contains(m)) {
+                m = entityManager.merge(m);
+            }
+            m.setRoles(new HashSet<>());
+            entityManager.merge(m);
+        }
+
         if (!entityManager.contains(group)) {
             group = entityManager.merge(group);
         }
diff --git a/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java b/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
index 86ab9dd..e2da736 100644
--- a/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
+++ b/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
@@ -42,12 +42,38 @@
 
     public void addMember (UserGroupMember member) throws KustvaktException {
         ParameterChecker.checkObjectValue(member, "userGroupMember");
+        if (member.getRoles() != null) {
+            java.util.Set<Role> normalized = new java.util.HashSet<Role>();
+            for (Role role : member.getRoles()) {
+                if (role.getId() == 0) {
+                    entityManager.persist(role);
+                    normalized.add(role);
+                } else {
+                    Role attached = entityManager.contains(role) ? role : entityManager.merge(role);
+                    normalized.add(attached);
+                }
+            }
+            member.setRoles(normalized);
+        }
         entityManager.persist(member);
         entityManager.flush();
     }
 
     public void updateMember (UserGroupMember member) throws KustvaktException {
         ParameterChecker.checkObjectValue(member, "UserGroupMember");
+        if (member.getRoles() != null) {
+            java.util.Set<Role> normalized = new java.util.HashSet<Role>();
+            for (Role role : member.getRoles()) {
+                if (role.getId() == 0) {
+                    entityManager.persist(role);
+                    normalized.add(role);
+                } else {
+                    Role attached = entityManager.contains(role) ? role : entityManager.merge(role);
+                    normalized.add(attached);
+                }
+            }
+            member.setRoles(normalized);
+        }
         entityManager.merge(member);
     }
 
@@ -76,7 +102,7 @@
         Root<UserGroupMember> root = query.from(UserGroupMember.class);
 
         Predicate predicate = criteriaBuilder.and(
-                criteriaBuilder.equal(root.get(UserGroupMember_.group),
+                criteriaBuilder.equal(root.get(UserGroupMember_.group).get("id"),
                         groupId),
                 criteriaBuilder.equal(root.get(UserGroupMember_.userId),
                         userId));
@@ -108,7 +134,7 @@
         Join<UserGroupMember, Role> memberRole = root.join("roles");
 
         Predicate predicate = criteriaBuilder.and(
-                criteriaBuilder.equal(root.get(UserGroupMember_.group),
+                criteriaBuilder.equal(root.get(UserGroupMember_.group).get("id"),
                         groupId),
                 criteriaBuilder.equal(memberRole.get(Role_.NAME), role));
 
@@ -136,7 +162,7 @@
         Root<UserGroupMember> root = query.from(UserGroupMember.class);
 
         Predicate predicate = criteriaBuilder.and(criteriaBuilder
-                .equal(root.get(UserGroupMember_.group), groupId));
+                .equal(root.get(UserGroupMember_.group).get("id"), groupId));
 
         query.select(root);
         query.where(predicate);
diff --git a/src/main/java/de/ids_mannheim/korap/entity/UserGroupMember.java b/src/main/java/de/ids_mannheim/korap/entity/UserGroupMember.java
index d5d54ae..30f690c 100644
--- a/src/main/java/de/ids_mannheim/korap/entity/UserGroupMember.java
+++ b/src/main/java/de/ids_mannheim/korap/entity/UserGroupMember.java
@@ -4,6 +4,7 @@
 
 import de.ids_mannheim.korap.constant.PredefinedRole;
 import jakarta.persistence.Column;
+import jakarta.persistence.CascadeType;
 import jakarta.persistence.Entity;
 import jakarta.persistence.FetchType;
 import jakarta.persistence.GeneratedValue;
@@ -50,7 +51,7 @@
      * describe a member.
      * 
      */
-    @ManyToMany(fetch = FetchType.EAGER)
+    @ManyToMany(fetch = FetchType.EAGER, cascade = { CascadeType.MERGE })
     @JoinTable(name = "group_member_role", joinColumns = @JoinColumn(name = "group_member_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"), uniqueConstraints = @UniqueConstraint(columnNames = {
             "group_member_id", "role_id" }))
     private Set<Role> roles;
diff --git a/src/main/java/de/ids_mannheim/korap/service/QueryService.java b/src/main/java/de/ids_mannheim/korap/service/QueryService.java
index e3b0ce6..f329ab0 100644
--- a/src/main/java/de/ids_mannheim/korap/service/QueryService.java
+++ b/src/main/java/de/ids_mannheim/korap/service/QueryService.java
@@ -178,10 +178,29 @@
         else if (query.getCreatedBy().equals(deletedBy)
                 || adminDao.isAdmin(deletedBy)) {
 
-            if (query.getType().equals(ResourceType.PUBLISHED)) {
-                UserGroup group = userGroupDao
-                        .retrieveHiddenGroupByQueryName(queryName);
-                userGroupDao.deleteGroup(group.getId(), deletedBy);
+            // If published, fetch the hidden group BEFORE deleting roles, so we can remove
+            // it later
+            UserGroup hiddenGroup = null;
+            boolean isPublished = query.getType().equals(ResourceType.PUBLISHED);
+            if (isPublished) {
+                hiddenGroup = userGroupDao.retrieveHiddenGroupByQueryName(queryName);
+            }
+
+            // Detach member-role links and delete all roles linked to the query
+            List<Role> queryRoles = roleDao.retrieveRolesByQueryIdWithMembers(query.getId());
+            for (Role role : queryRoles) {
+                if (role.getUserGroupMembers() != null) {
+                    for (UserGroupMember m : role.getUserGroupMembers()) {
+                        if (m.getRoles() != null && m.getRoles().remove(role)) {
+                            memberDao.updateMember(m);
+                        }
+                    }
+                }
+            }
+            roleDao.deleteRolesByQueryId(query.getId());
+
+            if (isPublished && hiddenGroup != null) {
+                userGroupDao.deleteGroup(hiddenGroup.getId(), deletedBy);
             }
             if (type.equals(QueryType.VIRTUAL_CORPUS)
                     && VirtualCorpusCache.contains(queryName)) {
diff --git a/src/main/resources/default-config.xml b/src/main/resources/default-config.xml
index a4a30d9..c82010f 100644
--- a/src/main/resources/default-config.xml
+++ b/src/main/resources/default-config.xml
@@ -125,9 +125,7 @@
 				<prop key="hibernate.show_sql">${hibernate.show_sql}</prop>
 				<prop key="hibernate.cache.use_query_cache">${hibernate.cache.use_query_cache}</prop>
 				<prop key="hibernate.cache.use_second_level_cache">${hibernate.cache.use_second_level_cache}
-				</prop>
-				<prop key="hibernate.cache.provider_class">${hibernate.cache.provider}</prop>
-				<prop key="hibernate.cache.region.factory_class">${hibernate.cache.region.factory}</prop>
+				</prop>				
 				<prop key="hibernate.jdbc.time_zone">${hibernate.jdbc.time_zone}</prop>
 			</props>
 		</property>
diff --git a/src/main/resources/properties/hibernate.properties b/src/main/resources/properties/hibernate.properties
index e394a88..bd073e5 100644
--- a/src/main/resources/properties/hibernate.properties
+++ b/src/main/resources/properties/hibernate.properties
@@ -3,6 +3,7 @@
 hibernate.show_sql=false
 hibernate.cache.use_query_cache=false
 hibernate.cache.use_second_level_cache=false
-hibernate.cache.provider=org.hibernate.cache.EhCacheProvider
-hibernate.cache.region.factory=org.hibernate.cache.ehcache.EhCacheRegionFactory
-hibernate.jdbc.time_zone=UTC
\ No newline at end of file
+hibernate.jdbc.time_zone=UTC
+# Ehcache 2 integration removed; disable legacy cache settings
+hibernate.cache.provider=
+hibernate.cache.region.factory=