/*
* Copyright 2012 Anton Van Zyl. http://code.google.com/p/java-swiss-knife/
*
* 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.
* under the License.
*/
package com.knife.web.threading;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.codec.binary.Hex;
import com.knife.web.threading.exception.InvalidRequestTokenException;
/**
* This is a thread lock that only allows one thread to enter and ignoring extra
* threads until the first thread is completed and then letting the user
* continue to the result page.
*
* <br/>
* <br/>
* Please visit <a
* href="http://code.google.com/p/java-swiss-knife/">Java-Swiss-Knife</a> and
* comment, rate, contribute or raise a issue/enhancement for my library. <br/>
*
* @author Anton Van Zyl
*
*/
public class DoubleSubmitLock {
private static final String SESSION_REQ_ID = "session_req_id";
private static SecureRandom secureRandom;
private final String uniqueRequestId;
private final Lock controlLock = new ReentrantLock();
private final Lock mainLock = new ReentrantLock();
private final Lock subLock = new ReentrantLock();
private final Condition cond = subLock.newCondition();
private boolean finished;
private String jspReturnPage;
private Map<String, Object> jspReturnPageData = new HashMap<String, Object>();
static {
try {
secureRandom = SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException e) {
System.out.println("getUniqueRequestIdenitfier: " + e.getMessage());
e.printStackTrace();
}
}
public DoubleSubmitLock(String uniqueRequestId) {
this.uniqueRequestId = uniqueRequestId;
}
public boolean isSameRequestId(String requestId) {
return uniqueRequestId.equalsIgnoreCase(requestId);
}
public boolean tryGetLock(String errorJspPage, Map<String, Object> jspErrorPageData) throws InterruptedException {
// obtain controlLock so we can also get subLock before the main thread
// can signal release - and cause us to wait forever
controlLock.lock();
if (mainLock.tryLock()) {
System.out.println("Lock obtained [id=" + uniqueRequestId + "]");
jspReturnPage = errorJspPage;
jspReturnPageData = jspErrorPageData;
controlLock.unlock();
return true;
} else {
try {
System.out.println("Lock NOT obtained; blocking [id=" + uniqueRequestId + "]");
subLock.lock();
// we have the subLock; can release controlLock now before we
// enter the wait
controlLock.unlock();
cond.await();
System.out.println("Blocking released [id=" + uniqueRequestId + "]");
} finally {
subLock.unlock();
}
}
return false;
}
public void unlock() {
unlock(true);
}
public void unlock(boolean completed) {
controlLock.lock();
System.out.println("Lock released [id=" + uniqueRequestId + "; completed=" + completed + "]");
try {
finished = completed;
subLock.lock();
try {
cond.signalAll();
} finally {
subLock.unlock();
}
} finally {
controlLock.unlock();
}
}
/**
* @return the current JSP page
*/
public String getJspReturnPage() {
return this.jspReturnPage;
}
/**
* @return The data the JSP page need to render correctly
*/
public Map<String, Object> getJspReturnPageData() {
return this.jspReturnPageData;
}
/**
*
* @param jspReturnPage
*/
public void setJspReturnPage(String jspReturnPage) {
this.jspReturnPage = jspReturnPage;
}
/**
*
* @param jspReturnPageData
*/
public void setJspReturnPageData(Map<String, Object> jspReturnPageData) {
this.jspReturnPageData = jspReturnPageData;
}
/**
* @return Test if request is processed
*/
public boolean isRequestAlreadyProcessed() {
controlLock.lock();
try {
return finished;
} finally {
controlLock.unlock();
}
}
/**
* Generates a random Token and adds it to the session to be used in
* validating incoming requests.<br/>
* Use the validateUniqueRequestIdentifier method to validate the key value
* against the session.<br/>
*/
public static String createDoubleSubmitLock(HttpServletRequest request, String callerId) {
byte[] bytes = new byte[32];
secureRandom.nextBytes(bytes);
String uniqueRequestId = Hex.encodeHexString(bytes);
System.out.println("Created lock [id=" + uniqueRequestId + "]");
request.getSession().setAttribute(getSessionId(callerId), new DoubleSubmitLock(uniqueRequestId));
return uniqueRequestId;
}
private static String getSessionId(String callerId) {
return SESSION_REQ_ID + ":" + callerId;
}
/**
* This will validate the key in the session and if invalid throw an
* exception to be handled and send the user to a generic double submit
* page.<br/>
*
* @throws InvalidRequestTokenException
*/
public static DoubleSubmitLock getDoubleSubmitLock(HttpServletRequest request, String callerId, String requestId) throws InvalidRequestTokenException {
DoubleSubmitLock dsl = (DoubleSubmitLock) request.getSession().getAttribute(getSessionId(callerId));
if ((dsl == null) || (!dsl.isSameRequestId(requestId))) {
String msg = "DoubleSubmitLock not bound to session [callerId=" + callerId + "; requestId=" + requestId + "]";
System.out.println(msg);
throw new InvalidRequestTokenException(msg);
}
return dsl;
}
}