Index: src/org/dspace/browse/Browse.java =================================================================== --- src/org/dspace/browse/Browse.java (revision 4884) +++ src/org/dspace/browse/Browse.java (revision 5020) @@ -59,11 +59,15 @@ import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.DCValue; +import org.dspace.content.DSpaceObject; import org.dspace.content.Item; import org.dspace.content.ItemComparator; import org.dspace.content.ItemIterator; import org.dspace.core.ConfigurationManager; import org.dspace.core.Context; +import org.dspace.core.PluginManager; +import org.dspace.mask.MaskType; +import org.dspace.mask.MaskService; import org.dspace.storage.rdbms.DatabaseManager; import org.dspace.storage.rdbms.TableRow; @@ -485,14 +489,24 @@ public static void itemAdded(Context context, Item item) throws SQLException { + log.info( "Adding item " + item ); + + if ( isMasked( item ) ) + { + log.info( "Item " + item.getHandle() + " is masked from browse." ); + return; + } + // add all parent communities to communities2item table - Community[] parents = item.getCommunities(); + Community[] parents = item.getCommunities(); for (int j = 0; j < parents.length; j++) { + Community community = parents[j]; + TableRow row = DatabaseManager.create(context, "Communities2Item"); row.setColumn("item_id", item.getID()); - row.setColumn("community_id", parents[j].getID()); + row.setColumn("community_id", community.getID()); DatabaseManager.update(context, row); } @@ -586,6 +600,23 @@ } /** + * Check to see if the Item is masked from being browsable. + *

+ * If the item itelf or its owning collection are masked, return true. + * If all communities the item belongs to are masked, return true. + * Otherwise return false. + * + * @param item the dspace item + * @return true if the object should be masked from browse + */ + private static boolean isMasked( Item item ) throws SQLException + { + MaskService maskSvc = (MaskService)PluginManager.getSinglePlugin( MaskService.class ); + String itemHandle = item.getHandle(); + return maskSvc.isMasked( itemHandle, MaskType.BROWSE ); + } + + /** * Index all items in DSpace. This method may be resource-intensive. * * @param context @@ -595,12 +626,13 @@ * If a database error occurs */ public static int indexAll(Context context) throws SQLException - { + { indexRemoveAll(context); int count = 0; ItemIterator iterator = Item.findAll(context); + while (iterator.hasNext()) { itemAdded(context, iterator.next()); Index: src/org/dspace/browse/InitializeBrowse.java =================================================================== --- src/org/dspace/browse/InitializeBrowse.java (revision 4884) +++ src/org/dspace/browse/InitializeBrowse.java (revision 5020) @@ -41,6 +41,8 @@ import java.sql.SQLException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.dspace.core.Context; /** @@ -51,6 +53,8 @@ */ public class InitializeBrowse { + private static Log log = LogFactory.getLog( InitializeBrowse.class ); + /** Private Constructor */ private InitializeBrowse() { @@ -68,13 +72,13 @@ try { - System.out.print("Indexing all Items in DSpace...."); + log.info("Indexing all Items in DSpace...."); context = new Context(); Browse.indexAll(context); context.complete(); - System.out.println(" ... Done"); + log.info(" ... Done"); } catch (SQLException sqle) { @@ -83,7 +87,7 @@ context.abort(); } - System.err.println("Error: Browse index NOT created"); + log.error("Error: Browse index NOT created", sqle); sqle.printStackTrace(); } } Index: src/org/dspace/search/DSIndexer.java =================================================================== --- src/org/dspace/search/DSIndexer.java (revision 4884) +++ src/org/dspace/search/DSIndexer.java (revision 5020) @@ -74,7 +74,10 @@ import org.dspace.core.Context; import org.dspace.core.Email; import org.dspace.core.LogManager; +import org.dspace.core.PluginManager; import org.dspace.handle.HandleManager; +import org.dspace.mask.MaskType; +import org.dspace.mask.MaskService; import edu.jhmi.welch.dspace.DspaceSelector; @@ -556,6 +559,14 @@ for(ItemIterator i = Item.findAll(context);i.hasNext();) { Item item = (Item) i.next(); + if ( isMasked( item ) ) + { + if ( log.isInfoEnabled() ) + { + log.info( "Item [" + item.getHandle() + "] is masked from search indexing." ); + } + continue; + } indexContent(context,item,force); item.decache(); } @@ -563,6 +574,14 @@ Collection[] collections = Collection.findAll(context); for (int i = 0; i < collections.length; i++) { + if ( isMasked( collections[i] ) ) + { + if ( log.isInfoEnabled() ) + { + log.info( "Collection [" + collections[i].getHandle() + "] is masked from search indexing." ); + } + continue; + } indexContent(context,collections[i],force); context.removeCached(collections[i], collections[i].getID()); @@ -571,6 +590,14 @@ Community[] communities = Community.findAll(context); for (int i = 0; i < communities.length; i++) { + if ( isMasked( communities[i] ) ) + { + if ( log.isInfoEnabled() ) + { + log.info( "Community [" + communities[i].getHandle() + "] is masked from search indexing." ); + } + continue; + } indexContent(context,communities[i],force); context.removeCached(communities[i], communities[i].getID()); } @@ -1122,5 +1149,12 @@ return doc; } + + private static boolean isMasked( DSpaceObject dso ) throws SQLException + { + MaskService maskSvc = (MaskService)PluginManager.getSinglePlugin( MaskService.class ); + String handle = dso.getHandle(); + return maskSvc.isMasked( handle, MaskType.INDEX ); + } } Index: src/org/dspace/mask/PropertyMaskService.java =================================================================== --- src/org/dspace/mask/PropertyMaskService.java (revision 0) +++ src/org/dspace/mask/PropertyMaskService.java (revision 5020) @@ -0,0 +1,316 @@ +package org.dspace.mask; + +import java.sql.SQLException; +import java.util.BitSet; +import java.util.StringTokenizer; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.core.ConfigurationManager; +import org.dspace.core.Context; +import org.dspace.handle.HandleManager; + +/** + * A {@link MaskService} which is initialized by + * properties from the {@link ConfigurationManager}. + */ +public class PropertyMaskService implements MaskService +{ + private static final String TOKEN = ","; + private Log log = LogFactory.getLog( PropertyMaskService.class ); + private final InMemoryMaskManager mgr; + + public PropertyMaskService() + { + this( new InMemoryMaskManager() ); + } + + /** + * TODO: Instead of InMemoryMaskManager, should be MaskManager. Or update MaskManager contract. + * @param mgr + */ + public PropertyMaskService( InMemoryMaskManager mgr ) + { + if ( mgr == null ) + { + throw new IllegalArgumentException( "Mask Manager cannot be null" ); + } + this.mgr = mgr; + + log.info( "Configuring the Property based Mask Service..." ); + + // Get properties + String browse = ConfigurationManager.getProperty( "mask.browse" ); + if ( browse != null && !browse.trim().equals( "" ) ) + { + addMask( browse.trim(), MaskType.BROWSE ); + } + + String index = ConfigurationManager.getProperty( "mask.index" ); + if ( index != null && !browse.trim().equals( "" ) ) + { + addMask( index.trim(), MaskType.INDEX ); + } + + String harvest = ConfigurationManager.getProperty( "mask.harvest" ); + if ( harvest != null && !harvest.trim().equals( "" ) ) + { + addMask( harvest.trim(), MaskType.HARVEST ); + } + + log.info( "Property based Mask Service configuration complete." ); + } + + /** + * TODO: Extract MaskType handlers. Handlers should be independent of the mask service itself. + * TODO: Negative cache. If an item isn't masked, that should be cached for a certain period. + */ + public boolean isMasked( String handle, MaskType type ) + { + // The handle may be null for Item objects which have + // just been submitted. + if ( handle == null || handle.trim().equals( "" ) ) + { + // assume false + return false; + } + + // Check the MaskManager: it may already know that + // the handle is masked. + if ( isMaskedInternal( handle, type ) ) + { + return true; + } + + // If the MaskManager didn't know the handle was + // masked, it could be that the handle is an + // Item which is a member of a masked collection + // or community. + + // assume that the item is masked. + boolean isMasked = true; + // temp context for HandleManager + Context tempContext = null; + try + { + tempContext = new Context(); + DSpaceObject dso = HandleManager.resolveToObject( tempContext, handle ); + + // if the handle doesn't resolve to an object, return false + if ( dso == null ) + { + return false; + } + + + switch ( type ) + { + case BROWSE: + { + isMasked = handleBrowse( dso ); + break; + } + + case INDEX: + { + isMasked = handleIndex( dso ); + break; + } + + case HARVEST: + { + isMasked = handleHarvest( dso ); + break; + } + default: + { + log.warn( "Unknown mask type: [" + type + "]. Assuming handle is not masked." ); + isMasked = false; + break; + } + } + + // Don't forget to clean up + tempContext.complete(); + } + catch ( SQLException e ) + { + isMasked = false; + log.error( "Unable to determine if handle [" + handle + "] is masked from [" + type + "]: " + + e.getMessage(), e ); + } + finally + { + if ( tempContext != null && tempContext.isValid() ) + { + tempContext.abort(); + } + } + + return isMasked; + } + + private boolean handleBrowse( DSpaceObject dso ) throws SQLException + { + Item i = null; + if ( dso instanceof Item ) + { + i = (Item)dso; + } + else + { + if ( log.isDebugEnabled() ) + { + log.debug( "Can only check Item masks. Assuming this DSO is not masked. Received a DSO that was not an item: " + + "Type: [" + dso.getType() + "] Handle: [" + dso.getHandle() + "] ID: [" + dso.getID() + "]" ); + } + return false; + } + + return isItemMasked( i, MaskType.BROWSE ); + } + + private boolean handleIndex( DSpaceObject dso ) throws SQLException + { + Item i = null; + if ( dso instanceof Item ) + { + i = (Item)dso; + } + else + { + if ( log.isDebugEnabled() ) + { + log.debug( "Can only check Item masks. Assuming this DSO is not masked. Received a DSO that was not an item: " + + "Type: [" + dso.getType() + "] Handle: [" + dso.getHandle() + "] ID: [" + dso.getID() + "]" ); + } + return false; + } + + return isItemMasked( i, MaskType.BROWSE ); + } + + private boolean handleHarvest( DSpaceObject dso ) throws SQLException + { + Item i = null; + if ( dso instanceof Item ) + { + i = (Item)dso; + } + else + { + if ( log.isDebugEnabled() ) + { + log.debug( "Can only check Item masks. Assuming this DSO is not masked. Received a DSO that was not an item: " + + "Type: [" + dso.getType() + "] Handle: [" + dso.getHandle() + "] ID: [" + dso.getID() + "]" ); + } + return false; + } + + return isItemMasked( i, MaskType.BROWSE ); + } + + /** + * Add the supplied handles and mask type to the + * MaskManager. + *

+ * handles may be a {@link #TOKEN} separated + * String of handles or just a single handle. + *

+ * + * @param handles a single handle or a {@link #TOKEN} separated + * list of handles. + * @param type the mask type for the handles + */ + private void addMask( String handles, MaskType type ) + { + StringTokenizer st = new StringTokenizer( handles, TOKEN ); + while ( st.hasMoreTokens() ) + { + String handle = st.nextToken().trim(); + log.info( "Masking handle [" + handle + "] from [" + type + "]" ); + mgr.addMask( handle, type ); + } + } + + /** + * Checks with the MaskManager for a mask type for + * handle. + *

+ * If the MaskManager knows that handle is masked, + * it will return a BitSet of mask flags. This method + * checks to see if the flag for type is set. + * + * @param handle the DSpace object handle + * @param type the type of masking + * @return true if handle is masked for type + */ + private boolean isMaskedInternal( String handle, MaskType type ) + { + BitSet mask = mgr.getMask( handle ); + + if ( mgr.getBitIndex( type ) != -1 && mask != null ) + { + if ( mask.get( mgr.getBitIndex( type ) ) ) + { + return true; + } + } + + return false; + } + + /** + * Check to see if a DSpace Item is masked. + *

+ * An Item is masked if one of the following + * conditions is true: + *

+ *

    + *
  1. The MaskManager asserts the Item is masked
  2. + *
  3. The Item's owning collection is masked, or ALL of the Item's parent + * communities are masked.
  4. + *
+ *

+ * @param i + * @param type + * @return + * @throws SQLException + */ + private boolean isItemMasked( Item i, MaskType type ) throws SQLException + { + // Check to see if the Item's owning collection + // is masked. If so, then the Item is masked. + Collection owningC = i.getOwningCollection(); + if ( isMaskedInternal( owningC.getHandle(), type ) ) + { + mgr.addMask( i.getHandle(), type ); + return true; + } + + // Check to see if *all* of the Item's parent communities + // are masked. If so, then the Item is masked. + Community[] communities = i.getCommunities(); + boolean communityMask = true; + for ( Community comm : communities ) + { + if (! isMaskedInternal( comm.getHandle(), type ) ) + { + communityMask = false; + break; + } + } + + if ( communityMask ) + { + mgr.addMask( i.getHandle(), type ); + return true; + } + + return false; + } +} Property changes on: src/org/dspace/mask/PropertyMaskService.java ___________________________________________________________________ Name: svn:mime-type + text/plain Name: svn:keywords + Id Name: svn:eol-style + native Index: src/org/dspace/mask/InMemoryMaskManager.java =================================================================== --- src/org/dspace/mask/InMemoryMaskManager.java (revision 0) +++ src/org/dspace/mask/InMemoryMaskManager.java (revision 5020) @@ -0,0 +1,260 @@ +package org.dspace.mask; + +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This {@link MaskManager} implementation maintains a Map + * of DSpace handles to masks. The map is not persisted in any way, + * so it must be seeded by calling {@link #addMask(String, MaskType)} + * each time this object is instantiated. This implementation is + * designed to be threadsafe. It can be shared by multiple {@link MaskService} + * instances. + * + * @see MaskService + */ +class InMemoryMaskManager implements MaskManager +{ + /** + * Logger + */ + private Log log = LogFactory.getLog( InMemoryMaskManager.class ); + + /** + * Maps DSpace object handles to thier mask information. + * This Map is meant to serve as a cache. + *

+ * This map is shared amongst all instances of this class. + * The map is mutable. Access to the map is threadsafe, note + * however that update operations like put and + * set may interleave retrieval operations like + * get per the {@link ConcurrentHashMap} contract. + * Interleaving retrievals and updates is a reasonable sacrifice + * for reducing lock contention and synchronization overhead. + */ + private static ConcurrentMap masks = new ConcurrentHashMap(); + + /** + * Maps {@link MaskType}s to bit indexes (0-based). + *

+ * This map is shared amongst all instances of this class. However, + * in this implementation it is only mutated on Class instantion, so + * it is not synchronized and is unmodifiable. + * + * TODO: Hard-code the bit index to MaskType for readability? + */ + private static Map typeToBit; + static + { + typeToBit = new HashMap( MaskType.values().length ); + Iterator maskItr = Arrays.asList( MaskType.values() ).iterator(); + int bit = 0; + while ( maskItr.hasNext() ) + { + typeToBit.put( maskItr.next(), bit++ ); + } + typeToBit = Collections.unmodifiableMap( typeToBit ); + } + + /** + * No-arg package-private constructor. This implementation shouldn't be exposed. + */ + InMemoryMaskManager() + { + log.debug( "Constructed " + this.getClass().getName() + "@" + Integer.toHexString( this.hashCode() ) ); + } + + public synchronized void addMask( String handle, MaskType type ) + { + DspaceObjectMask mask = masks.get( handle ); + if ( mask != null ) + { + log.debug( "Updating mask for " + handle + ": Adding mask " + type ); + mask.mask.set( typeToBit.get( type ) ); + } + else + { + BitSet bitSet = new BitSet( typeToBit.size() ); + bitSet.set( typeToBit.get( type ) ); + putNewMask( handle, bitSet ); + } + } + + public synchronized void mask( String handle ) + { + DspaceObjectMask mask = masks.get( handle ); + if ( mask != null ) + { + log.debug( "Updating mask for " + handle + ": enabling all masks." ); + mask.mask.set( 0, mask.mask.size() - 1 ); + } + else + { + BitSet bits = new BitSet( typeToBit.size() ); + bits.set( 0, bits.size() - 1 ); + putNewMask( handle, bits ); + } + } + + public synchronized void removeMask( String handle, MaskType type ) + { + DspaceObjectMask mask = masks.get( handle ); + log.debug( "Clearing mask " + type + " from " + handle ); + if ( mask != null ) + { + mask.mask.clear( typeToBit.get( type ) ); + if ( mask.mask.cardinality() == 0 ) + { + masks.remove( handle ); + } + } + } + + /** + * Implementation note: "unmask" means remove it from the + * 'masks' map. + */ + public synchronized void unmask( String handle ) + { + log.debug( "Clearing all masks from " + handle ); + masks.remove( handle ); + } + + /** + * Obtain the bitset representing the {@link MaskType}s + * in effect for this handle. Mutating the bitset + * has no effect. Use the {@link MaskManager} methods + * to mutate masks. + * + * @param handle the DSpace object handle + * @return + */ + BitSet getMask( String handle ) + { + log.debug( "Mask cache contains " + masks.size() + " handles" ); + DspaceObjectMask dsoMask = masks.get( handle ); + if ( dsoMask != null ) + { + // Note that if another thread concurrently + // modifies the DspaceObjectMask's bitset, + // the updated bitset - representing its masks - + // may not be visible to this thread. + // + // This is acceptable to avoid synchronization + // overhead. + return (BitSet)dsoMask.mask.clone(); + } + + return null; + } + + /** + * Returns the bit index corresponding to the supplied + * type. + * + * @param type the {@link MaskType} + * @return the bit index of the type, or -1 if no mapping exists. + */ + int getBitIndex( MaskType type ) + { + if (! typeToBit.containsKey( type ) ) + { + return -1; + } + + return typeToBit.get( type ); + } + + private synchronized void putNewMask( String handle, BitSet maskBits ) + { + log.debug( "Adding new mask for " + handle ); + DspaceObjectMask dsMask = new DspaceObjectMask( handle, maskBits ); + masks.put( handle, dsMask ); + } + + /** + * Encapsulates all the masks of a particular handle. References to member + * objects should never be handed out. + */ + private class DspaceObjectMask + { + final String handle; + final BitSet mask; + + private DspaceObjectMask( String handle ) + { + if ( handle == null || handle.trim().equals( "" ) ) + { + throw new IllegalArgumentException( "Handle must not be empty or null." ); + } + this.handle = handle; + this.mask = new BitSet( MaskType.values().length ); + } + + private DspaceObjectMask( String handle, BitSet mask ) + { + if ( handle == null || handle.trim().equals( "" ) ) + { + throw new IllegalArgumentException( "Handle must not be empty or null." ); + } + + if ( mask == null ) + { + throw new IllegalArgumentException( "Mask must not be null." ); + } + this.handle = handle; + this.mask = mask; + } + + public int hashCode() + { + final int PRIME = 31; + int result = 1; + result = PRIME * result + ( ( handle == null ) ? 0 : handle.hashCode() ); + result = PRIME * result + ( ( mask == null ) ? 0 : mask.hashCode() ); + return result; + } + + public boolean equals( Object obj ) + { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + final DspaceObjectMask other = (DspaceObjectMask) obj; + if ( handle == null ) + { + if ( other.handle != null ) + return false; + } + else if ( !handle.equals( other.handle ) ) + return false; + if ( mask == null ) + { + if ( other.mask != null ) + return false; + } + else if ( !mask.equals( other.mask ) ) + return false; + return true; + } + + public String toString() + { + return "Mask handle: [" + handle + "] mask: [" + mask.toString() + "]"; + } + + } + +} Property changes on: src/org/dspace/mask/InMemoryMaskManager.java ___________________________________________________________________ Name: svn:mime-type + text/plain Name: svn:keywords + Id Name: svn:eol-style + native Index: src/org/dspace/mask/MaskManager.java =================================================================== --- src/org/dspace/mask/MaskManager.java (revision 0) +++ src/org/dspace/mask/MaskManager.java (revision 5020) @@ -0,0 +1,38 @@ +package org.dspace.mask; + +/** + * Responsible for managing the masks which + * are placed on DSpace objects. + *

+ * This interface should not be exposed. + */ +interface MaskManager +{ + /** + * Mask the handle entirely: all + * masks are enabled. + */ + void mask( String handle ); + + /** + * Unmask the handle entirely: all + * masks are removed. + */ + void unmask( String handle ); + + /** + * Add a mask to the handle. + * + * @param handle the handle + * @param type the mask type + */ + void addMask( String handle, MaskType type ); + + /** + * Remove a mask from a handle + * + * @param handle the handle + * @param type the mask type + */ + void removeMask( String handle, MaskType type ); +} Property changes on: src/org/dspace/mask/MaskManager.java ___________________________________________________________________ Name: svn:mime-type + text/plain Name: svn:keywords + Id Name: svn:eol-style + native Index: src/org/dspace/mask/MaskService.java =================================================================== --- src/org/dspace/mask/MaskService.java (revision 0) +++ src/org/dspace/mask/MaskService.java (revision 5020) @@ -0,0 +1,19 @@ +package org.dspace.mask; + +/** + * The MaskService is responsible for determining if + * the object represented by handle should be masked + * for the operation represented by the {@link MaskType} type. + */ +public interface MaskService +{ + /** + * Determine if handle should be masked for + * type. + * + * @param handle the handle identifying the object + * @param type the type of mask + * @return true if the object should be masked + */ + public boolean isMasked( String handle, MaskType type ); +} Property changes on: src/org/dspace/mask/MaskService.java ___________________________________________________________________ Name: svn:mime-type + text/plain Name: svn:keywords + Id Name: svn:eol-style + native Index: src/org/dspace/mask/MaskType.java =================================================================== --- src/org/dspace/mask/MaskType.java (revision 0) +++ src/org/dspace/mask/MaskType.java (revision 5020) @@ -0,0 +1,8 @@ +package org.dspace.mask; + +public enum MaskType +{ + BROWSE, + HARVEST, + INDEX +} Property changes on: src/org/dspace/mask/MaskType.java ___________________________________________________________________ Name: svn:mime-type + text/plain Name: svn:keywords + Id Name: svn:eol-style + native