All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dspace.core.Context Maven / Gradle / Ivy

There is a newer version: 8.0
Show newest version
/**
 * 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.SQLException;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.logging.log4j.Logger;
import org.dspace.authorize.ResourcePolicy;
import org.dspace.content.DSpaceObject;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.event.Dispatcher;
import org.dspace.event.Event;
import org.dspace.event.factory.EventServiceFactory;
import org.dspace.event.service.EventService;
import org.dspace.storage.rdbms.DatabaseConfigVO;
import org.dspace.storage.rdbms.DatabaseUtils;
import org.dspace.utils.DSpace;
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.
 * 

* 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, complete is called * to commit the changes and free up any resources used by the context. If * anything has gone wrong, abort is called to roll back any * changes and free up the resources. *

* The context object is also used as a cache for CM API objects. */ public class Context implements AutoCloseable { private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(Context.class); protected static final AtomicBoolean databaseUpdated = new AtomicBoolean(false); /** * Current user - null means anonymous access */ private EPerson currentUser; /** * Temporary store when the current user is temporary switched */ private EPerson currentUserPreviousState; /** * 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 Deque authStateChangeHistory; /** * A stack with the name of the caller class that modify authorisation * system check */ private Deque authStateClassCallHistory; /** * Group IDs of special groups user is a member of */ private List specialGroups; /** * Temporary store for the specialGroups when the current user is temporary switched */ private List specialGroupsPreviousState; /** * Content events */ private LinkedList events = null; /** * Event dispatcher name */ private String dispName = null; /** * Context mode */ private Mode mode = Mode.READ_WRITE; /** * Cache that is only used the context is in READ_ONLY mode */ private final ContextReadOnlyCache readOnlyCache = new ContextReadOnlyCache(); protected EventService eventService; private DBConnection dbConnection; public enum Mode { READ_ONLY, READ_WRITE, BATCH_EDIT } protected Context(EventService eventService, DBConnection dbConnection) { this.mode = Mode.READ_WRITE; this.eventService = eventService; this.dbConnection = dbConnection; init(); } /** * Construct a new context object with default options. A database connection is opened. * No user is authenticated. */ public Context() { this.mode = Mode.READ_WRITE; init(); } /** * Construct a new context object with the given mode enabled. A database connection is opened. * No user is authenticated. * * @param mode The mode to use when opening the context. */ public Context(Mode mode) { this.mode = mode; init(); } /** * Initializes a new context object. */ protected void init() { updateDatabase(); if (eventService == null) { eventService = EventServiceFactory.getInstance().getEventService(); } if (dbConnection == null) { // Obtain a non-auto-committing connection dbConnection = new DSpace().getServiceManager() .getServiceByName(null, DBConnection.class); if (dbConnection == null) { log.fatal("Cannot obtain the bean which provides a database connection. " + "Check previous entries in the dspace.log to find why the db failed to initialize."); } } currentUser = null; currentLocale = I18nUtil.getDefaultLocale(); extraLogInfo = ""; ignoreAuth = false; specialGroups = new ArrayList<>(); authStateChangeHistory = new ConcurrentLinkedDeque<>(); authStateClassCallHistory = new ConcurrentLinkedDeque<>(); setMode(this.mode); } /** * Update the DSpace database, ensuring that any necessary migrations are run prior to initializing * Hibernate. *

* This is synchronized as it only needs to be run successfully *once* (for the first Context initialized). * * @return true/false, based on whether database was successfully updated */ public static synchronized boolean updateDatabase() { //If the database has not been updated yet, update it and remember that. if (databaseUpdated.compareAndSet(false, true)) { // Before initializing a Context object, we need to ensure the database // is up-to-date. This ensures any outstanding Flyway migrations are run // PRIOR to Hibernate initializing (occurs when DBConnection is loaded in calling init() method). try { DatabaseUtils.updateDatabase(); } catch (SQLException sqle) { log.fatal("Cannot update or initialize database via Flyway!", sqle); databaseUpdated.set(false); } } return databaseUpdated.get(); } /** * Get the database connection associated with the context * * @return the database connection */ DBConnection getDBConnection() { return dbConnection; } public DatabaseConfigVO getDBConfig() throws SQLException { return dbConnection.getDatabaseConfig(); } public String getDbType() { return dbConnection.getType(); } /** * 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 null if no user is * authenticated */ public void setCurrentUser(EPerson user) { currentUser = user; } /** * Get the current (authenticated) user * * @return the current user, or null 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 true 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: * * mycontext.turnOffAuthorisationSystem(); * some java code that require no authorisation check * mycontext.restoreAuthSystemState(); * 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 (NoSuchElementException ex) { log.warn(LogHelper.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; try { previousCaller = (String) authStateClassCallHistory.pop(); } catch (NoSuchElementException ex) { previousCaller = "none"; log.warn(LogHelper.getHeader(this, "restore_auth_sys_state", "no previous caller info available: {}"), ex::getLocalizedMessage); } // if previousCaller is not the current caller *only* log a warning if (!previousCaller.equals(caller)) { log.warn(LogHelper.getHeader( this, "restore_auth_sys_state", "Class: " + caller + " call restore but previous state change made by " + previousCaller)); } } ignoreAuth = previousState; } /** * 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: *

* setExtraLogInfo("session_id="+request.getSession().getId()); * * @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- null */ 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. *

* Calling complete() on a Context which is no longer valid (isValid()==false), * is a no-op. * * @throws 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."); return; } try { // As long as we have a valid, writeable database connection, // commit changes. Otherwise, we'll just close the DB connection (see below) if (!isReadOnly()) { commit(); } } finally { if (dbConnection != null) { // Free the DB connection and invalidate the Context dbConnection.closeDBConnection(); dbConnection = null; } } } /** * Commit the current transaction with the database, persisting any pending changes. * The database connection is not closed and can be reused afterwards. * * WARNING: After calling this method all previously fetched entities are "detached" (pending * changes are not tracked anymore). You have to reload all entities you still want to work with * manually after this method call (see {@link Context#reloadEntity(ReloadableEntity)}). * * @throws SQLException When committing the transaction in the database fails. */ public void commit() throws SQLException { // If Context is no longer open/valid, just note that it has already been closed if (!isValid()) { log.info("commit() was called on a closed Context object. No changes to commit."); return; } if (isReadOnly()) { throw new UnsupportedOperationException("You cannot commit a read-only context"); } try { // Dispatch events before committing changes to the database, // as the consumers may change something too dispatchEvents(); } finally { if (log.isDebugEnabled()) { log.debug("Cache size on commit is " + getCacheSize()); } if (dbConnection != null) { // Commit our changes (this closes the transaction but leaves database connection open) dbConnection.commit(); reloadContextBoundEntities(); } } } /** * Dispatch any events (cached in current Context) to configured EventListeners (consumers) * in the EventService. This should be called prior to any commit as some consumers may add * to the current transaction. Once events are dispatched, the Context's event cache is cleared. */ public void dispatchEvents() { Dispatcher dispatcher = null; try { if (events != null) { if (dispName == null) { dispName = EventService.DEFAULT_DISPATCHER; } dispatcher = eventService.getDispatcher(dispName); dispatcher.dispatch(this); } } finally { events = null; if (dispatcher != null) { eventService.returnDispatcher(dispName, dispatcher); } } } /** * Select an event dispatcher, null selects the default * * @param dispatcher dispatcher */ 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. * NOTE: Read-only Contexts cannot add events, as they cannot modify objects. * * @param event event to be dispatched */ 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<>(); } 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 getEvents() { return events; } /** * Whether or not the context has events cached. * @return true or false */ public boolean hasEvents() { return !CollectionUtils.isEmpty(events); } /** * Retrieves the first element in the events list and removes it from the list of events once retrieved * * @return The first event of the list or null 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. *

* 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."); return; } try { // Rollback ONLY if we have a database transaction, and it is NOT Read Only if (!isReadOnly() && isTransactionAlive()) { dbConnection.rollback(); } } catch (SQLException se) { log.error("Error rolling back transaction during an abort()", se); } finally { try { if (dbConnection != null) { // Free the DB connection & invalidate the Context dbConnection.closeDBConnection(); dbConnection = null; } } catch (Exception ex) { log.error("Error closing the database connection", ex); } events = null; } } /** * Close this Context, discarding any uncommitted changes and releasing its * database connection. */ @Override public void close() { if (isValid()) { abort(); } } /** * Find out if this context is valid. Returns false if this * context has been aborted or completed. * * @return true if the context is still valid, otherwise * false */ public boolean isValid() { // Only return true if our DB connection is live // NOTE: A transaction need not exist for our Context to be valid, as a Context may use multiple transactions. return dbConnection != null && dbConnection.isSessionAlive(); } /** * Find out whether our context includes an open database transaction. * Returns true if there is an open transaction. Returns * false if the context is invalid (e.g. abort() or complete()) * was called OR no current transaction exists (e.g. commit() was just called * and no new transaction has begun) * * @return */ protected boolean isTransactionAlive() { // Only return true if both Context is valid *and* transaction is alive return isValid() && dbConnection.isTransActionAlive(); } /** * Reports whether context supports updating DSpaceObjects, or only reading. * * @return true if the context is read-only, otherwise * false */ public boolean isReadOnly() { return mode != null && mode == Mode.READ_ONLY; } /** * Add a group's UUID to the list of special groups cached in Context * @param groupID UUID of group */ public void setSpecialGroup(UUID groupID) { specialGroups.add(groupID); } /** * Test if a group is a special group * * @param groupID ID of special group to test * @return true if member */ public boolean inSpecialGroup(UUID groupID) { return specialGroups.contains(groupID); } /** * Get an array of all of the special groups that current user is a member of. * * @return list of special groups * @throws SQLException if database error */ public List getSpecialGroups() throws SQLException { List myGroups = new ArrayList<>(); for (UUID groupId : specialGroups) { myGroups.add(EPersonServiceFactory.getInstance().getGroupService().find(this, groupId)); } return myGroups; } /** * Temporary change the user bound to the context, empty the special groups that * are retained to allow subsequent restore * * @param newUser the EPerson to bound to the context * * @throws IllegalStateException if the switch was already performed without be * restored */ public void switchContextUser(EPerson newUser) { if (currentUserPreviousState != null) { throw new IllegalStateException( "A previous user is already set, you can only switch back and foreward one time"); } currentUserPreviousState = currentUser; specialGroupsPreviousState = specialGroups; specialGroups = new ArrayList<>(); currentUser = newUser; } /** * Restore the user bound to the context and his special groups * * @throws IllegalStateException if no switch was performed before */ public void restoreContextUser() { if (specialGroupsPreviousState == null) { throw new IllegalStateException("No previous state found"); } currentUser = currentUserPreviousState; specialGroups = specialGroupsPreviousState; specialGroupsPreviousState = null; currentUserPreviousState = null; } /** * Close the context, aborting any open transactions (if any). * @throws Throwable */ @Override 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 (dbConnection != null && dbConnection.isTransActionAlive()) { abort(); } super.finalize(); } public void shutDownDatabase() throws SQLException { dbConnection.shutdown(); } /** * Returns the size of the cache of all object that have been read from the * database so far. A larger number means that more memory is consumed by * the cache. This also has a negative impact on the query performance. In * that case you should consider uncaching entities when they are no longer * needed (see {@link Context#uncacheEntity(ReloadableEntity)} () uncacheEntity}). * * @return cache size. * @throws SQLException When connecting to the active cache fails. */ public long getCacheSize() throws SQLException { return this.getDBConnection().getCacheSize(); } /** * Change the mode of this current context. * * BATCH_EDIT: Enabling batch edit mode means that the database connection is configured so that it is optimized to * process a large number of records. * * READ_ONLY: READ ONLY mode will tell the database we are nog going to do any updates. This means it can disable * optimalisations for delaying or grouping updates. * * READ_WRITE: This is the default mode and enables the normal database behaviour. This behaviour is optimal for * querying and updating a * small number of records. * * @param newMode The mode to put this context in */ public void setMode(Mode newMode) { try { //update the database settings switch (newMode) { case BATCH_EDIT: dbConnection.setConnectionMode(true, false); break; case READ_ONLY: dbConnection.setConnectionMode(false, true); break; case READ_WRITE: dbConnection.setConnectionMode(false, false); break; default: log.warn("New context mode detected that has not been configured."); break; } } catch (SQLException ex) { log.warn("Unable to set database connection mode", ex); } //Always clear the cache, except when going from READ_ONLY to READ_ONLY if (mode != Mode.READ_ONLY || newMode != Mode.READ_ONLY) { //clear our read-only cache to prevent any inconsistencies readOnlyCache.clear(); } //save the new mode mode = newMode; } /** * The current database mode of this context. * * @return The current mode */ public Mode getCurrentMode() { return mode; } /** * Enable or disable "batch processing mode" for this context. * * Enabling batch processing mode means that the database connection is configured so that it is optimized to * process a large number of records. * * Disabling batch processing mode restores the normal behaviour that is optimal for querying and updating a * small number of records. * * @param batchModeEnabled When true, batch processing mode will be enabled. If false, it will be disabled. * @throws SQLException When configuring the database connection fails. */ @Deprecated public void enableBatchMode(boolean batchModeEnabled) throws SQLException { if (batchModeEnabled) { setMode(Mode.BATCH_EDIT); } else { setMode(Mode.READ_WRITE); } } /** * Check if "batch processing mode" is enabled for this context. * * @return True if batch processing mode is enabled, false otherwise. */ @Deprecated public boolean isBatchModeEnabled() { return mode != null && mode == Mode.BATCH_EDIT; } /** * Reload an entity from the database into the cache. This method will return a reference to the "attached" * entity. This means changes to the entity will be tracked and persisted to the database. * * @param entity The entity to reload * @param The class of the entity. The entity must implement the {@link ReloadableEntity} interface. * @return A (possibly) NEW reference to the entity that should be used for further processing. * @throws SQLException When reloading the entity from the database fails. */ @SuppressWarnings("unchecked") public E reloadEntity(E entity) throws SQLException { return (E) dbConnection.reloadEntity(entity); } /** * Remove an entity from the cache. This is necessary when batch processing a large number of items. * * @param entity The entity to reload * @param The class of the entity. The entity must implement the {@link ReloadableEntity} interface. * @throws SQLException When reloading the entity from the database fails. */ @SuppressWarnings("unchecked") public void uncacheEntity(E entity) throws SQLException { dbConnection.uncacheEntity(entity); } public Boolean getCachedAuthorizationResult(DSpaceObject dspaceObject, int action, EPerson eperson) { if (isReadOnly()) { return readOnlyCache.getCachedAuthorizationResult(dspaceObject, action, eperson); } else { return null; } } public void cacheAuthorizedAction(DSpaceObject dspaceObject, int action, EPerson eperson, Boolean result, ResourcePolicy rp) { if (isReadOnly()) { readOnlyCache.cacheAuthorizedAction(dspaceObject, action, eperson, result); try { uncacheEntity(rp); } catch (SQLException e) { log.warn("Unable to uncache a resource policy when in read-only mode", e); } } } public Boolean getCachedGroupMembership(Group group, EPerson eperson) { if (isReadOnly()) { return readOnlyCache.getCachedGroupMembership(group, eperson); } else { return null; } } public void cacheGroupMembership(Group group, EPerson eperson, Boolean isMember) { if (isReadOnly()) { readOnlyCache.cacheGroupMembership(group, eperson, isMember); } } public void cacheAllMemberGroupsSet(EPerson ePerson, Set groups) { if (isReadOnly()) { readOnlyCache.cacheAllMemberGroupsSet(ePerson, groups); } } public Set getCachedAllMemberGroupsSet(EPerson ePerson) { if (isReadOnly()) { return readOnlyCache.getCachedAllMemberGroupsSet(ePerson); } else { return null; } } /** * Reload all entities related to this context. * * @throws SQLException When reloading one of the entities fails. */ private void reloadContextBoundEntities() throws SQLException { currentUser = reloadEntity(currentUser); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy