taylor 01/07/29 21:51:25 Added: src/java/org/apache/jetspeed/services/psmlmanager DatabasePsmlManagerService.java Log: - added DatabasePsmlManagerService.java Database-based PsmlManager implementation - from Atul Dambalkar, Anil Shinde, and Narendra Vaidya Revision Changes Path 1.1 jakarta-jetspeed/src/java/org/apache/jetspeed/services/psmlmanager/DatabasePsmlManagerService.java Index: DatabasePsmlManagerService.java =================================================================== /* ==================================================================== * The Apache Software License, Version 1.1 * * Copyright (c) 2000-2001 The Apache Software Foundation. All rights * reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The end-user documentation included with the redistribution, * if any, must include the following acknowledgment: * "This product includes software developed by the * Apache Software Foundation (http://www.apache.org/)." * Alternately, this acknowledgment may appear in the software itself, * if and wherever such third-party acknowledgments normally appear. * * 4. The names "Apache" and "Apache Software Foundation" and * "Apache Jetspeed" must not be used to endorse or promote products * derived from this software without prior written permission. For * written permission, please contact [EMAIL PROTECTED] * * 5. Products derived from this software may not be called "Apache" or * "Apache Jetspeed", nor may "Apache" appear in their name, without * prior written permission of the Apache Software Foundation. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * <http://www.apache.org/>. */ package org.apache.jetspeed.services.psmlmanager; // PSML Manager Service interface import org.apache.jetspeed.services.psmlmanager.PsmlManagerService; // Profile and ProfileLocator interface import org.apache.jetspeed.om.profile.Profile; import org.apache.jetspeed.om.profile.ProfileLocator; import org.apache.jetspeed.om.profile.QueryLocator; //Castor defined API import org.apache.jetspeed.xml.api.portletmarkup.Portlets; import org.apache.jetspeed.om.profile.PSMLDocument; import org.apache.jetspeed.om.profile.BasePSMLDocument; //turbine stuff import org.apache.turbine.util.Log; import org.apache.turbine.services.TurbineBaseService; import org.apache.turbine.services.InitializationException; import org.apache.turbine.services.TurbineServices; import org.apache.turbine.services.resources.ResourceService; import org.apache.turbine.om.security.User; import org.apache.turbine.om.security.Group; import org.apache.turbine.om.security.Role; import org.apache.turbine.util.db.pool.DBConnection; import org.apache.turbine.services.db.TurbineDB; //Servlet API import javax.servlet.ServletConfig; //castor support import org.exolab.castor.xml.MarshalException; import org.exolab.castor.xml.ValidationException; //standard java stuff import java.lang.Thread; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringWriter; import java.io.ByteArrayInputStream; import java.util.Iterator; import java.util.List; import java.util.ArrayList; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.sql.Connection; import java.sql.ResultSet; import java.sql.Statement; import java.sql.PreparedStatement; import java.sql.SQLException; import java.io.IOException; /** * This service is responsible for loading and saving PSML documents. It uses * database to persist the PSML documents. * * @author <a href="mailto:[EMAIL PROTECTED]">Atul Dambalkar</a> * @author <a href="mailto:[EMAIL PROTECTED]">Anil Shinde</a> * @author <a href="mailto:[EMAIL PROTECTED]">Narendra Vaidya</a> * @version */ public class DatabasePsmlManagerService extends TurbineBaseService implements PsmlManagerService { private Map psmlCache = new WeakHashMap(); /** The watcher for the document locations */ private CacheRefresher refresher = null; /** the base refresh rate for documents */ private long refreshRate; // default will be 8 hours /** Anonymous user name */ private String userName; // default will be "anonymous" private final static String REFRESH_RATE = "refresh-rate"; private final static String USER_NAME = ".user.name"; private final static long DEFAULT_REFRESH_RATE = 60 * 60 * 8 * 1000; //8hrs private final static String DEFAULT_USER_NAME = "anonymous"; private final static String JETSPEED_USER_PROFILE = "jetspeed_user_profile"; private final static String JETSPEED_GROUP_PROFILE = "jetspeed_group_profile"; private final static String JETSPEED_ROLE_PROFILE = "jetspeed_role_profile"; private final static String JETSPEED_USER_PROFILE_SEQ = "jetspeed_user_profile_seq"; private final static String JETSPEED_GROUP_PROFILE_SEQ = "jetspeed_group_profile_seq"; private final static String JETSPEED_ROLE_PROFILE_SEQ = "jetspeed_role_profile_seq"; /** * This is the early initialization method called by the * Turbine <code>Service</code> framework */ public void init( ServletConfig conf ) throws InitializationException { Log.note("Initializing DatabasePsmlManagerService..."); initConfiguration(); //Mark that we are done setInit(true); Log.note("Done initializing DatabasePsmlManagerService."); } /** * Loads the configuration parameters for this service from the * JetspeedResources.properties file. * * @exception throws a <code>InitializationException</code> if the service * fails to initialize */ private void initConfiguration() throws InitializationException { ResourceService serviceConf = ((TurbineServices)TurbineServices.getInstance()) .getResources(PsmlManagerService.SERVICE_NAME); try { // get configuration parameters from Turbine Resources refreshRate = serviceConf.getLong(REFRESH_RATE, DEFAULT_REFRESH_RATE); userName = serviceConf.getString(USER_NAME, DEFAULT_USER_NAME); } catch (Throwable t) { throw new InitializationException( "Missing default refresh rate parameter, during initializing DatabasePsmlManagerService, using defaults"); } this.refresher = new CacheRefresher(); refresher.start(); } /** Late init method from Turbine Service model */ public void init( ) throws InitializationException { while( !getInit() ) { //Not yet... try { Thread.sleep( 500 ); } catch (InterruptedException ie ) { Log.error( ie ); } } } /** * A thread implementation of cache refreshing mechanism for database * persisted PSMLs. We have to refresh the cache after specific intervals * if someone manually updates the PSML database. * * @author <a href="mailto:[EMAIL PROTECTED]">Atul Dambalkar</a> */ class CacheRefresher extends Thread { private boolean done = false; /** * Constructor to to set the priority. */ CacheRefresher() { setDaemon(true); setPriority(Thread.MIN_PRIORITY+1); } /** * We are all done, system is shutting down. */ void setDone(boolean done) { this.done = done; } /** * Method as needed for a Thread to run */ public void run() { try { while ( !done ) { Log.note("Cache Refresher thread working now!"); try { synchronized (this) { Iterator i = psmlCache.keySet().iterator(); while(i.hasNext()) { ProfileLocator locator = (ProfileLocator)i.next(); // do refresh for the locator PSMLDocument doc = refresh(locator); // over write the existing document in cache psmlCache.put(locator, doc); } } } catch (Exception e) { Log.error("CacheRefresher: Error in iteration...", e); } Log.note("Cache Refresher thread sleeping now!"); sleep (refreshRate); Log.note("Cache Refresher thread woke up!"); } } catch (InterruptedException e) { Log.note("CacheRefresher: recieved interruption, aborting."); } } } public boolean saveDocument(PSMLDocument doc) { // do nothing return false; } public PSMLDocument getDocument( String name ) { // do mothing, deprecated return null; } public boolean saveDocument(String fileOrUrl, PSMLDocument doc) { // do mothing, deprecated return false; } /** * Returns a PSML document for the given locator * * @param locator The locator descriptor(ProfileLocator object) of the * document to be retrieved. * @return psmldoc The PSMLDocument object * @author <a href="mailto:[EMAIL PROTECTED]">Anil Shinde</a> */ public PSMLDocument getDocument( ProfileLocator locator ) { // check the cache for the req'e document if not available in cache // query the database for Portlets for the given locator // use unmarsheller to create Portlets object, get PSMLDocument // object for it, update the cache so that next time when user logs in // use cached PSMLDocument if (locator == null) { String message = "PSMLManager: Must specify a locator"; Log.error( message ); throw new IllegalArgumentException( message ); } PSMLDocument psmldoc = null; synchronized (psmlCache) { psmldoc = (PSMLDocument)psmlCache.get(locator); } if (psmldoc != null) { return psmldoc; } DBConnection dbConn = null; Statement stmt = null; try { dbConn = TurbineDB.getConnection(); stmt = dbConn.createStatement(); ResultSet rset = stmt.executeQuery (getQueryString(locator)); while ( rset.next() ) { String page = rset.getString("page"); Portlets portlets = getPortlets(rset.getBinaryStream("profile")); psmldoc = getPSMLDocument(page, portlets); synchronized (psmlCache) { psmlCache.put(locator, psmldoc); } return psmldoc; } } catch (Exception e) { //From obtaining the connection or from your application code. Log.error("Unable to get a psml document from the database.", e); } finally { try { if (stmt != null) { stmt.close(); stmt = null; } TurbineDB.releaseConnection(dbConn); } catch (Exception e) { // Error releasing database connection back to pool. Log.error("Release of connection failed.", e); } } return null; } /** * Stores the PSML document in DB for the given profile * * @param profile The profile that holds the PSMLDocument. * @return PSMLDocument The PSMLDocument that got created in DB. * @author <a href="mailto:[EMAIL PROTECTED]">Narendra Vaidya</a> */ public PSMLDocument createDocument( Profile profile ) { // create record in the database for Portlets for the given // profile/PSMLDocuemnt,use marsheller to create Portlets // object and then put it in database, update the cache if (profile == null) { String message = "PSMLManager: Must specify a profile"; Log.error( message ); throw new IllegalArgumentException( message ); } PortletInputStream portletInputStream = getPortletInputStream( profile.getDocument().getPortlets() ); String tableName=null; String seqName=null; String name=null; if ( profile.getUser() != null ) { tableName = JETSPEED_USER_PROFILE; name = profile.getUser().getUserName(); seqName = JETSPEED_USER_PROFILE_SEQ + ".nextval"; } else if ( profile.getGroup() != null ) { tableName = JETSPEED_GROUP_PROFILE; name = profile.getGroup().getName(); seqName = JETSPEED_GROUP_PROFILE_SEQ + ".nextval"; } else { tableName = JETSPEED_ROLE_PROFILE; name = profile.getRole().getName(); seqName = JETSPEED_ROLE_PROFILE_SEQ + ".nextval"; } Log.note("Login Name: " + name); DBConnection dbConn = null; PreparedStatement pStatement = null; try { dbConn = TurbineDB.getConnection(); String sqlString = "INSERT INTO " + tableName + " VALUES " + "(" + seqName + ", ? , ? , ? , ? , ? , ?)"; pStatement = dbConn.prepareStatement( sqlString ); pStatement.setString(1, name); pStatement.setString(2, profile.getMediaType() ); pStatement.setString(3, profile.getLanguage() ); pStatement.setString(4, profile.getCountry() ); pStatement.setString(5, profile.getName() ); Log.error("SQL String for creating document " + pStatement); pStatement.setBinaryStream(6, portletInputStream , portletInputStream.length() ); if ( pStatement.executeUpdate() != 1 ) { // insert falied Log.error("Insert in table " + tableName + " failed"); throw new RuntimeException( "Could not create new document in DB" ); } else { // insert successful synchronized (psmlCache) { psmlCache.put( profile, profile.getDocument() ); } } return profile.getDocument(); } catch (Exception e) { Log.error("Error occurred in Database PSML Manager: " + e); } finally { try { if (pStatement != null) { pStatement.close() ; } TurbineDB.releaseConnection(dbConn); if (portletInputStream != null ) { portletInputStream.close(); } } catch (IOException e) { Log.error("Error occurred while closing Input stream: " + e); } catch (Exception e) { Log.error("Error occurred while releasing the connection: " + e); } } return null; // shouldn't be here } /** * Remove the PSMLDocument/profile for given locator object. * * @param locator The profile locator criteria for profile to be removed. * @author <a href="mailto:[EMAIL PROTECTED]">Atul Dambalkar</a> */ public void removeDocument( ProfileLocator locator ) { if (locator == null) { String message = "PSMLManager: Must specify a locator"; Log.error( message ); throw new IllegalArgumentException( message ); } DBConnection conn = null; Statement stmt = null; String delete = getDeleteString(locator); try { conn = TurbineDB.getConnection(); conn.setAutoCommit(false); stmt = conn.createStatement(); Log.note ("SQL Executing: " + delete); int rs = stmt.executeUpdate(delete); Log.note ("Number of rows deleted: " + rs); } catch (Exception e) { Log.error("Error occurred in Database PSML Manager: " + e); } finally { try { // commit the delete conn.commit(); // update the cache. removeFromCache(locator); if (stmt != null) { stmt.close(); // underlying ResultSet will also be closed stmt = null; } TurbineDB.releaseConnection(conn); } catch (Exception e) { Log.error("Error occurred while releasing the connection: " + e); } } } /** * Prepare SQL delete string depending upon ProfileLocator object contents. * * @param locator The profile locator criteria. * @return SQL Delete String object for given locator object. * @author <a href="mailto:[EMAIL PROTECTED]">Atul Dambalkar</a> */ private String getDeleteString(ProfileLocator locator) { QueryHelper qhelper = new QueryHelper(locator); String mediaType = qhelper.getMediaType(); String language = qhelper.getLanguage(); String country = qhelper.getCountry(); String page = qhelper.getPage(); String name = qhelper.getName(); String column = qhelper.getColumn(); String table = qhelper.getTable(); if (name == null) { String message = "PSMLManager: Must specify a name to delete PSMLDocument"; Log.error( message ); throw new IllegalArgumentException( message ); } String whereClause = ""; if (!name.equals("*")) { whereClause = " where " + column + " = '" + name + "' " + ((page != null && page.length() > 0)? " and page = '" + page + "' ": "" ) + ((mediaType != null && mediaType.length() > 0)? " and media_type = '" + mediaType + "' ": "" ) + ((language != null && language.length() > 0)? " and language = '" + language + "' ": "" ) + ((country != null && country.length() > 0)? " and country = '" + country + "' ": "" ); } return "delete from " + table + whereClause; } /** * Class to help creating the query string from the ProfileLocator object. * @author <a href="mailto:[EMAIL PROTECTED]">Atul Dambalkar</a> */ class QueryHelper { private String mediaType; private String language; private String country; private String page; private String name; private String column; private String table; QueryHelper(ProfileLocator locator) { mediaType = locator.getMediaType(); language = locator.getLanguage(); country = locator.getCountry(); page = locator.getName(); User user = locator.getUser(); Group group = locator.getGroup(); Role role = locator.getRole(); if (user != null) { table = JETSPEED_USER_PROFILE; name = user.getUserName(); column = "login_name"; } else if (group != null) { table = JETSPEED_GROUP_PROFILE; name = group.getName(); column = "group_name"; } else if (role != null) { table = JETSPEED_ROLE_PROFILE; name = role.getName(); column = "role_name"; } else // it's a anonymous access { table = JETSPEED_USER_PROFILE; name = userName; // "anonymous" user name column = "login_name"; } } String getMediaType() { return mediaType; } String getLanguage() { return language; } String getCountry() { return country; } String getPage() { return page; } String getName() { return name; } String getTable() { return table; } String getColumn() { return column; } } /** * Remove PSMLDocuments from the cache matching locator object. If locator * object contains name as "*", we need to remove all those PSMLDocuments * from cache. */ private void removeFromCache(ProfileLocator locator) { QueryHelper qhelper = new QueryHelper(locator); String name = qhelper.getName(); String table = qhelper.getTable(); if (name == null) { String message = "PSMLManager: Must specify a name to delete PSMLDocument"; Log.error( message ); throw new IllegalArgumentException( message ); } if (!name.equals("*")) { synchronized (psmlCache) { psmlCache.remove(locator); } return; } // else, remove all the psml documents for the entire user/group/role // table synchronized (psmlCache) { Set keyset = psmlCache.keySet(); Iterator iterator = keyset.iterator(); while (iterator.hasNext()) { locator = (ProfileLocator)iterator.next(); if ( ((table.equals(JETSPEED_USER_PROFILE) && locator.getUser() != null)) || ((table.equals(JETSPEED_GROUP_PROFILE) && locator.getGroup() != null)) || ((table.equals(JETSPEED_ROLE_PROFILE) && locator.getRole() != null)) ) { psmlCache.remove(locator); } } } } /** * Query for a collection of profiles given a profile locator criteria. * Use SQL engine to get the required profiles. * * @param locator The profile locator criteria. * @return Iterator object with the PSMLDocuments satisfying query * @author <a href="mailto:[EMAIL PROTECTED]">Atul Dambalkar</a> */ public Iterator query( QueryLocator locator ) { if (locator == null) { String message = "PSMLManager: Must specify a locator"; Log.error( message ); throw new IllegalArgumentException( message ); } DBConnection conn = null; Statement stmt = null; String query = getQueryString(locator); try { conn = TurbineDB.getConnection(); stmt = conn.createStatement(); List list = new ArrayList(); Log.note ("SQL Executing: " + query); ResultSet rs = stmt.executeQuery(query); while (rs.next()) { String page = rs.getString("page"); Portlets portlets = getPortlets(rs.getBinaryStream("profile")); list.add(getPSMLDocument(page, portlets)); } return list.iterator(); } catch (Exception e) { Log.error("Error occurred in Database PSML Manager: " + e); } finally { try { if (stmt != null) { stmt.close(); // underlying ResultSet will be also closed stmt = null; } TurbineDB.releaseConnection(conn); } catch (Exception e) { Log.error("Error occurred while releasing the connection: " + e); } } return new ArrayList().iterator(); // return empty non-null iterator } /** * Get PSMLDocument object for given pagename and portlets. * * @param portlets Portlets for the given page name * @param page page name for this resource * @return PSMLDocument object for given page and portlets * @author <a href="mailto:[EMAIL PROTECTED]">Atul Dambalkar</a> */ private PSMLDocument getPSMLDocument(String page, Portlets portlets) { PSMLDocument psmldoc = new BasePSMLDocument(); psmldoc.setName(page); psmldoc.setPortlets(portlets); return psmldoc; } /** * Prepare SQL query string depending upon ProfileLocator object contents. * * @param locator The profile locator criteria. * @return Select query String object for given locator object. * @author <a href="mailto:[EMAIL PROTECTED]">Atul Dambalkar</a> */ private String getQueryString(ProfileLocator locator) { QueryHelper qhelper = new QueryHelper(locator); String mediaType = qhelper.getMediaType(); String language = qhelper.getLanguage(); String country = qhelper.getCountry(); String page = qhelper.getPage(); String name = qhelper.getName(); String column = qhelper.getColumn(); String table = qhelper.getTable(); return "select page, profile from " + table + " where " + column + " = '" + name + "' " + ((page != null && page.length() > 0)? " and page = '" + page + "' ": "" ) + ((mediaType != null && mediaType.length() > 0)? " and media_type = '" + mediaType + "' ": "" ) + ((language != null && language.length() > 0)? " and language = '" + language + "' ": "" ) + ((country != null && country.length() > 0)? " and country = '" + country + "' ": "" ); } /** * Given ordered list of locators, find the first document matching * a profile locator, starting from the beginning of the list and working * to the end. * * @param locator The ordered list of profile locators. * @return PSMLDocument object for the first document matching a locator * @author <a href="mailto:[EMAIL PROTECTED]">Atul Dambalkar</a> */ public PSMLDocument getDocument( List locators ) { if (locators == null) { String message = "PSMLManager: Must specify a list of locators"; Log.error( message ); throw new IllegalArgumentException( message ); } // iterate over the list and invoke getDocument(locator) method for (int i = 0; i < locators.size(); i++) { PSMLDocument psmldoc = getDocument((ProfileLocator)locators.get(i)); if (psmldoc != null) { return psmldoc; } } // no document found for matching single locator, hypothetical return null; } /** * Returns a PSML document for the given locator, it is called by the cache * refresher * * @param locator The locator descriptor(ProfileLocator object) of the * document to be retrieved. * @return psmldoc The PSMLDocument object * @author <a href="mailto:[EMAIL PROTECTED]">Anil Shinde</a> */ private PSMLDocument refresh( ProfileLocator locator ) { // go to database and get the blob, and marshal the Portlets if (locator == null) { String message = "PSMLManager: Must specify a locator"; Log.error( message ); throw new IllegalArgumentException( message ); } DBConnection dbConn = null; Statement stmt = null; try { dbConn = TurbineDB.getConnection(); stmt = dbConn.createStatement(); ResultSet rset = stmt.executeQuery(getQueryString(locator)); while ( rset.next() ) { String page = rset.getString("page"); Portlets portlets = getPortlets(rset.getBinaryStream("profile")); return getPSMLDocument(page, portlets); } } catch (Exception e) { // From obtaining the connection or from your application code. Log.error("Unable to get a psml document from the database.", e); } finally { try { if (stmt != null) { stmt.close(); stmt = null; } TurbineDB.releaseConnection(dbConn); } catch (Exception e) { // Error releasing database connection back to pool. Log.error("Release of connection failed.", e); } } return null; // shouldn't be here } /** Deserialize a PSML structure read from the input stream using Castor * XML unmarshaller * * @param in the input stream to load the PSML from * @return PSML structure Portlets object * @author <a href="mailto:[EMAIL PROTECTED]">Atul Dambalkar</a> */ private Portlets getPortlets(InputStream in) { InputStreamReader reader = new InputStreamReader(in); try { return Portlets.unmarshal(reader); } catch (MarshalException e) { Log.error("PSMLManager: Could not unmarshal the inputstream ", e); } catch (ValidationException e) { Log.error("PSMLManager: document is not valid", e); } finally { try { reader.close(); } catch (IOException e) { Log.error("", e); } } return null; // control shouldn't be here } /** Serialize a PSML structure using string writer with Castor XML * marshaller, put it in input stream and return it. * * @param portlets the structure to save * @param len integer object which contains the length for the PSML string * @param PortletStream PortletStream object * @return PortletInputStream object * @author <a href="mailto:[EMAIL PROTECTED]">Atul Dambalkar</a> */ private PortletInputStream getPortletInputStream(Portlets portlets) { if (portlets == null) { String message = "PSMLManager: Must specify portlets"; Log.error( message ); throw new IllegalArgumentException( message ); } StringWriter writer = new StringWriter(); try { portlets.marshal(writer); return new PortletInputStream(writer.toString()); } catch (MarshalException e) { Log.error("PSMLManager: Could not marshal the stringwriter ", e); } catch (ValidationException e) { Log.error("PSMLManager: document is not valid", e); } finally { try { writer.close(); } catch (IOException e) { Log.error("", e); } } return null; // control shouldn't be here } /** * Class to convert the marshalled PSML String object into InputStream * @author <a href="mailto:[EMAIL PROTECTED]">Atul Dambalkar</a> */ class PortletInputStream extends ByteArrayInputStream { private int length; PortletInputStream(String psml) { /**** Platform's default character encoding will be used ****/ super(psml.getBytes()); this.length = psml.length(); } public int length() { return length; } } /** Removes all documents for a given user. * * @param user The user object. */ public void removeUserDocuments( User user ) { // IMPLEMENT ME: } /** Removes all documents for a given role. * * @param role The role object. */ public void removeRoleDocuments( Role role ) { // IMPLEMENT ME: } /** Removes all documents for a given group. * * @param group The group object. */ public void removeGroupDocuments( Group group ) { // IMPLEMENT ME: } } --------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]