/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.core;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;
import org.apache.log4j.Logger;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.event.Dispatcher;
import org.dspace.event.Event;
import org.dspace.event.EventManager;
import org.dspace.storage.rdbms.DatabaseManager;
import org.springframework.util.CollectionUtils;
/**
* Class representing the context of a particular DSpace operation. This stores
* information such as the current authenticated user and the database
* connection being used.
* <P>
* Typical use of the context object will involve constructing one, and setting
* the current user if one is authenticated. Several operations may be performed
* using the context object. If all goes well, <code>complete</code> is called
* to commit the changes and free up any resources used by the context. If
* anything has gone wrong, <code>abort</code> is called to roll back any
* changes and free up the resources.
* <P>
* The context object is also used as a cache for CM API objects.
*
*
* @version $Revision$
*/
public class Context
{
private static final Logger log = Logger.getLogger(Context.class);
/** option flags */
public static final short READ_ONLY = 0x01;
/** Database connection */
private Connection connection;
/** Current user - null means anonymous access */
private EPerson currentUser;
/** Current Locale */
private Locale currentLocale;
/** Extra log info */
private String extraLogInfo;
/** Indicates whether authorisation subsystem should be ignored */
private boolean ignoreAuth;
/** A stack with the history of authorisation system check modify */
private Stack<Boolean> authStateChangeHistory;
/**
* A stack with the name of the caller class that modify authorisation
* system check
*/
private Stack<String> authStateClassCallHistory;
/** Object cache for this context */
private Map<String, Object> objectCache;
/** Group IDs of special groups user is a member of */
private List<Integer> specialGroups;
/** Content events */
private LinkedList<Event> events = null;
/** Event dispatcher name */
private String dispName = null;
/** options */
private short options = 0;
/**
* Construct a new context object with default options. A database connection is opened.
* No user is authenticated.
*
* @exception SQLException
* if there was an error obtaining a database connection
*/
public Context() throws SQLException
{
init();
}
/**
* Construct a new context object with passed options. A database connection is opened.
* No user is authenticated.
*
* @param options context operation flags
* @exception SQLException
* if there was an error obtaining a database connection
*/
public Context(short options) throws SQLException
{
this.options = options;
init();
}
/**
* Initializes a new context object.
*
* @exception SQLException
* if there was an error obtaining a database connection
*/
private void init() throws SQLException
{
// Obtain a non-auto-committing connection
connection = DatabaseManager.getConnection();
connection.setAutoCommit(false);
currentUser = null;
currentLocale = I18nUtil.DEFAULTLOCALE;
extraLogInfo = "";
ignoreAuth = false;
objectCache = new HashMap<String, Object>();
specialGroups = new ArrayList<Integer>();
authStateChangeHistory = new Stack<Boolean>();
authStateClassCallHistory = new Stack<String>();
}
/**
* Get the database connection associated with the context
*
* @return the database connection
*/
public Connection getDBConnection()
{
return connection;
}
/**
* Set the current user. Authentication must have been performed by the
* caller - this call does not attempt any authentication.
*
* @param user
* the new current user, or <code>null</code> if no user is
* authenticated
*/
public void setCurrentUser(EPerson user)
{
currentUser = user;
}
/**
* Get the current (authenticated) user
*
* @return the current user, or <code>null</code> if no user is
* authenticated
*/
public EPerson getCurrentUser()
{
return currentUser;
}
/**
* Gets the current Locale
*
* @return Locale the current Locale
*/
public Locale getCurrentLocale()
{
return currentLocale;
}
/**
* set the current Locale
*
* @param locale
* the current Locale
*/
public void setCurrentLocale(Locale locale)
{
currentLocale = locale;
}
/**
* Find out if the authorisation system should be ignored for this context.
*
* @return <code>true</code> if authorisation should be ignored for this
* session.
*/
public boolean ignoreAuthorization()
{
return ignoreAuth;
}
/**
* Turn Off the Authorisation System for this context and store this change
* in a history for future use.
*/
public void turnOffAuthorisationSystem()
{
authStateChangeHistory.push(ignoreAuth);
if (log.isDebugEnabled())
{
Thread currThread = Thread.currentThread();
StackTraceElement[] stackTrace = currThread.getStackTrace();
String caller = stackTrace[stackTrace.length - 1].getClassName();
authStateClassCallHistory.push(caller);
}
ignoreAuth = true;
}
/**
* Restore the previous Authorisation System State. If the state was not
* changed by the current caller a warning will be displayed in log. Use:
* <code>
* mycontext.turnOffAuthorisationSystem();
* some java code that require no authorisation check
* mycontext.restoreAuthSystemState();
* </code> If Context debug is enabled, the correct sequence calling will be
* checked and a warning will be displayed if not.
*/
public void restoreAuthSystemState()
{
Boolean previousState;
try
{
previousState = authStateChangeHistory.pop();
}
catch (EmptyStackException ex)
{
log.warn(LogManager.getHeader(this, "restore_auth_sys_state",
"not previous state info available "
+ ex.getLocalizedMessage()));
previousState = Boolean.FALSE;
}
if (log.isDebugEnabled())
{
Thread currThread = Thread.currentThread();
StackTraceElement[] stackTrace = currThread.getStackTrace();
String caller = stackTrace[stackTrace.length - 1].getClassName();
String previousCaller = (String) authStateClassCallHistory.pop();
// if previousCaller is not the current caller *only* log a warning
if (!previousCaller.equals(caller))
{
log
.warn(LogManager
.getHeader(
this,
"restore_auth_sys_state",
"Class: "
+ caller
+ " call restore but previous state change made by "
+ previousCaller));
}
}
ignoreAuth = previousState.booleanValue();
}
/**
* Specify whether the authorisation system should be ignored for this
* context. This should be used sparingly.
*
* @deprecated use turnOffAuthorisationSystem() for make the change and
* restoreAuthSystemState() when change are not more required
* @param b
* if <code>true</code>, authorisation should be ignored for this
* session.
*/
public void setIgnoreAuthorization(boolean b)
{
ignoreAuth = b;
}
/**
* Set extra information that should be added to any message logged in the
* scope of this context. An example of this might be the session ID of the
* current Web user's session:
* <P>
* <code>setExtraLogInfo("session_id="+request.getSession().getId());</code>
*
* @param info
* the extra information to log
*/
public void setExtraLogInfo(String info)
{
extraLogInfo = info;
}
/**
* Get extra information to be logged with message logged in the scope of
* this context.
*
* @return the extra log info - guaranteed non- <code>null</code>
*/
public String getExtraLogInfo()
{
return extraLogInfo;
}
/**
* Close the context object after all of the operations performed in the
* context have completed successfully. Any transaction with the database is
* committed.
* <p>
* Calling complete() on a Context which is no longer valid (isValid()==false),
* is a no-op.
*
* @exception SQLException
* if there was an error completing the database transaction
* or closing the connection
*/
public void complete() throws SQLException
{
// If Context is no longer open/valid, just note that it has already been closed
if(!isValid())
log.info("complete() was called on a closed Context object. No changes to commit.");
// FIXME: Might be good not to do a commit() if nothing has actually
// been written using this connection
try
{
// As long as we have a valid, writeable database connection,
// commit any changes made as part of the transaction
if (isValid() && !isReadOnly())
{
commit();
}
}
finally
{
// Free the DB connection
// If connection is closed or null, this is a no-op
DatabaseManager.freeConnection(connection);
connection = null;
clearCache();
}
}
/**
* Commit any transaction that is currently in progress, but do not close
* the context.
*
* @exception SQLException
* if there was an error completing the database transaction
* or closing the connection
* @exception IllegalStateException
* if the Context is read-only or is no longer valid
*/
public void commit() throws SQLException
{
// Invalid Condition. The Context is Read-Only, and transactions cannot
// be committed.
if (isReadOnly())
{
throw new IllegalStateException("Attempt to commit transaction in read-only context");
}
// Invalid Condition. The Context has been either completed or aborted
// and is no longer valid
if (!isValid())
{
throw new IllegalStateException("Attempt to commit transaction to a completed or aborted context");
}
// Commit any changes made as part of the transaction
Dispatcher dispatcher = null;
try
{
if (events != null)
{
if (dispName == null)
{
dispName = EventManager.DEFAULT_DISPATCHER;
}
dispatcher = EventManager.getDispatcher(dispName);
connection.commit();
dispatcher.dispatch(this);
}
else
{
connection.commit();
}
}
finally
{
events = null;
if (dispatcher != null)
{
EventManager.returnDispatcher(dispName, dispatcher);
}
}
}
/**
* Select an event dispatcher, <code>null</code> selects the default
*
*/
public void setDispatcher(String dispatcher)
{
if (log.isDebugEnabled())
{
log.debug(this.toString() + ": setDispatcher(\"" + dispatcher
+ "\")");
}
dispName = dispatcher;
}
/**
* Add an event to be dispatched when this context is committed.
*
* @param event
*/
public void addEvent(Event event)
{
/*
* invalid condition if in read-only mode: events - which
* indicate mutation - are firing: no recourse but to bail
*/
if (isReadOnly())
{
throw new IllegalStateException("Attempt to mutate object in read-only context");
}
if (events == null)
{
events = new LinkedList<Event>();
}
events.add(event);
}
/**
* Get the current event list. If there is a separate list of events from
* already-committed operations combine that with current list.
*
* @return List of all available events.
*/
public LinkedList<Event> getEvents()
{
return events;
}
public boolean hasEvents()
{
return !CollectionUtils.isEmpty(events);
}
/**
* Retrieves the first element in the events list & removes it from the list of events once retrieved
* @return The first event of the list or <code>null</code> if the list is empty
*/
public Event pollEvent()
{
if(hasEvents())
{
return events.poll();
}else{
return null;
}
}
/**
* Close the context, without committing any of the changes performed using
* this context. The database connection is freed. No exception is thrown if
* there is an error freeing the database connection, since this method may
* be called as part of an error-handling routine where an SQLException has
* already been thrown.
* <p>
* Calling abort() on a Context which is no longer valid (isValid()==false),
* is a no-op.
*/
public void abort()
{
// If Context is no longer open/valid, just note that it has already been closed
if(!isValid())
log.info("abort() was called on a closed Context object. No changes to abort.");
try
{
// Rollback if we have a database connection, and it is NOT Read Only
if (isValid() && !connection.isClosed() && !isReadOnly())
{
connection.rollback();
}
}
catch (SQLException se)
{
log.error(se.getMessage(), se);
}
finally
{
try
{
// Free the DB connection
// If connection is closed or null, this is a no-op
DatabaseManager.freeConnection(connection);
}
catch (Exception ex)
{
log.error("Exception aborting context", ex);
}
connection = null;
events = null;
clearCache();
}
}
/**
*
* Find out if this context is valid. Returns <code>false</code> if this
* context has been aborted or completed.
*
* @return <code>true</code> if the context is still valid, otherwise
* <code>false</code>
*/
public boolean isValid()
{
// Only return true if our DB connection is live
return (connection != null);
}
/**
* Reports whether context supports updating DSpaceObjects, or only reading.
*
* @return <code>true</code> if the context is read-only, otherwise
* <code>false</code>
*/
public boolean isReadOnly()
{
return (options & READ_ONLY) > 0;
}
/**
* Store an object in the object cache.
*
* @param objectClass
* Java Class of object to check for in cache
* @param id
* ID of object in cache
*
* @return the object from the cache, or <code>null</code> if it's not
* cached.
*/
public Object fromCache(Class<?> objectClass, int id)
{
String key = objectClass.getName() + id;
return objectCache.get(key);
}
/**
* Store an object in the object cache.
*
* @param o
* the object to store
* @param id
* the object's ID
*/
public void cache(Object o, int id)
{
// bypass cache if in read-only mode
if (! isReadOnly())
{
String key = o.getClass().getName() + id;
objectCache.put(key, o);
}
}
/**
* Remove an object from the object cache.
*
* @param o
* the object to remove
* @param id
* the object's ID
*/
public void removeCached(Object o, int id)
{
String key = o.getClass().getName() + id;
objectCache.remove(key);
}
/**
* Remove all the objects from the object cache
*/
public void clearCache()
{
objectCache.clear();
}
/**
* Get the count of cached objects, which you can use to instrument an
* application to track whether it is "leaking" heap space by letting cached
* objects build up. We recommend logging a cache count periodically or
* episodically at the INFO or DEBUG level, but ONLY when you are diagnosing
* cache leaks.
*
* @return count of entries in the cache.
*
* @return the number of items in the cache
*/
public int getCacheSize()
{
return objectCache.size();
}
/**
* set membership in a special group
*
* @param groupID
* special group's ID
*/
public void setSpecialGroup(int groupID)
{
specialGroups.add(Integer.valueOf(groupID));
// System.out.println("Added " + groupID);
}
/**
* test if member of special group
*
* @param groupID
* ID of special group to test
* @return true if member
*/
public boolean inSpecialGroup(int groupID)
{
if (specialGroups.contains(Integer.valueOf(groupID)))
{
// System.out.println("Contains " + groupID);
return true;
}
return false;
}
/**
* Get an array of all of the special groups that current user is a member
* of.
* @throws SQLException
*/
public Group[] getSpecialGroups() throws SQLException
{
List<Group> myGroups = new ArrayList<Group>();
for (Integer groupId : specialGroups)
{
myGroups.add(Group.find(this, groupId.intValue()));
}
return myGroups.toArray(new Group[myGroups.size()]);
}
protected void finalize() throws Throwable
{
/*
* If a context is garbage-collected, we roll back and free up the
* database connection if there is one.
*/
if (connection != null)
{
abort();
}
super.finalize();
}
}