/***************************************************
*
* cismet GmbH, Saarbruecken, Germany
*
* ... and it just works.
*
****************************************************/
package Sirius.server.localserver.history;
import Sirius.server.AbstractShutdownable;
import Sirius.server.ServerExitError;
import Sirius.server.Shutdown;
import Sirius.server.localserver.DBServer;
import Sirius.server.localserver.attribute.ClassAttribute;
import Sirius.server.middleware.impls.domainserver.DomainServerImpl;
import Sirius.server.middleware.types.HistoryObject;
import Sirius.server.middleware.types.MetaClass;
import Sirius.server.middleware.types.MetaObject;
import Sirius.server.newuser.User;
import Sirius.server.newuser.permission.PermissionHolder;
import Sirius.server.sql.DBConnection;
import org.apache.log4j.Logger;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import de.cismet.cids.utils.MetaClassCacheService;
/**
* DOCUMENT ME!
*
* @author martin.scholl@cismet.de
* @version $Revision$, $Date$
*/
public final class HistoryServer extends Shutdown {
//~ Static fields/initializers ---------------------------------------------
private static final transient Logger LOG = Logger.getLogger(HistoryServer.class);
public static final String JSON_DELETED = "{ DELETED }"; // NOI18N
//~ Instance fields --------------------------------------------------------
private final DBServer server;
private final transient HistoryExecutor executor;
//~ Constructors -----------------------------------------------------------
/**
* Creates a new HistoryServer object.
*
* @param server the {@link DBServer} that is responsible for delivering classes, connections and objects
*
* @throws IllegalArgumentException if the given <code>DBServer</code> instance is null
*/
public HistoryServer(final DBServer server) {
if (server == null) {
final String message = "given server must not be null"; // NOI18N
LOG.error(message);
throw new IllegalArgumentException(message);
}
this.server = server;
executor = new HistoryExecutor();
addShutdown(new AbstractShutdownable() {
@Override
protected void internalShutdown() throws ServerExitError {
if (LOG.isDebugEnabled()) {
LOG.debug("shutting down HistoryServer"); // NOI18N
}
executor.shutdown();
try {
if (!executor.awaitTermination(15, TimeUnit.SECONDS)) {
final String message =
"executor did not terminate, history may be incomplete: active tasks: " // NOI18N
+ executor.getActiveCount()
+ " || tasks in queue: " // NOI18N
+ executor.getTaskCount();
LOG.error(message);
throw new ServerExitError(message);
}
} catch (final InterruptedException ex) {
final String message = "could not await HistoryExecutor termination, history may be incomplete"; // NOI18N
LOG.error(message, ex);
throw new ServerExitError(message, ex);
}
}
});
}
//~ Methods ----------------------------------------------------------------
/**
* Returns the history of the given object of the given class. The number of historic elements that will be
* retrieved depends on the given element count and the amount of available historic elements. Resolution strategy:
*
* <ul>
* <li>elements < 1: order by timestamp</li>
* <li>elements > 0: order by timestamp limit <code>elements</code></li>
* </ul>
*
* <p>This operation initialises the history of the object if its class is history enabled and it is not initialised
* yet. Thus this operation never returns an empty list but always at least one object in case of a history enabled
* status.</p>
*
* @param classId the id of the desired class
* @param objectId the id of the object of the desired class
* @param usr the {@link User} that requests the history
* @param elements the number of historic elements to be retrieved or an int < 1 to retrieve all available
* elements
*
* @return the historic objects or <code>null</code> if the class is not history enabled
*
* @throws HistoryException
* <ul>
* <li>if the given <code>User</code> is null</li>
* <li>if the classcache did not provide a class (e.g. because the classid is
* unknown/invalid)</li>
* <li>if the user does not have read permissions for given class</li>
* <li>if any other error occurs</li>
* </ul>
*
* @see ClassAttribute#HISTORY_ENABLED
* @see #initHistory(Sirius.server.middleware.types.MetaObject, Sirius.server.newuser.User, java.util.Date)
*/
public HistoryObject[] getHistory(final int classId, final int objectId, final User usr, final int elements)
throws HistoryException {
if (usr == null) {
final String message = "given user must not be null"; // NOI18N
LOG.error(message);
throw new HistoryException(message);
}
final Sirius.server.localserver._class.Class clazz;
try {
clazz = server.getClassCache().getClass(classId);
} catch (final Exception ex) {
final String message = "cannot get class for id: " + classId; // NOI18N
LOG.error(message, ex);
throw new HistoryException(message, ex);
}
if (clazz == null) {
final String message = "cannot get class for id: " + classId; // NOI18N
LOG.error(message);
throw new HistoryException(message);
}
final PermissionHolder permHolder = clazz.getPermissions();
if (permHolder == null) {
final String message = "no permissionsholder set for class: " + clazz; // NOI18N
LOG.error(message);
throw new HistoryException(message);
}
if (!permHolder.hasReadPermission(usr.getUserGroup())) {
final String message = "given user's usergroup has no read permission for class: " + clazz // NOI18N
+ " || user: " + usr; // NOI18N
LOG.warn(message);
throw new HistoryException(message);
}
if (clazz.getClassAttribute(ClassAttribute.HISTORY_ENABLED) != null) {
final DBConnection con = server.getConnectionPool().getDBConnection();
ResultSet set = null;
try {
final int expectedElements;
if (elements < 1) {
set = con.submitInternalQuery(DBConnection.DESC_FETCH_HISTORY, classId, objectId);
expectedElements = 15;
} else {
set = con.submitInternalQuery(DBConnection.DESC_FETCH_HISTORY_LIMIT, classId, objectId, elements);
expectedElements = elements;
}
final List<HistoryObject> objects = new ArrayList<HistoryObject>(expectedElements);
while (set.next()) {
final String jsonData = set.getString(1);
final Date timestamp = new Date(set.getTimestamp(2).getTime());
objects.add(new HistoryObject(clazz, jsonData, timestamp));
}
// if objects is empty we have to init the history for the object
if (objects.isEmpty()) {
initHistory(getMetaObject(classId, objectId, usr), usr, new Date());
// add the initial object
DBConnection.closeResultSets(set);
set = con.submitInternalQuery(DBConnection.DESC_FETCH_HISTORY_LIMIT, classId, objectId, 1);
set.next();
final String jsonData = set.getString(1);
final Date timestamp = new Date(set.getTimestamp(2).getTime());
objects.add(new HistoryObject(clazz, jsonData, timestamp));
}
return objects.toArray(new HistoryObject[objects.size()]);
} catch (final SQLException e) {
final String message = "cannot fetch history elements for class: " + clazz; // NOI18N
LOG.error(message, e);
throw new HistoryException(message, e);
} finally {
DBConnection.closeResultSets(set);
}
}
return null;
}
/**
* Determines if there are any history entries for the given {@link MetaObject}. This operation does not care about
* the {@link ClassAttribute#HISTORY_ENABLED} flag. It simply looks up whether there are entries in the database or
* not.<br/>
* <b>NOTE: This operation does not initialise the history!</b><br/>
* <br/>
* <b>IMPORTANT: This operation should not be exposed directly through server api/middleware since it is not
* protected by permission check</b>
*
* @param mo the <code>MetaObject</code> to check
*
* @return true if there is at least one historic entry, false otherwise
*
* @throws HistoryException if the given <code>MetaObject</code> is null or any error occurs during database query
*/
public boolean hasHistory(final MetaObject mo) throws HistoryException {
if (mo == null) {
final String message = "given MetaObject must not be null"; // NOI18N
LOG.error(message);
throw new HistoryException(message);
}
final int classId = mo.getClassID();
final int objectId = mo.getID();
ResultSet set = null;
try {
set = server.getConnectionPool().getDBConnection()
.submitInternalQuery(DBConnection.DESC_HAS_HISTORY, classId, objectId);
// only one result - the count
set.next();
final int count = set.getInt(1);
return count > 0;
} catch (final SQLException ex) {
final String message = "cannot determine history status for metaobject: " + mo; // NOI18N
LOG.error(message, ex);
throw new HistoryException(message, ex);
} finally {
DBConnection.closeResultSets(set);
}
}
/**
* Classes cannot be resolved by a DBServer instance as long as the Navigator is not connected since it has the only
* {@link MetaClassCacheService} implementation to date ({@link NavigatorMetaClassService}). This is rather bad
* design as the server should be able to run independently from the navigator
*
* @param classId DOCUMENT ME!
* @param objectId DOCUMENT ME!
* @param usr DOCUMENT ME!
*
* @return DOCUMENT ME!
*
* @throws HistoryException DOCUMENT ME!
*/
private MetaObject getMetaObject(final int classId, final int objectId, final User usr) throws HistoryException {
try {
final MetaObject mo = server.getObject(objectId + "@" + classId, usr.getUserGroup());
if (mo == null) {
throw new HistoryException("server did not provide metaobject: classId: " + classId // NOI18N
+ " || objectId: " + objectId // NOI18N
+ " || usr: " + usr); // NOI18N
}
final MetaClass[] allReadableMCs = server.getClasses(usr.getUserGroup());
assert allReadableMCs.length > 0 : "at least the metaclass of the metaobject must be readable"; // NOI18N
mo.setAllClasses(DomainServerImpl.getClassHashTable(allReadableMCs, allReadableMCs[0].getDomain()));
return mo;
} catch (final Exception ex) {
final String message = "cannot create object for history initialisation: classId: " + classId // NOI18N
+ " || objectId: " + objectId // NOI18N
+ " || usr: " + usr; // NOI18N
LOG.error(message, ex);
throw new HistoryException(message, ex);
}
}
/**
* Creates an initial entry for the given {@link MetaObject}. Basically it does the same as the
* {@link #enqueueEntry(Sirius.server.middleware.types.MetaObject, Sirius.server.newuser.User, java.util.Date) }
* operation except that it checks whether there is an entry already, actually runs the insertion if not and throws
* an exception if an error occured.
*
* @param mo the metaobject that shall be historicised
* @param usr the user that implicitely creates the history entry
* @param timestamp the timestamp when the entry is created
*
* @throws HistoryException if the given <code>MetaObject</code> is null or the given <code>Date</code> is null or
* an error occurred during history insertion
*
* @see #enqueueEntry(Sirius.server.middleware.types.MetaObject, Sirius.server.newuser.User, java.util.Date)
*/
public void initHistory(final MetaObject mo, final User usr, final Date timestamp) throws HistoryException {
if ((mo == null) || (timestamp == null)) {
final String message = "mo or timestamp must not be null: " // NOI18N
+ "mo: " + mo // NOI18N
+ " || user: " + usr // NOI18N
+ " || date: " + timestamp; // NOI18N
LOG.error(message);
throw new HistoryException(message);
}
if ((mo.getMetaClass().getClassAttribute(ClassAttribute.HISTORY_ENABLED) != null) && !hasHistory(mo)) {
final MetaObject dbMo = getMetaObject(mo.getClassID(), mo.getID(), usr);
final HistoryRunner runner = getRunner(dbMo, usr, timestamp);
if (LOG.isDebugEnabled()) {
LOG.debug("init history entry: " + runner); // NOI18N
}
runner.run();
if (runner.executionException != null) {
throw runner.executionException;
}
}
}
/**
* Returns rather fast and only enqueues the entry instead of actually creating the history entry. If the
* {@link MetaObject}'s {@link MetaClass} is not history enabled at all nothing will be done. If the "anonymous"
* option is enabled the given user is ignored.<br/>
* <br/>
* <b>NOTE: This operation does not initialise the history!</b>
*
* @param mo the metaobject that shall be historicised
* @param user the user that implicitely creates the history entry
* @param timestamp the timestamp when the entry is created
*
* @see ClassAttribute#HISTORY_ENABLED
* @see ClassAttribute#HISTORY_OPTION_ANONYMOUS
*/
public void enqueueEntry(final MetaObject mo, final User user, final Date timestamp) {
final HistoryRunner runner = getRunner(mo, user, timestamp);
if (runner != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("enqueue history entry: " + runner); // NOI18N
}
executor.execute(runner);
}
}
/**
* Creates a new {@link HistoryRunner} for the {@link MetaObject} if the object's class has the history enabled
* attribute set. If the anonymous option is set the given user is ignored.
*
* @param mo the metaobject that shall be historicised
* @param user the user that implicitely creates the history entry
* @param timestamp the timestamp when the entry is created
*
* @return an initialised <code>HistoryRunner</code> or null if the history enabled attribute is not set
*
* @throws IllegalArgumentException if the <code>MetaObject</code> or the timestamp is null
*
* @see ClassAttribute#HISTORY_ENABLED
* @see ClassAttribute#HISTORY_OPTION_ANONYMOUS
*/
private HistoryRunner getRunner(final MetaObject mo, final User user, final Date timestamp) {
if ((mo == null) || (timestamp == null)) {
throw new IllegalArgumentException("mo or timestamp must not be null: " // NOI18N
+ "mo: " + mo // NOI18N
+ " || user: " + user // NOI18N
+ " || date: " + timestamp); // NOI18N
}
final ClassAttribute histEnabled = mo.getMetaClass().getClassAttribute(ClassAttribute.HISTORY_ENABLED);
if (histEnabled == null) {
return null;
} else {
final String anonymous = histEnabled.getOptions().get(ClassAttribute.HISTORY_OPTION_ANONYMOUS);
final User historyUser;
if (Boolean.TRUE.toString().equalsIgnoreCase(anonymous)) {
historyUser = null;
} else {
historyUser = user;
}
return new HistoryRunner(mo, historyUser, timestamp);
}
}
//~ Inner Classes ----------------------------------------------------------
/**
* DOCUMENT ME!
*
* @version $Revision$, $Date$
*/
private final class HistoryRunner implements Runnable {
//~ Instance fields ----------------------------------------------------
private final transient MetaObject mo;
private final transient User user;
private final transient Date timestamp;
private transient HistoryException executionException;
//~ Constructors -------------------------------------------------------
/**
* Creates a new HistoryRunner object.
*
* @param mo DOCUMENT ME!
* @param user DOCUMENT ME!
* @param timestamp DOCUMENT ME!
*/
public HistoryRunner(final MetaObject mo, final User user, final Date timestamp) {
this.mo = mo;
this.user = user;
this.timestamp = timestamp;
executionException = null;
}
//~ Methods ------------------------------------------------------------
@Override
public void run() {
try {
if (LOG.isDebugEnabled()) {
LOG.debug("creating history: " + this); // NOI18N
}
final int classId = mo.getClassID();
final int objectId = mo.getId();
final Integer usrId = (user == null) ? null : user.getId();
final Integer ugId = (user == null) ? null : user.getUserGroup().getId();
final Timestamp valid_from = new Timestamp(timestamp.getTime());
final String jsonData = mo.isPersistent() ? mo.getBean().toJSONString() : JSON_DELETED;
final DBConnection con = server.getConnectionPool().getDBConnection();
final int result = con.submitInternalUpdate(
DBConnection.DESC_INSERT_HISTORY_ENTRY,
classId,
objectId,
usrId,
ugId,
valid_from,
jsonData);
if (LOG.isDebugEnabled()) {
LOG.debug("history entry insertion result: " + result); // NOI18N
}
} catch (final Exception e) {
executionException = new HistoryException("could not create history entry: " // NOI18N
+ "mo: " + mo // NOI18N
+ " || user: " + user // NOI18N
+ " || date: " + timestamp, // NOI18N
e);
}
}
@Override
public String toString() {
return "history runner: mo: " + mo + " || user: " + user + " || timestamp: " + timestamp; // NOI18N
}
}
/**
* DOCUMENT ME!
*
* @version $Revision$, $Date$
*/
private static final class HistoryExecutor extends ThreadPoolExecutor {
//~ Constructors -------------------------------------------------------
/**
* Creates a new HistoryExecutor object.
*/
public HistoryExecutor() {
super(0, 100, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}
//~ Methods ------------------------------------------------------------
@Override
protected void afterExecute(final Runnable r, final Throwable t) {
super.afterExecute(r, t);
if (r instanceof HistoryRunner) {
final HistoryRunner runner = (HistoryRunner)r;
if (runner.executionException != null) {
LOG.error(runner.executionException.getMessage(), runner.executionException);
}
}
}
}
}