DS-2086 fix. Corrects the logic to deleting Communities & Collections such that hierarchical deletion test now succeeds. Also corrects minor permissions error in a different unit test.

This commit is contained in:
Tim Donohue
2014-08-15 13:48:03 -05:00
parent 56fda36375
commit 0aa39100a9
2 changed files with 145 additions and 101 deletions

View File

@@ -994,10 +994,15 @@ public class Community extends DSpaceObject
} }
/** /**
* Remove a collection. Any items then orphaned are deleted. * Remove a collection. If it only belongs to one parent community,
* then it is permanently deleted. If it has more than one parent community,
* it is simply unmapped from the current community.
* *
* @param c * @param c
* collection to remove * collection to remove
* @throws SQLException
* @throws AuthorizeException
* @throws IOException
*/ */
public void removeCollection(Collection c) throws SQLException, public void removeCollection(Collection c) throws SQLException,
AuthorizeException, IOException AuthorizeException, IOException
@@ -1005,149 +1010,185 @@ public class Community extends DSpaceObject
// Check authorisation // Check authorisation
AuthorizeManager.authorizeAction(ourContext, this, Constants.REMOVE); AuthorizeManager.authorizeAction(ourContext, this, Constants.REMOVE);
// will be the collection an orphan? // Do the removal in a try/catch, so that we can rollback on error
TableRow trow = DatabaseManager.querySingle(ourContext, try
"SELECT COUNT(DISTINCT community_id) AS num FROM community2collection WHERE collection_id= ? ",
c.getID());
DatabaseManager.setConstraintDeferred(ourContext, "comm2coll_collection_fk");
if (trow.getLongColumn("num") == 1)
{ {
// Orphan; delete it // Capture ID & Handle of Collection we are removing, so we can trigger events later
c.delete(); int removedId = c.getID();
} String removedHandle = c.getHandle();
log.info(LogManager.getHeader(ourContext, "remove_collection",
"community_id=" + getID() + ",collection_id=" + c.getID()));
// Remove any mappings
DatabaseManager.updateQuery(ourContext,
"DELETE FROM community2collection WHERE community_id= ? "+
"AND collection_id= ? ", getID(), c.getID());
DatabaseManager.setConstraintImmediate(ourContext, "comm2coll_collection_fk"); // How many parent(s) does this collection have?
TableRow trow = DatabaseManager.querySingle(ourContext,
"SELECT COUNT(DISTINCT community_id) AS num FROM community2collection WHERE collection_id= ? ",
c.getID());
long numParents = trow.getLongColumn("num");
// Remove the parent/child mapping with this collection
// We do this before deletion, so that the deletion doesn't throw database integrity violations
DatabaseManager.updateQuery(ourContext,
"DELETE FROM community2collection WHERE community_id= ? "+
"AND collection_id= ? ", getID(), c.getID());
// As long as this Collection only had one parent, delete it
// NOTE: if it had multiple parents, we will keep it around,
// and just remove that single parent/child mapping
if (numParents == 1)
{
c.delete();
}
ourContext.addEvent(new Event(Event.REMOVE, Constants.COMMUNITY, getID(), Constants.COLLECTION, c.getID(), c.getHandle())); // log the removal & trigger any associated event(s)
log.info(LogManager.getHeader(ourContext, "remove_collection",
"community_id=" + getID() + ",collection_id=" + removedId));
ourContext.addEvent(new Event(Event.REMOVE, Constants.COMMUNITY, getID(), Constants.COLLECTION, removedId, removedHandle));
}
catch(SQLException|IOException e)
{
// Immediately abort the deletion, rolling back the transaction
ourContext.abort();
// Pass exception upwards for additional reporting/handling as needed
throw e;
}
} }
/** /**
* Remove a subcommunity. Any substructure then orphaned is deleted. * Remove a subcommunity. If it only belongs to one parent community,
* then it is permanently deleted. If it has more than one parent community,
* it is simply unmapped from the current community.
* *
* @param c * @param c
* subcommunity to remove * subcommunity to remove
* @throws SQLException
* @throws AuthorizeException
* @throws IOException
*/ */
public void removeSubcommunity(Community c) throws SQLException, public void removeSubcommunity(Community c) throws SQLException,
AuthorizeException, IOException AuthorizeException, IOException
{ {
// Check authorisation // Check authorisation.
AuthorizeManager.authorizeAction(ourContext, this, Constants.REMOVE); AuthorizeManager.authorizeAction(ourContext, this, Constants.REMOVE);
// will be the subcommunity an orphan? // Do the removal in a try/catch, so that we can rollback on error
TableRow trow = DatabaseManager.querySingle(ourContext, try
"SELECT COUNT(DISTINCT parent_comm_id) AS num FROM community2community WHERE child_comm_id= ? ",
c.getID());
DatabaseManager.setConstraintDeferred(ourContext, "com2com_child_fk");
if (trow.getLongColumn("num") == 1)
{ {
// Orphan; delete it // Capture ID & Handle of Community we are removing, so we can trigger events later
c.rawDelete(); int removedId = c.getID();
String removedHandle = c.getHandle();
// How many parent(s) does this subcommunity have?
TableRow trow = DatabaseManager.querySingle(ourContext,
"SELECT COUNT(DISTINCT parent_comm_id) AS num FROM community2community WHERE child_comm_id= ? ",
c.getID());
long numParents = trow.getLongColumn("num");
// Remove the parent/child mapping with this subcommunity
// We do this before deletion, so that the deletion doesn't throw database integrity violations
DatabaseManager.updateQuery(ourContext,
"DELETE FROM community2community WHERE parent_comm_id= ? " +
" AND child_comm_id= ? ", getID(),c.getID());
// As long as this Community only had one parent, delete it
// NOTE: if it had multiple parents, we will keep it around,
// and just remove that single parent/child mapping
if (numParents == 1)
{
c.rawDelete();
}
// log the removal & trigger any related event(s)
log.info(LogManager.getHeader(ourContext, "remove_subcommunity",
"parent_comm_id=" + getID() + ",child_comm_id=" + removedId));
ourContext.addEvent(new Event(Event.REMOVE, Constants.COMMUNITY, getID(), Constants.COMMUNITY, removedId, removedHandle));
}
catch(SQLException|IOException e)
{
// Immediately abort the deletion, rolling back the transaction
ourContext.abort();
// Pass exception upwards for additional reporting/handling as needed
throw e;
} }
log.info(LogManager.getHeader(ourContext, "remove_subcommunity",
"parent_comm_id=" + getID() + ",child_comm_id=" + c.getID()));
// Remove any mappings
DatabaseManager.updateQuery(ourContext,
"DELETE FROM community2community WHERE parent_comm_id= ? " +
" AND child_comm_id= ? ", getID(),c.getID());
ourContext.addEvent(new Event(Event.REMOVE, Constants.COMMUNITY, getID(), Constants.COMMUNITY, c.getID(), c.getHandle()));
DatabaseManager.setConstraintImmediate(ourContext, "com2com_child_fk");
} }
/** /**
* Delete the community, including the metadata and logo. Collections and * Delete the community, including the metadata and logo. Any children
* subcommunities that are then orphans are deleted. * (subcommunities or collections) are also deleted.
*
* @throws SQLException
* @throws AuthorizeException
* @throws IOException
*/ */
public void delete() throws SQLException, AuthorizeException, IOException public void delete() throws SQLException, AuthorizeException, IOException
{ {
// Check authorisation // Check for a parent Community
// FIXME: If this was a subcommunity, it is first removed from it's Community parent = getParentCommunity();
// parent.
// This means the parentCommunity == null // Check authorisation.
// But since this is also the case for top-level communities, we would // MUST have either REMOVE permissions on parent community (if exists)
// give everyone rights to remove the top-level communities. // OR have DELETE permissions on current community
// The same problem occurs in removing the logo if (parent!= null && !AuthorizeManager.authorizeActionBoolean(ourContext,
if (!AuthorizeManager.authorizeActionBoolean(ourContext,
getParentCommunity(), Constants.REMOVE)) getParentCommunity(), Constants.REMOVE))
{ {
// If we don't have Parent Community REMOVE permissions, then
// we MUST at least have current Community DELETE permissions
AuthorizeManager AuthorizeManager
.authorizeAction(ourContext, this, Constants.DELETE); .authorizeAction(ourContext, this, Constants.DELETE);
} }
// If not a top-level community, have parent remove me; this // Check if this is a top-level Community or not
// will call rawDelete() before removing the linkage
Community parent = getParentCommunity();
if (parent == null) if (parent == null)
{ {
// if removing a top level Community, simulate a REMOVE event at the Site. // Call rawDelete to clean up all sub-communities & collections
if (getParentCommunity() == null) // under this Community, then delete the Community itself
{ rawDelete();
ourContext.addEvent(new Event(Event.REMOVE, Constants.SITE, Site.SITE_ID,
Constants.COMMUNITY, getID(), getHandle())); // Since this is a top level Community, simulate a REMOVE event at the Site.
} ourContext.addEvent(new Event(Event.REMOVE, Constants.SITE, Site.SITE_ID,
Constants.COMMUNITY, getID(), getHandle()));
} else { } else {
// remove the subcommunities first // This is a subcommunity, so let the parent remove it
Community[] subcommunities = getSubcommunities(); // NOTE: this essentially just logs event and calls "rawDelete()"
for (int i = 0; i < subcommunities.length; i++)
{
subcommunities[i].delete();
}
// now let the parent remove the community
parent.removeSubcommunity(this); parent.removeSubcommunity(this);
return;
} }
rawDelete();
} }
/** /**
* Internal method to remove the community and all its childs from the database without aware of eventually parent * Internal method to remove the community and all its children from the
* database, and perform any pre/post-cleanup
*
* @throws SQLException
* @throws AuthorizeException
* @throws IOException
*/ */
private void rawDelete() throws SQLException, AuthorizeException, IOException private void rawDelete() throws SQLException, AuthorizeException, IOException
{ {
log.info(LogManager.getHeader(ourContext, "delete_community", log.info(LogManager.getHeader(ourContext, "delete_community",
"community_id=" + getID())); "community_id=" + getID()));
ourContext.addEvent(new Event(Event.DELETE, Constants.COMMUNITY, getID(), getHandle())); // Capture ID & Handle of object we are removing, so we can trigger events later
int deletedId = getID();
String deletedHandle = getHandle();
// Remove from cache // Remove Community object from cache
ourContext.removeCached(this, getID()); ourContext.removeCached(this, getID());
// Remove collections // Remove any collections directly under this Community
Collection[] cols = getCollections(); for (Collection collection : getCollections())
for (int i = 0; i < cols.length; i++)
{ {
removeCollection(cols[i]); removeCollection(collection);
} }
// delete subcommunities // Remove any SubCommunities under this Community
Community[] comms = getSubcommunities(); for (Community community : getSubcommunities())
for (int j = 0; j < comms.length; j++)
{ {
comms[j].delete(); removeSubcommunity(community);
} }
// Remove the logo // Remove the Community logo
setLogo(null); setLogo(null);
// Remove all authorization policies // Remove all associated authorization policies
AuthorizeManager.removeAllPolicies(ourContext, this); AuthorizeManager.removeAllPolicies(ourContext, this);
// get rid of the content count cache if it exists // get rid of the content count cache if it exists
@@ -1163,19 +1204,22 @@ public class Community extends DSpaceObject
throw new IllegalStateException(e.getMessage(),e); throw new IllegalStateException(e.getMessage(),e);
} }
// Remove any Handle // Unbind any handle associated with this Community
HandleManager.unbindHandle(ourContext, this); HandleManager.unbindHandle(ourContext, this);
// Delete community row // Delete community row (which actually removes the Community)
DatabaseManager.delete(ourContext, communityRow); DatabaseManager.delete(ourContext, communityRow);
// Remove administrators group - must happen after deleting community // Remove Community administrators group (if exists)
// NOTE: this must happen AFTER deleting community
Group g = getAdministrators(); Group g = getAdministrators();
if (g != null) if (g != null)
{ {
g.delete(); g.delete();
} }
// If everything above worked, then trigger any associated events
ourContext.addEvent(new Event(Event.DELETE, Constants.COMMUNITY, deletedId, deletedHandle));
} }
/** /**

View File

@@ -1027,15 +1027,15 @@ public class CommunityTest extends AbstractDSpaceObjectTest
{ {
new NonStrictExpectations(AuthorizeManager.class) new NonStrictExpectations(AuthorizeManager.class)
{{ {{
// Allow current Community ADD perms // Allow Community ADD perms (in order to add a new subcommunity to parent)
AuthorizeManager.authorizeAction((Context) any, (Community) any, AuthorizeManager.authorizeAction((Context) any, (Community) any,
Constants.ADD); result = null; Constants.ADD, true); result = null;
// Allow *parent* Community ADD perms // Allow Community REMOVE perms (needed to unmap/remove subcommunity)
AuthorizeManager.authorizeActionBoolean((Context) any, (Community) any,
Constants.ADD); result = true;
// Allow current Community REMOVE perms
AuthorizeManager.authorizeAction((Context) any, (Community) any, AuthorizeManager.authorizeAction((Context) any, (Community) any,
Constants.REMOVE); result = null; Constants.REMOVE, true); result = null;
// Allow Community DELETE perms (needed to actually delete subcommunity)
AuthorizeManager.authorizeAction((Context) any, (Community) any,
Constants.DELETE, true); result = null;
}}; }};
// Turn off authorization temporarily to create a new top-level community // Turn off authorization temporarily to create a new top-level community