package org.concord.otrunk.overlay;
import java.awt.EventQueue;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.concord.framework.otrunk.OTID;
import org.concord.framework.otrunk.OTObject;
import org.concord.framework.otrunk.OTObjectService;
import org.concord.framework.otrunk.wrapper.OTInt;
import org.concord.otrunk.OTObjectServiceImpl;
import org.concord.otrunk.OTrunkImpl;
import org.concord.otrunk.OTrunkUtil;
import org.concord.otrunk.datamodel.OTDatabase;
import org.concord.otrunk.datamodel.OTTransientMapID;
import org.concord.otrunk.net.HTTPRequestException;
import org.concord.otrunk.user.OTUserObject;
import org.concord.otrunk.util.StandardPasswordAuthenticator;
import org.concord.otrunk.view.OTConfig;
import org.concord.otrunk.view.OTViewer;
import org.concord.otrunk.xml.XMLDatabase;
public abstract class OTUserOverlayManager
{
private static final Logger logger = Logger.getLogger(OTUserOverlayManager.class.getName());
protected static boolean doHeadBeforeGet = true;
protected static StandardPasswordAuthenticator authenticator = new StandardPasswordAuthenticator();
protected OTrunkImpl otrunk;
protected ArrayList<OverlayImpl> globalOverlays = new ArrayList<OverlayImpl>();
protected HashMap<URL, OTObjectService> overlayToObjectServiceMap = new HashMap<URL, OTObjectService>();
protected HashMap<OTUserObject, URL> userToOverlayMap = new HashMap<OTUserObject, URL>();
protected ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
protected ArrayList<OTUserObject> readOnlyUsers = new ArrayList<OTUserObject>();
protected ArrayList<OTUserObject> writeableUsers = new ArrayList<OTUserObject>();
protected HashMap<OTID, Integer> nonRecurseObjects = new HashMap<OTID, Integer>();
private Map<OTUserObject, ArrayList<OverlayUpdateListener>> listenerMap = Collections.synchronizedMap(new HashMap<OTUserObject, ArrayList<OverlayUpdateListener>>());
private List<OverlayUpdateListener> globalListeners = Collections.synchronizedList(new ArrayList<OverlayUpdateListener>());
/**
* String used as the key in the annotations object map to record number of times a student "submits" an object
*/
public static final String SUBMIT_ANNOTATION = "intrasession-submits";
public static void setHeadBeforeGet(boolean doHead) {
doHeadBeforeGet = doHead;
}
/**
* Add an overlay or overlayreferencemap to the UserOverlayManager. This can be used when you have a URL to an otml snippet which contains an OTOverlay or OTOverlayReferenceMap object
* and you don't want to fetch the object yourself.
* @param overlayURL
* @param userObject
* @param isGlobal
* @throws Exception
*/
public abstract void addReadOnly(URL overlayURL, OTUserObject userObject, boolean isGlobal) throws Exception;
public abstract void addWriteable(URL overlayURL, OTUserObject userObject, boolean isGlobal) throws Exception;
public abstract <T extends OTObject> T getOTObject(OTUserObject userObject, T object) throws Exception;
protected abstract OTObjectService getObjectService(OTUserObject userObject, OTObject object);
public void remove(OTUserObject userObject) {
writeLock();
try {
userObject = getAuthoredObject(userObject);
URL otOverlay = userToOverlayMap.get(userObject);
OTObjectService objService = overlayToObjectServiceMap.get(otOverlay);
otrunk.removeObjectService((OTObjectServiceImpl) objService);
overlayToObjectServiceMap.remove(otOverlay);
readOnlyUsers.remove(userObject);
writeableUsers.remove(userObject);
} finally {
writeUnlock();
}
}
public abstract void reload(OTUserObject userObject) throws Exception;
public long getLastModified(OTUserObject userObject, OTObject object)
{
try {
OTObject otObject = getOTObject(userObject, object);
XMLDatabase db = getXMLDatabase(otObject);
return db.getUrlLastModifiedTime();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
return 0;
}
}
/**
* Copies an object into an overlay and saves the overlay to the remote file. Basically a
* combination of stageObject() followed by remoteSaveStagedObject().
* @param user
* @param object
* @throws Exception
*/
public abstract void remoteSave(OTUserObject user, OTObject object) throws Exception;
// /**
// * Copies an object into an overlay and returns the object accessed through that overlay.
// * This allows further modifications to be made to the object before it is saved remotely.
// * @param user
// * @param object
// * @throws Exception
// */
// public abstract OTObject stageObject(OTUserObject user, OTObject object) throws Exception;
//
// /**
// * Saves the overlay for this staged object.
// * @param user
// * @param object
// * @throws Exception
// */
// public abstract void remoteSaveStagedObject(OTUserObject user, OTObject object) throws Exception;
protected abstract Set<OTUserObject> getAllUsers();
public OTUserOverlayManager(OTrunkImpl otrunk) {
this.otrunk = otrunk;
XMLDatabase.SILENT_DB = true;
}
protected <T extends OTObject> T getAuthoredObject(T object) {
if (object == null) {
return null;
}
try {
object = otrunk.getRuntimeAuthoredObject(object);
} catch (Exception e) {
logger.log(Level.WARNING, "Couldn't get authored version of user object!", e);
}
return object;
}
@SuppressWarnings("unchecked")
protected synchronized <T extends OTObject> T loadRemoteObject(URL url, Class<T> klass) {
writeLock();
try {
T remoteObject = null;
try {
remoteObject = (T) otrunk.getExternalObject(url, otrunk.getRootObjectService(), true);
} catch (ClassCastException e) {
// something is there, but not the type of object expected
throw e;
} catch (Exception e) {
// some error occurred...
logger.warning("Couldn't get overlay for user\n" + url + "\n" + e.getMessage());
}
// if there isn't an overlay object, and it's not supposed to be a global one, go ahead and try to make a default one
if (remoteObject == null) {
// create a blank one
try {
logger.info("Creating empty database on the fly (root: " + klass.getSimpleName() + ")...");
XMLDatabase xmldb = new XMLDatabase();
OTObjectService objService = otrunk.createObjectService(xmldb);
remoteObject = objService.createObject(klass);
xmldb.setRoot(remoteObject.getGlobalId());
otrunk.remoteSaveData(xmldb, url, OTViewer.HTTP_PUT, authenticator);
remoteObject = (T) otrunk.getExternalObject(url, otrunk.getRootObjectService());
} catch (Exception e) {
// still an error. skip the overlay for this user/url
logger.warning("Couldn't create a default overlay for user\n" + url + "\n" + e.getMessage());
}
}
return remoteObject;
} finally {
writeUnlock();
}
}
protected OTObjectService loadOverlay(URL overlayURL, boolean isGlobal) {
writeLock();
try {
// get the OTOverlay OTObject from the otml at the URL specified
OTOverlay overlay = loadRemoteObject(overlayURL, OTOverlay.class);
OTObjectService objService = null;
// if the overlay exists, the create an objectservice for it and register it
if (overlay != null) {
OTDatabase db = registerOverlay(overlay, isGlobal);
objService = createObjectService(db);
overlayToObjectServiceMap.put(overlayURL, objService);
}
return objService;
} finally {
writeUnlock();
}
}
protected CompositeDatabase registerOverlay(OTOverlay overlay, boolean isGlobal) {
writeLock();
try {
// initialize an OverlayImpl with the OTOverlay
OverlayImpl myOverlay = new OverlayImpl(overlay);
if(isGlobal){
globalOverlays.add(myOverlay);
}
// set up the CompositeDatabase
CompositeDatabase db = new CompositeDatabase(otrunk.getDataObjectFinder(), myOverlay);
// if it's not a global overlay, add all the global overlays to its stack of overlays
if(!isGlobal){
ArrayList<Overlay> overlays = new ArrayList<Overlay>();
if (globalOverlays.size() > 0) {
overlays.addAll(globalOverlays);
}
db.setOverlays(overlays);
}
return db;
} finally {
writeUnlock();
}
}
/**
* Creates an OTObjectService object for an OTOverlay
* @param overlay
* @param isGlobal
* @return
*/
protected OTObjectService createObjectService(OTDatabase db) {
writeLock();
try {
// create the OTObjectService and return it
OTObjectService objService = otrunk.createObjectService(db);
return objService;
} finally {
writeUnlock();
}
}
protected OTObjectService getObjectService(OTOverlay overlay) {
readLock();
try {
return overlayToObjectServiceMap.get(overlay);
} finally {
readUnlock();
}
}
protected OTDatabase getDatabase(OTObject object) {
OTObjectServiceImpl objService = (OTObjectServiceImpl) object.getOTObjectService();
return getDatabase(objService);
}
protected OTDatabase getDatabase(OTObjectService objService) {
if (objService != null && objService instanceof OTObjectServiceImpl) {
return ((OTObjectServiceImpl)objService).getCreationDb();
}
return null;
}
protected XMLDatabase getXMLDatabase(OTObject object) {
OTDatabase db = getDatabase(object);
if (db instanceof XMLDatabase) {
return (XMLDatabase) db;
} else if (db instanceof CompositeDatabase) {
return (XMLDatabase) ((CompositeDatabase) db).getActiveOverlayDb();
}
return null;
}
protected XMLDatabase getXMLDatabase(OTObjectService objectService) {
OTDatabase db = getDatabase(objectService);
if (db instanceof XMLDatabase) {
return (XMLDatabase) db;
} else if (db instanceof CompositeDatabase) {
return (XMLDatabase) ((CompositeDatabase) db).getActiveOverlayDb();
}
return null;
}
/* (non-Javadoc)
* @see org.concord.otrunk.overlay.OTUserOverlayManager#pruneDatabase(org.concord.otrunk.overlay.OTOverlay)
*/
protected void pruneDatabase(OTObjectService overlayObjectService) {
OTDatabase db = getDatabase(overlayObjectService);
if (db instanceof CompositeDatabase) {
((CompositeDatabase) db).pruneNonDeltaObjects();
}
}
protected boolean copyObjectIntoOverlay(OTUserObject user, OTObject object, OTObject newObject) {
writeLock();
try {
OTObject newWrappedObject = newObject;
if (newWrappedObject == null) {
newWrappedObject = getChangedWrappedObject(user, object);
}
// only save if there are changes
if (newWrappedObject != null) {
int depth = -1;
boolean onlyChanges = true;
if (nonRecurseObjects.containsKey(getAuthoredId(object))) {
depth = nonRecurseObjects.get(getAuthoredId(object));
onlyChanges = false;
}
((OTObjectServiceImpl) object.getOTObjectService()).copyInto(object, newWrappedObject, depth, onlyChanges);
return true;
}
return false;
} catch (Exception e) {
logger.log(Level.SEVERE, "Couldn't save object into overlay!", e);
return false;
} finally {
writeUnlock();
}
}
/**
* Returns null if the object hasn't changed, otherwise returns the otobject loaded from the overlay
* @return
*/
private OTObject getChangedWrappedObject(OTUserObject user, OTObject obj) throws Exception {
OTObject newWrappedObject = getOTObject(user, obj);
if (OTrunkUtil.compareObjects(newWrappedObject, obj, true)) {
return null;
}
return newWrappedObject;
}
protected void actualRemoteSave(OTObjectService overlayObjectService) throws HTTPRequestException, Exception
{
writeLock();
try {
if (otrunk.isSailSavingDisabled() && ! OTConfig.isIgnoreSailViewMode()) {
logger.info("Not saving overlay because SAIL saving is disabled");
} else {
pruneDatabase(overlayObjectService);
otrunk.remoteSaveData(getXMLDatabase(overlayObjectService), OTViewer.HTTP_PUT, authenticator);
}
} finally {
writeUnlock();
}
}
protected void notifyListeners(final OTUserObject user) {
EventQueue.invokeLater(new Runnable() {
public void run() {
synchronized(globalListeners) {
for (OverlayUpdateListener l : globalListeners) {
l.updated(user);
}
}
synchronized (listenerMap) {
ArrayList<OverlayUpdateListener> listeners = listenerMap.get(user);
if (listeners != null) {
for (OverlayUpdateListener l : listeners) {
l.updated(user);
}
}
}
}
});
}
/**
* register a listener which will get notified when any user's overlay gets reloaded
* @param listener
*/
public void addOverlayUpdateListener(OverlayUpdateListener listener) {
synchronized(globalListeners) {
if (! globalListeners.contains(listener)) {
globalListeners.add(listener);
}
}
}
/**
* register a listener which will get notified when the specified user's overlay gets reloaded
* @param listener
* @param user
*/
public void addOverlayUpdateListener(OverlayUpdateListener listener, OTUserObject user) {
synchronized (listenerMap) {
if (! userToOverlayMap.keySet().contains(user)) {
throw new RuntimeException("Trying to register a listener for a user that's not loaded!");
}
ArrayList<OverlayUpdateListener> currentListeners = listenerMap.get(user);
if (currentListeners == null) {
currentListeners = new ArrayList<OverlayUpdateListener>();
}
if (! currentListeners.contains(listener)) {
currentListeners.add(listener);
}
listenerMap.put(user, currentListeners);
}
}
/**
* remove a listener
* @param listener
*/
public void removeOverlayUpdateListener(OverlayUpdateListener listener) {
synchronized(globalListeners) {
globalListeners.remove(listener);
}
synchronized (listenerMap) {
for (ArrayList<OverlayUpdateListener> listeners : listenerMap.values()) {
listeners.remove(listener);
}
}
}
/**
* Save all user overlays.
* @param object
* @throws Exception
*/
public void remoteSaveAll(OTObject object) throws Exception
{
writeLock();
try {
for (OTUserObject user : writeableUsers) {
remoteSave(user, object);
}
} finally {
writeUnlock();
}
}
public boolean isSubmitted(OTUserObject user, OTObject obj, boolean includeChildren) {
try {
OTObject usersVersion = getOTObject(user, obj);
OTObject annotation = usersVersion.getAnnotations().getObject(SUBMIT_ANNOTATION);
if (annotation != null && annotation instanceof OTInt) {
if (((OTInt)annotation).getValue() > 0) {
return true;
}
} else {
// try old method
return oldIsSubmitted(user, obj, includeChildren);
}
} catch (Exception e) {
logger.log(Level.SEVERE, "Couldn't get object for user", e);
}
return false;
}
private boolean oldIsSubmitted(OTUserObject user, OTObject obj, boolean includeChildren) {
OTObjectService objService = getObjectService(user, obj);
if (objService == null) {
return false;
}
return otrunk.isModified(obj, objService, includeChildren);
}
public void reloadAll() throws Exception {
writeLock();
try {
// use an array of the set, so that the set can be manipulated in the reload method, and we won't
// have concurrent modification problems
Object[] allUsers = readOnlyUsers.toArray();
for (Object user: allUsers) {
reload((OTUserObject)user);
}
} finally {
writeUnlock();
}
}
protected OTID getAuthoredId(OTObject object) {
OTID id = object.getGlobalId();
if (id instanceof OTTransientMapID) {
id = ((OTTransientMapID)id).getMappedId();
}
return id;
}
protected boolean doesUrlNeedReloaded(URL url) throws ProtocolException, IOException {
OTObjectService otObjectService = overlayToObjectServiceMap.get(url);
if (otObjectService == null) {
return true;
}
XMLDatabase db = getXMLDatabase(otObjectService);
return doesDbNeedReloaded(db);
}
protected boolean doesDbNeedReloaded(XMLDatabase xmlDb) throws IOException, ProtocolException {
if (xmlDb == null) {
return false;
}
if (doHeadBeforeGet) {
long existingTime = xmlDb.getUrlLastModifiedTime();
URLConnection conn = xmlDb.getSourceURL().openConnection();
if (conn instanceof HttpURLConnection) {
((HttpURLConnection) conn).setRequestMethod(OTViewer.HTTP_HEAD);
}
long serverTime = conn.getLastModified();
logger.finer("checking reload of: " + xmlDb.getSourceURL());
if (existingTime != 0 && serverTime != 0 && existingTime == serverTime) {
// no reload needed
logger.finer("Not reloading overlay as modified time is the same as the currently loaded version");
return false;
} else {
logger.finer("Modified times indicated reload needed. current: " + existingTime + ", server: " + serverTime);
return true;
}
}
return true;
}
public void addNonRecurseObject(OTObject obj, int depth) {
OTID authoredId = getAuthoredId(obj);
nonRecurseObjects.put(authoredId, depth);
}
public void setReadOnly(OTUserObject user) {
user = getAuthoredObject(user);
writeLock();
try {
if (writeableUsers.remove(user)) {
readOnlyUsers.add(user);
}
} finally {
writeUnlock();
}
}
public void setWriteable(OTUserObject user) {
user = getAuthoredObject(user);
writeLock();
try {
if (readOnlyUsers.remove(user)) {
writeableUsers.add(user);
}
} finally {
writeUnlock();
}
}
protected void incrementSubmitCount(OTObject object) {
OTInt submitCount = (OTInt) object.getAnnotations().getObject(OTUserOverlayManager.SUBMIT_ANNOTATION);
if (submitCount == null) {
try {
submitCount = object.getOTObjectService().createObject(OTInt.class);
submitCount.setValue(0);
object.getAnnotations().putObject(OTUserOverlayManager.SUBMIT_ANNOTATION, submitCount);
} catch (Exception e) {
logger.log(Level.SEVERE, "Couldn't create OTInt object!", e);
return;
}
}
submitCount.setValue(submitCount.getValue() + 1);
}
protected void writeLock() {
readWriteLock.writeLock().lock();
}
protected void writeUnlock() {
readWriteLock.writeLock().unlock();
}
protected void readLock() {
readWriteLock.readLock().lock();
}
protected void readUnlock() {
readWriteLock.readLock().unlock();
}
}