/* ========================================================================== *
* Copyright (C) 2006, 2007 TAO Consulting Pte <http://www.taoconsulting.sg/> *
* All rights reserved. *
* ========================================================================== *
* *
* 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 biz.taoconsulting.dominodav;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import biz.taoconsulting.dominodav.interfaces.IDAVAddressInformation;
import biz.taoconsulting.dominodav.interfaces.IDAVResource;
/**
* @author Bastian Buch (TAO Consulting), Stephan H. Wissel Implemented as Singleton
*/
public class LockManager {
/**
*
* Run periodical cleanup of the LockManager
*
* @author Stephan H. Wissel
*
*/
private class LockCleaner extends TimerTask {
/**
* The parent class lockmanager
*/
private LockManager lockmanager;
/**
* The timer for the cleaning interval
*/
private Timer timer = null;
/**
*
* @param lockmanager
* The LockManager to be cleaned
*/
LockCleaner(LockManager lockmanager) {
this.lockmanager = lockmanager;
}
/**
* Scheduled execution of the cleaner
*/
public void run() {
// Do a cleanup
this.lockmanager.clean();
}
/**
* Start the cleanup task
*
*/
public void startCleaning() {
try {
this.timer = new Timer();
this.timer.schedule(this, 0, 30000);
} catch (Exception e) {
LOGGER.error("StartClean failed", e);
}
}
}
/**
* What is our maximum timeout value we accept
*/
public static final Long MAX_LOCK_DURATION_SEC = new Long(300); // 5 minutes
/**
* The logger object for event logging
*/
private static final Log LOGGER = LogFactory.getLog(LockManager.class);
/**
* Table with currently locked elements
*/
private Hashtable<String, LockInfo> lockTable;
/**
* The lock cleaner background task
*
*/
private LockCleaner lockcleaner;
/**
* The internal Object to hold the singleTon
*/
private static LockManager internalLockManager;
/**
*
* @return LockManager - the single instance
*/
public static synchronized LockManager getLockManager() {
// Initialize the lock Manager Singleton
if (internalLockManager == null) {
internalLockManager = new LockManager();
}
return internalLockManager;
}
/**
* Default constructor is private to implement a singleton
*/
private LockManager() {
this.lockTable = new Hashtable<String, LockInfo>();
this.lockcleaner = new LockCleaner(this);
this.lockcleaner.startCleaning();
}
/**
*
*/
public synchronized void clean() {
List<LockInfo> morituri = new ArrayList<LockInfo>();
// We collect all the keys to delete in
for (Map.Entry<String, LockInfo> entry : this.lockTable.entrySet()) {
LockInfo li = entry.getValue();
if (li.isExpired()) {
morituri.add(li);
}
}
// Now we have them, unlock them all
try {
for (LockInfo li : morituri) {
this.unlock(li.getPathinfo(), li.getToken());
}
} catch (ConcurrentModificationException c) {
return;
}
}
/**
* Make sure our singleton can't be cloned
*
* @return actually nothing -- throws an exception
* @throws CloneNotSupportedException
* -- we don't Clown around
*/
public Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
// that'll teach 'em
}
/**
*
* @param res
* Resource to check
* @return Owner of the lock, null if not locked
*/
public String getLockOwner(IDAVResource res) {
String lockKey = ((IDAVAddressInformation) res).getInternalAddress();
boolean isLocked = this.lockTable.containsKey(lockKey);
if (isLocked) {
LockInfo li = this.lockTable.get(lockKey);
return li.getUsername();
}
return null;
}
/**
*
* @param res
* Resource to check
* @return Status of the lock
*/
public boolean isLocked(IDAVResource res) {
return this.lockTable.containsKey(((IDAVAddressInformation) res)
.getInternalAddress());
}
/**
*
* @param path
* Resource to check
* @return Lockstatus
*/
public boolean isLocked(String path) {
return this.lockTable.containsKey(path);
}
/**
* Creates a new lock and returns it when successful
*
* @param resource
* @param lockrequestorName
* - can be different from logged in username
* @param timeOutValue
* The timeout value requested
* @return a lock object or null if it didn't work
*/
public synchronized LockInfo lock(IDAVResource resource,
String lockrequestorName, Long timeOutValue) {
LockInfo li = new LockInfo(resource);
li.setTimeout(timeOutValue);
li.setLocalUsername(lockrequestorName);
if (this.lock(li)) {
return li;
}
// Didn't work, so we don't give back the lock-info
return null;
}
/**
* @param li
* - LockInfo we want to use to lock
* @return true if it worked
*
* Even the same user can lock only once otherwise you overwrite
* your own work
*/
private synchronized boolean lock(LockInfo li) {
String curPath = li.getPathinfo();
if (this.lockTable.containsKey(curPath)) {
return false;
}
// Lock it in
li.extenExpiryDate(); // Update the lock
this.lockTable.put(curPath, li);
return true;
}
/**
* @param href
* Resource to lock
* @param token
* Existing lock token
* @param timeout
* timeout in senconds
* @return LockInfo Object
*/
public synchronized LockInfo relock(IDAVResource resource, String token,
long timeout) {
String href = ((IDAVAddressInformation) resource).getInternalAddress();
if (this.lockTable.containsKey(href)) {
LockInfo li = this.lockTable.get(href);
// You have to present the token properly
// ToDo: Should we also check for the username?
String oldToken = li.getToken();
boolean isStillValid = !li.isExpired();
if (oldToken.equals(token) && isStillValid) {
li.extenExpiryDate();
return li;
}
}
// Didn't work for all other cases
return null;
}
/**
* Shuts down the lock manager thread
*
*/
public synchronized void shutdown() {
this.lockcleaner.cancel();
this.lockcleaner = null;
}
/**
* @return Status of the lock
*/
public String status() {
StringBuffer s = new StringBuffer(); // Faster than Strings
Set<String> keys = this.lockTable.keySet();
Iterator<String> it = keys.iterator();
while (it.hasNext()) {
LockInfo li = this.lockTable.get(it.next());
s.append(" \n"
+ li.getUsername()
+ " --> "
+ li.getToken()
+ " ("
+ (DateFormat.getDateTimeInstance(DateFormat.LONG,
DateFormat.LONG).format(li.getExpirydate())) + ")");
}
return s.toString();
}
/**
* Explicit unlock!
*
* @param pathinfo
* What element to unlock
* @param token
* empty if unlock not implicit (expire check)
* @return Success of the unlock operation
*/
public synchronized boolean unlock(String pathinfo /* key */, String token) {
boolean result = false;
// Without a token no unlock
if (token == null) {
return result;
}
if (token.startsWith("<")) {
token = token.substring(1);
token = token.substring(0, token.length() - 1);
}
if (!this.lockTable.containsKey(pathinfo)) {
result = false;
} else {
LockInfo li = this.lockTable.get(pathinfo);
if (li.isExpired() || li.getToken().equals(token)) {
this.lockTable.remove(pathinfo);
}
result = true;
}
// Return the success
return result;
}
}