/*
* #!
* Ontopia Webed
* #-
* Copyright (C) 2001 - 2013 The Ontopia Project
* #-
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* !#
*/
package net.ontopia.topicmaps.webed.impl.utils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpSession;
import net.ontopia.topicmaps.core.TMObjectIF;
import net.ontopia.topicmaps.core.TopicMapIF;
import net.ontopia.topicmaps.entry.TopicMapReferenceIF;
import net.ontopia.topicmaps.nav2.core.UserIF;
import net.ontopia.utils.ObjectUtils;
import net.ontopia.utils.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* INTERNAL: Class to handle basic lock controlling for objects in the web
* editor framework.
*/
public class NamedLockManager {
// initialization of logging facility
private static Logger logger = LoggerFactory.getLogger(NamedLockManager.class
.getName());
private Map locked; // key: Object(locked object), value: UserIF
private Map nameLocks; // key: String(name), value: NamedLock
private Map userLocks; // key: UserIF(user), value: NamedLock[]
private Map objectLocks; // key: Object(locked object), value: NamedLock
private UniqueStringCreator suffixCreator = new UniqueStringCreator();
public NamedLockManager() {
locked = CollectionUtils.createConcurrentMap();
nameLocks = CollectionUtils.createConcurrentMap();
userLocks = CollectionUtils.createConcurrentMap();
objectLocks = CollectionUtils.createConcurrentMap();
logger.info("NamedLockManager initialised");
}
/**
* INTERNAL: Attempts to lock all the objects in the input collection and
* assigns name to the collection for later retrieval.
*/
public synchronized LockResult attemptToLock(UserIF user, Collection objects,
String nameBase,
HttpSession session) {
if (nameBase == null)
throw new IllegalArgumentException("Lock name must be specified.");
String name = nameBase + "$" + suffixCreator.getNextUniqueString();
objects = makeSerializable(objects);
// Remove expired locks
expireNamedLocks(session);
// Attempt to lock objects
logger.debug("locking '" + name + "' for " + user);
Collection unlockable = _attemptToLock(user, objects);
if (!unlockable.isEmpty())
// Let the client know that this lock was never given in the first place.
name += "-unlocked";
if (unlockable.isEmpty()) {
// Update named lock map
NamedLock nlock = createNamedLock(name, user, objects, session);
nameLocks.put(name, nlock);
if (logger.isDebugEnabled())
logger.debug("--> (lock) " + nameLocks);
logger.info("Registered locked objects under '" + name + "'.");
// Update list of user's locks
Collection nlocks = (Collection)userLocks.get(user);
if (nlocks == null) {
nlocks = CollectionUtils.createConcurrentSet();
userLocks.put(user, nlocks);
}
nlocks.add(nlock);
}
Iterator unlockableIt = unlockable.iterator();
Object firstUnlockable = unlockableIt.hasNext() ?
unlockableIt.next() : null;
NamedLock unlockableLock = (firstUnlockable == null ? null : (NamedLock)objectLocks.get(firstUnlockable));
return new LockResult(unlockable, unlockableLock, name);
}
private synchronized Collection _attemptToLock(Object user, Collection objects) {
Collection unlockable = null;
Object lockednow[] = new Object[objects.size()];
int lockedCount = 0;
Iterator it = objects.iterator();
while (it.hasNext()) {
Object object = it.next();
Object _user = locked.get(object);
if (_user != null && !_user.equals(user)) {
if (unlockable == null)
unlockable = new HashSet();
unlockable.add(object);
} else {
logger.debug("Locking " + object + " for " + user);
locked.put(object, user);
lockednow[lockedCount++] = object;
}
} // while
// unlock locked objects
if (unlockable != null) {
for (int ix = 0; ix < lockedCount; ix++) {
logger.debug("Unlocking " + lockednow[ix] + " for " + user);
locked.remove(lockednow[ix]);
}
}
return (unlockable == null ? Collections.EMPTY_SET : unlockable);
}
private NamedLock createNamedLock(String name, UserIF user,
Collection objects, HttpSession session) {
NamedLock namedLock = new NamedLock(name, user, objects);
namedLock.setExpiry(session);
Iterator objectsIt = objects.iterator();
while (objectsIt.hasNext()) {
Object currentObject = objectsIt.next();
objectLocks.put(currentObject, namedLock);
}
return namedLock;
}
/**
* INTERNAL:
* If forced is false, unlocks the lock with the given 'name' for the given
* user.
* If forced is true, unlocks all locks starting with the stem of 'name', the
* stem being defined as everything before and including the last '$'
* character. These locks are unlocked independent on which user locked them.
*/
public synchronized void unlock(UserIF user, String name, boolean forced) {
if (forced) {
logger.warn("Forced unlock of " + name);
Collection allNames = namesWithSameStem(name);
Iterator allNamesIt = allNames.iterator();
while (allNamesIt.hasNext()) {
String currentName = (String)allNamesIt.next();
NamedLock namedLock = (NamedLock)nameLocks.get(currentName);
_unlock(namedLock.getUser(), currentName);
}
logger.warn("Forced unlock completed.");
} else
_unlock(user, name);
}
/**
* INTERNAL:
* Finds all names in the nameLocks.keySet() that start with the stem of
* 'name'. The stem is defined as everything before and including the last '$'
* character in 'name'.
* @param name The source name.
* @return All names having the same stem as 'name'.
*/
private Collection namesWithSameStem(String name) {
String stemmedName = name.substring(0, name.lastIndexOf("$") + 1);
Collection retVal = new HashSet();
Iterator namesIt = nameLocks.keySet().iterator();
while (namesIt.hasNext()) {
String currentName = (String)namesIt.next();
if (currentName.startsWith(stemmedName));
retVal.add(currentName);
}
return retVal;
}
private void _unlock(UserIF user, String name) {
logger.debug("unlocking '" + name + "' for " + user);
NamedLock nlock = (NamedLock) nameLocks.get(name);
if (logger.isDebugEnabled())
logger.debug("--> (unlock) " + nameLocks);
if (nlock != null) {
// update named lock map
logger.debug("Trying to unlock objects from '" + name + "'.");
// Update list of user's locks
// IMPORTANT: This needs to happen before the canUnlock() call below.
Collection ulocks = (Collection)userLocks.get(user);
ulocks.remove(nlock);
// Unlock objects
_unlock(user, canUnlock(user, nlock.objects));
nameLocks.remove(name);
Iterator lockedIt = nlock.objects.iterator();
while (lockedIt.hasNext()) {
Object currentObject = lockedIt.next();
if (objectLocks.get(currentObject) == nlock)
objectLocks.remove(currentObject);
}
} else
logger.warn("Unlocking not possible, '" + name + "' not known.");
}
private void _unlock(Object user, Collection objects) {
Set unlocked = new HashSet();
Iterator it = objects.iterator();
while (it.hasNext()) {
Object object = it.next();
if (unlocked.contains(object))
continue;
// unlock individual object
try {
logger.debug("Unlocking " + object + " for " + user);
Object _user = locked.get(object);
if (_user == null) {
logger.warn("User " + user + " attempted to unlock object '" + object
+ "' which was not locked");
} else if (!_user.equals(user)) {
logger.warn("Attempted to unlock object '" + object
+ "' which was locked by a " + "different user");
} else {
locked.remove(object);
}
} catch (Throwable e) {
e.printStackTrace();
}
unlocked.add(object);
}
}
private Collection canUnlock(UserIF user, Collection unlockCandidates) {
Collection canUnlock = new HashSet(unlockCandidates);
// Remove from canUnlock, candidates that should remain locked by this user.
Collection userNamedLocks = (Collection)userLocks.get(user);
Iterator userNamedLocksIt = userNamedLocks.iterator();
while (userNamedLocksIt.hasNext()) {
NamedLock lock = (NamedLock)userNamedLocksIt.next();
canUnlock.removeAll(lock.objects);
}
return canUnlock;
}
/**
* INTERNAL: Returns true if the user owns the given lock.
*/
public synchronized boolean ownsLock(UserIF user, String name) {
NamedLock lock = (NamedLock) nameLocks.get(name);
if (lock == null)
return false;
else
return lock.user.equals(user);
}
/**
* INTERNAL: Releases all the locks that the user owns.
*/
public synchronized void releaseLocksFor(UserIF user) {
logger.debug("unlocking objects held by " + user);
Collection releaseableLocks = new ArrayList();
Iterator lockedIterator = nameLocks.keySet().iterator();
while (lockedIterator.hasNext()) {
String name = (String)lockedIterator.next();
NamedLock lock = (NamedLock)nameLocks.get(name);
if (user.equals(lock.user))
releaseableLocks.add(name);
}
Iterator iter = releaseableLocks.iterator();
while (iter.hasNext()) {
String name = (String) iter.next();
unlock(user, name, false);
}
}
// --- Internal methods
/**
* INTERNAL: Returns true if the session has support for time-based
* lock expiry. For internal and testing purposes only.
*/
public static boolean usesTimedLockExpiry(HttpSession session) {
// Lock expiration is only carried out in containers
// that do not support servlets 2.3, i.e. J2EE 1.2
return session.getServletContext().getMajorVersion() <= 2
&& session.getServletContext().getMinorVersion() < 3;
}
/**
* INTERNAL: Returns the number of locks held by the user. For
* testing purposes only.
*/
public synchronized int lockCountFor(Object user) {
Collection ulocks = (Collection)userLocks.get(user);
if (ulocks == null)
return 0;
else
return ulocks.size();
}
private void expireNamedLocks(HttpSession session) {
if (!NamedLockManager.usesTimedLockExpiry(session))
return;
Iterator it = nameLocks.keySet().iterator();
while (it.hasNext()) {
String name = (String)it.next();
NamedLock lock = (NamedLock) nameLocks.get(name);
if (lock.hasExpired()) {
logger.debug("Lock '" + name + "' expired");
unlock(lock.user, name, false);
}
}
}
private Collection makeSerializable(Collection objects) {
Collection result = new ArrayList(objects.size());
Iterator iter = objects.iterator();
while (iter.hasNext()) {
Object object = iter.next();
if (object instanceof TMObjectIF) {
result.add(new TMObjectIFHandle((TMObjectIF)object));
} else {
result.add(object);
}
}
return result;
}
static class TMObjectIFHandle {
private String objectId;
private String topicmapId;
private String referenceId;
public TMObjectIFHandle(TMObjectIF o) {
this.objectId = o.getObjectId();
TopicMapIF tm = o.getTopicMap();
this.topicmapId = tm.getObjectId();
TopicMapReferenceIF ref = tm.getStore().getReference();
if (ref != null)
this.referenceId = ref.getId();
}
public boolean equals(Object o) {
if (!(o instanceof TMObjectIFHandle)) return false;
TMObjectIFHandle other = (TMObjectIFHandle)o;
return (ObjectUtils.equals(this.objectId, other.objectId) &&
ObjectUtils.equals(this.topicmapId, other.topicmapId) &&
ObjectUtils.equals(this.referenceId, other.referenceId));
}
public int hashCode() {
return objectId.hashCode();
}
}
/**
* INTERNAL: For testing purposes only.
*/
public void clear() {
locked.clear();
nameLocks.clear();
userLocks.clear();
objectLocks.clear();
}
}