package ch.elexis.core.data.lock;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import org.eclipse.core.runtime.IProgressMonitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.eclipsesource.jaxrs.consumer.ConsumerFactory;
import ch.elexis.core.common.InstanceStatus;
import ch.elexis.core.common.InstanceStatus.STATE;
import ch.elexis.core.constants.Preferences;
import ch.elexis.core.data.activator.CoreHub;
import ch.elexis.core.data.constants.ElexisSystemPropertyConstants;
import ch.elexis.core.data.events.ElexisEvent;
import ch.elexis.core.data.events.ElexisEventDispatcher;
import ch.elexis.core.data.status.ElexisStatus;
import ch.elexis.core.lock.ILocalLockService;
import ch.elexis.core.lock.types.LockInfo;
import ch.elexis.core.lock.types.LockRequest;
import ch.elexis.core.lock.types.LockRequest.Type;
import ch.elexis.core.lock.types.LockResponse;
import ch.elexis.core.model.IPersistentObject;
import ch.elexis.core.server.IInstanceService;
import ch.elexis.core.server.ILockService;
import ch.elexis.data.PersistentObject;
import ch.elexis.data.User;
/**
* ILocalLockService implementation. Managing locks of PersistentObjects.</br>
* If the environment variable <b>ELEXIS_SERVER_REST_INTERFACE_URL</b> is set a connection to a
* remote LockService will used internal.
*
* @author marco
*
*/
public class LocalLockService implements ILocalLockService {
private ILockService ils;
private IInstanceService iis;
private InstanceStatus inst;
private final HashMap<String, Integer> lockCount = new HashMap<String, Integer>();
private final HashMap<String, LockInfo> locks = new HashMap<String, LockInfo>();
private boolean standalone = false;
private Logger logger = LoggerFactory.getLogger(LocalLockService.class);
/**
* A unique id for this instance of Elexis. Changes on every restart
*/
private static final UUID systemUuid = UUID.randomUUID();
private Timer timer;
/**
* Construct a new LocalLockService. Application code should access via
* {@link CoreHub#getLocalLockService()} and <b>NOT</b> create its own instance.
*
*/
public LocalLockService(){
ils = new DenyAllLockService();
timer = new Timer();
timer.schedule(new LockRefreshTask(), 10000, 10000);
inst = new InstanceStatus();
inst.setState(InstanceStatus.STATE.ACTIVE);
inst.setUuid(getSystemUuid());
inst.setVersion(CoreHub.readElexisBuildVersion());
inst.setOperatingSystem(
System.getProperty("os.name") + "/" + System.getProperty("os.version") + "/"
+ System.getProperty("os.arch") + "/J" + System.getProperty("java.version"));
}
public void reconfigure(){
final String restUrl =
System.getProperty(ElexisSystemPropertyConstants.ELEXIS_SERVER_REST_INTERFACE_URL);
if (restUrl != null && restUrl.length() > 0) {
standalone = false;
logger.info("Operating against elexis-server instance on " + restUrl);
ils = ConsumerFactory.createConsumer(restUrl, ILockService.class);
iis = ConsumerFactory.createConsumer(restUrl, IInstanceService.class);
String identId = CoreHub.localCfg.get(Preferences.STATION_IDENT_ID, "");
String identTxt = CoreHub.localCfg.get(Preferences.STATION_IDENT_TEXT, "");
inst.setIdentifier(identTxt + " [" + identId + "]");
} else {
standalone = true;
logger.info("Operating in stand-alone mode.");
}
}
@Override
public LockResponse releaseAllLocks(){
if (standalone) {
return LockResponse.OK;
}
List<LockInfo> lockList = new ArrayList<LockInfo>(locks.values());
for (LockInfo lockInfo : lockList) {
LockRequest lockRequest = new LockRequest(LockRequest.Type.RELEASE, lockInfo);
LockResponse lr = acquireOrReleaseLocks(lockRequest);
if (!lr.isOk()) {
return lr;
}
}
return LockResponse.OK;
}
@Override
public LockResponse releaseLock(IPersistentObject po){
if (po == null) {
return LockResponse.DENIED(null);
}
logger.debug("Releasing lock on [" + po + "]");
return releaseLock(po.storeToString());
}
@Override
public LockResponse releaseLock(LockInfo lockInfo){
if (lockInfo.getElementStoreToString() == null) {
return LockResponse.DENIED(null);
}
logger.debug("Releasing lock on [" + lockInfo.getElementStoreToString() + "]");
return releaseLock(lockInfo.getElementStoreToString());
}
private LockResponse releaseLock(String storeToString){
User user = (User) ElexisEventDispatcher.getSelected(User.class);
LockInfo lil = new LockInfo(storeToString, user.getId(), systemUuid.toString());
LockRequest lockRequest = new LockRequest(LockRequest.Type.RELEASE, lil);
return acquireOrReleaseLocks(lockRequest);
}
@Override
public LockResponse acquireLockBlocking(IPersistentObject po, int secTimeout,
IProgressMonitor monitor){
if (po == null) {
return LockResponse.DENIED(null);
}
if (monitor != null) {
monitor.beginTask("Acquiring Lock for [" + po.getLabel() + "]", (secTimeout * 10) + 1);
}
logger.debug("Acquiring lock blocking on [" + po + "]");
String storeToString = po.storeToString();
LockResponse response = acquireLock(storeToString);
int sleptMilli = 0;
while (!response.isOk()) {
if (response.getStatus() == LockResponse.Status.DENIED_PERMANENT) {
return response;
}
try {
Thread.sleep(100);
sleptMilli += 100;
response = acquireLock(storeToString);
if (sleptMilli > (secTimeout * 1000)) {
return response;
}
// update monitor
if (monitor != null) {
monitor.worked(1);
if (monitor.isCanceled()) {
return LockResponse.DENIED(response.getLockInfo());
}
}
} catch (InterruptedException e) {
// ignore and keep trying
}
}
return response;
}
@Override
public LockResponse acquireLock(IPersistentObject po){
if (po == null) {
return LockResponse.DENIED(null);
}
logger.debug("Acquiring lock on [" + po + "]");
LockResponse lr = acquireLock(po.storeToString());
if (lr.getStatus() == LockResponse.Status.ERROR) {
logger.warn("LockResponse ERROR");
}
return lr;
}
private LockResponse acquireLock(String storeToString){
if (storeToString == null) {
return LockResponse.DENIED(null);
}
User user = (User) ElexisEventDispatcher.getSelected(User.class);
LockInfo lockInfo = new LockInfo(storeToString, user.getId(), systemUuid.toString());
LockRequest lockRequest = new LockRequest(LockRequest.Type.ACQUIRE, lockInfo);
return acquireOrReleaseLocks(lockRequest);
}
@Override
public LockResponse acquireOrReleaseLocks(LockRequest lockRequest){
if (standalone) {
return LockResponse.OK(lockRequest.getLockInfo());
}
if (ils == null) {
String message =
"System not configured for standalone mode, and elexis-server not available!";
logger.error(message);
ElexisEventDispatcher
.fireElexisStatusEvent(new ElexisStatus(org.eclipse.core.runtime.Status.ERROR,
CoreHub.PLUGIN_ID, ElexisStatus.CODE_NONE, message, null));
return new LockResponse(LockResponse.Status.ERROR, lockRequest.getLockInfo());
}
LockInfo lockInfo = lockRequest.getLockInfo();
synchronized (locks) {
// does the requested lock match the cache on our side?
if (LockRequest.Type.ACQUIRE == lockRequest.getRequestType()
&& locks.keySet().contains(lockInfo.getElementId())) {
incrementLockCount(lockInfo);
return LockResponse.OK(lockRequest.getLockInfo());
}
// do not release lock if it was locked multiple times
if (LockRequest.Type.RELEASE == lockRequest.getRequestType()
&& getCurrentLockCount(lockInfo) > 1) {
decrementLockCount(lockInfo);
return LockResponse.OK(lockRequest.getLockInfo());
}
// TODO should we release all locks on acquiring a new one?
// if yes, this has to be dependent upon the strategy
try {
if (LockRequest.Type.RELEASE == lockRequest.getRequestType()) {
PersistentObject po =
CoreHub.poFactory.createFromString(lockInfo.getElementStoreToString());
if (po != null) {
ElexisEventDispatcher.getInstance().fire(new ElexisEvent(po, po.getClass(),
ElexisEvent.EVENT_LOCK_PRERELEASE, ElexisEvent.PRIORITY_SYNC));
}
}
LockResponse lr = ils.acquireOrReleaseLocks(lockRequest);
if (!lr.isOk()) {
return lr;
}
if (LockRequest.Type.ACQUIRE == lockRequest.getRequestType()) {
// ACQUIRE ACTIONS
// lock is granted only if we have non-exception on acquire
locks.put(lockInfo.getElementId(), lockInfo);
incrementLockCount(lockInfo);
PersistentObject po =
CoreHub.poFactory.createFromString(lockInfo.getElementStoreToString());
if (po != null) {
ElexisEventDispatcher.getInstance().fire(
new ElexisEvent(po, po.getClass(), ElexisEvent.EVENT_LOCK_AQUIRED));
}
}
return lr;
} catch (Exception e) {
// if we have an exception here, our lock copies never get
// deleted!!!
String message = "Error trying to acquireOrReleaseLocks.";
logger.error(message);
ElexisEventDispatcher
.fireElexisStatusEvent(new ElexisStatus(org.eclipse.core.runtime.Status.ERROR,
CoreHub.PLUGIN_ID, ElexisStatus.CODE_NONE, message, e));
return new LockResponse(LockResponse.Status.ERROR, lockRequest.getLockInfo());
} finally {
if (LockRequest.Type.RELEASE.equals(lockRequest.getRequestType())) {
// RELEASE ACTIONS
// releases are also to be performed on occurence of an
// exception
decrementLockCount(lockInfo);
locks.remove(lockInfo.getElementId());
PersistentObject po =
CoreHub.poFactory.createFromString(lockInfo.getElementStoreToString());
if (po != null) {
ElexisEventDispatcher.getInstance().fire(
new ElexisEvent(po, po.getClass(), ElexisEvent.EVENT_LOCK_RELEASED));
}
}
}
}
}
private void incrementLockCount(LockInfo lockInfo){
Integer count = lockCount.get(lockInfo.getElementId());
if (count == null) {
count = new Integer(0);
}
lockCount.put(lockInfo.getElementId(), ++count);
logger.debug("Increment to " + count + " locks on " + lockInfo.getElementId());
}
private void decrementLockCount(LockInfo lockInfo){
Integer count = lockCount.get(lockInfo.getElementId());
if (count != null) {
lockCount.put(lockInfo.getElementId(), --count);
logger.debug("Decrement to " + count + " locks on " + lockInfo.getElementId());
if (count < 1) {
lockCount.remove(lockInfo.getElementId());
}
}
}
private Integer getCurrentLockCount(LockInfo lockInfo){
Integer count = lockCount.get(lockInfo.getElementId());
if (count == null) {
count = new Integer(0);
}
logger.debug("Got currently " + count + " locks on " + lockInfo.getElementId());
return count;
}
@Override
public boolean isLockedLocal(IPersistentObject po){
if (po == null) {
return false;
}
if (standalone) {
return true;
}
// check local locks first
if (locks.containsKey(po.getId())) {
return true;
}
return false;
}
@Override
public boolean isLocked(IPersistentObject po){
if (po == null) {
return false;
}
logger.debug("Checking lock on [" + po + "]");
User user = (User) ElexisEventDispatcher.getSelected(User.class);
LockInfo lockInfo = new LockInfo(po.storeToString(), user.getId(), systemUuid.toString());
LockRequest lockRequest = new LockRequest(LockRequest.Type.INFO, lockInfo);
return isLocked(lockRequest);
}
@Override
public boolean isLocked(LockRequest lockRequest){
if (lockRequest == null || lockRequest.getLockInfo().getElementId() == null) {
return false;
}
if (standalone) {
return true;
}
// check local locks first
if (locks.containsKey(lockRequest.getLockInfo().getElementId())) {
return true;
}
try {
return ils.isLocked(lockRequest);
} catch (Exception e) {
logger.error("Catched exception in isLocked: ", e);
return false;
}
}
@Override
public List<LockInfo> getCopyOfAllHeldLocks(){
Collection<LockInfo> values = locks.values();
if (values.size() == 0) {
return Collections.emptyList();
}
return new ArrayList<LockInfo>(values);
}
@Override
public String getSystemUuid(){
return systemUuid.toString();
}
@Override
public LockInfo getLockInfo(String storeToString){
String elementId = LockInfo.getElementId(storeToString);
LockInfo lockInfo = locks.get(elementId);
return lockInfo;
}
private class LockRefreshTask extends TimerTask {
private ILockService restService;
@Override
public void run(){
try {
final String restUrl = System
.getProperty(ElexisSystemPropertyConstants.ELEXIS_SERVER_REST_INTERFACE_URL);
if (restUrl != null && !restUrl.isEmpty()) {
// if service is available but we are not using it -> use it
// if service not available but we are using it -> dont use it
if (testRestUrl(restUrl) && ils instanceof DenyAllLockService
&& restService != null) {
ils = restService;
iis = ConsumerFactory.createConsumer(restUrl, IInstanceService.class);
// publish change
ElexisEventDispatcher.getInstance().fire(new ElexisEvent(null,
ILocalLockService.class, ElexisEvent.EVENT_RELOAD));
} else if (!testRestUrl(restUrl) && !(ils instanceof DenyAllLockService)) {
restService = ils;
iis = null;
ils = new DenyAllLockService();
// publish change
ElexisEventDispatcher.getInstance().fire(new ElexisEvent(null,
ILocalLockService.class, ElexisEvent.EVENT_RELOAD));
}
}
if (standalone) {
return;
}
if (iis != null) {
User u = (User) ElexisEventDispatcher.getSelected(User.class);
inst.setActiveUser((u != null) ? u.getId() : "NO USER ACTIVE");
iis.updateStatus(inst);
}
// verify and update the locks
boolean publishUpdate = false;
synchronized (locks) {
List<String> lockKeys = new ArrayList<String>();
lockKeys.addAll(locks.keySet());
for (String key : lockKeys) {
boolean success = ils.isLocked(new LockRequest(Type.INFO, locks.get(key)));
if (!success) {
publishUpdate = true;
releaseLock(locks.get(key).getElementStoreToString());
}
}
}
if (publishUpdate) {
ElexisEventDispatcher.getInstance()
.fire(new ElexisEvent(null, LockInfo.class, ElexisEvent.EVENT_RELOAD));
}
} catch (Exception e) {
LoggerFactory.getLogger(LockRefreshTask.class).error("Execution error", e);
}
}
private boolean testRestUrl(String restUrl){
try {
URL url = new URL(restUrl);
HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();
urlConn.connect();
return HttpURLConnection.HTTP_OK == urlConn.getResponseCode();
} catch (IOException e) {
return false;
}
}
}
private class DenyAllLockService implements ILockService {
@Override
public LockResponse acquireOrReleaseLocks(LockRequest request){
return LockResponse
.DENIED(getLockInfo(request.getLockInfo().getElementStoreToString()));
}
@Override
public boolean isLocked(LockRequest request){
return false;
}
@Override
public LockInfo getLockInfo(String storeToString){
return new LockInfo(storeToString, "LockService", "DenyAllLockService");
}
}
@Override
public Status getStatus(){
if (standalone) {
return Status.STANDALONE;
} else if (ils == null || ils instanceof DenyAllLockService) {
return Status.LOCAL;
}
return Status.REMOTE;
}
@Override
public void shutdown(){
timer.cancel();
if (iis != null) {
inst.setState(STATE.SHUTTING_DOWN);
iis.updateStatus(inst);
}
}
}