3
votes

What is the best way to deal with document locking in xPages? Currently we use the standard soft locking and it seems to work fairly well in the Notes client.

In xPages I considered using the "Allow Document Locking" feature but I am worried that people would close the browser without using a close or save button then the lock would never be cleared.

Is there a way to clear the locks when the user has closed his session? I am seeing no such event.

Or is there an easier way to have document locking?

I realize I can clear the locks using an agent but when to run it? I would think sometime a night then I am fairly certain the lock should no longer really be active.

5

5 Answers

1
votes

Here is code I'm using:

/* DOCUMENT LOCKING */
/*

    use the global object "documentLocking" with:
    .lock(doc) -> locks a document
    .unlock(doc) -> unlocks a document
    .isLocked(doc) -> returns true/false
    .lockedBy(doc) -> returns name of lock holder
    .lockedDT(doc) -> returns datetime stamp of lock

*/ 
function ynDocumentLocking() {

    /*

        a lock is an entry in the application scope

        with key = "$ynlock_"+UNID

        containing an array with
        (0) = username of lock holder
        (1) = timestamp of lock


    */

    var lockMaxAge = 60 * 120; // in seconds, default 120 min

    this.getUNID = function(v) {
        if (!v) return null;
        if (typeof v == "NotesXspDocument") return v.getDocument().getUniversalID();
        if (typeof v == "string") return v;
        return v.getUniversalID();
    }

    /* puts a lock into application scope */
    this.lock = function(doc:NotesDocument) {
        var a = new Array(1);
        a[0] = @UserName();
        a[1] = @Now();
        applicationScope.put("$ynlock_"+this.getUNID(doc), a);  
        // print("SET LOCK "+"$ynlock_"+doc.getUniversalID()+" / "+a[0]+" / "+a[1]);
    }   

    /* removes a lock from the application scope */
    this.unlock = function(doc:NotesDocument) {
        applicationScope.put("$ynlock_"+this.getUNID(doc), null);
        //print("REMOVED LOCK for "+"$ynlock_"+doc.getUniversalID());
    }

    this.isLocked = function(doc:NotesDocument) {
        try {
            //print("ISLOCKED for "+"$ynlock_"+doc.getUniversalID());       

            // check how old the lock is
            var v = applicationScope.get("$ynlock_"+this.getUNID(doc));
            if (!v) {
                //print("no lock found -> return false");
                return false;   
            }

            // if lock holder is the current user, treat as not locked
            if (v[0] == @UserName()) {
                //print("lock holder = user -> not locked");
                return false;
            }


            var dLock:NotesDateTime = session.createDateTime(v[1]);
            var dNow:NotesDateTime = session.createDateTime(@Now());
            // diff is in seconds
            //print("time diff="+dNow.timeDifference(dLock)+" dLock="+v[1]+" now="+@Now());
            // if diff > x seconds then remove lock, it not locked
            if (dNow.timeDifference(dLock) > lockMaxAge) {
                // print("LOCK is older than maxAge "+lockMaxAge+" -> returning false");
                return false;
            }
            //print("return true");
            return true;
        // TODO: check how old the lock is
        } catch (e) {
            print("ynDocumentLocking.isLocked: "+e);
        }

    }

    this.lockedBy = function(doc:NotesDocument) {
        try {
        var v = applicationScope.get("$ynlock_"+this.getUNID(doc));
        if (!v) return "";
        //print("ISLOCKEDBY "+"$ynlock_"+doc.getUniversalID()+" = "+v[0]);
        return v[0];

        } catch (e) {
            print("ynDocumentLocking.isLockedBy: "+e);
        }
    }

    this.lockedDT = function(doc:NotesDocument) {
        try {
        var v = applicationScope.get("$ynlock_"+this.getUNID(doc));
        if (!v) return "";
        return v[1];

        } catch (e) {
            print("ynDocumentLocking.isLockedBy: "+e);
        }
    }

}

var documentLocking = new ynDocumentLocking();
0
votes

You could take a page from the way webDAV works. There a servlet manages a "lock-list" of locked documents. The locks automatically expire after 10 minutes. Locks can be renewed or terminated trough calls. So when you edit a document you would request a lock, then kick off a CSJS timer that calls the relocking function every 8 minutes (so you have some margin for error) and the postSave calls the unlock (unless you stay in edit mode). If a user closes the browser after 10 minutes the document is automatically unlocked. Since you are free how to implement the locking function, you can capture user/location and use that information in the "lock failed" display (you event could push that further and let the original author know about it or do some "retry" option. It isn't simple to implement, but once implemented simple to use

0
votes

ApplicationScope may be a good place to capture "locked" documents. After all, for applicationScope to expire, all users' sessions have to have expired, so anyone with the page open will not be able to save anyway.

Maybe capture UNID, user and time when someone edits a doc. Clear the value when the document is saved. Bear in mind that the user might close the browser etc. I've been discussing this approach internally and if we end up building this I would look to add it to OpenNTF. But we're unlikely to get onto it within the next month.

0
votes

I Prefer to use a solution similar to Mr. Withers' answer. The main issue is how to deal with the unwanted and dreaded back button. It is easy to lock a document when it is opened, but there are many ways to close the XPage, and the user is not limited to just the navigation you provide but also can, as he stated, close the browser completely, use the back button, etc. So, the best way that I can think of is to create a few java objects which we will use in the application and session scopes.

The first step is to create a "LockedDocument" class. As we know, the documents are not serializable and we do not want to save the document itself in this object, we want to save the UNID and the time it was saved. We want to do it this way so that we can manage to clear the object after a given time (like thirty minutes to an hour). This class should also implement the comparable interface in order to sort the collection by this time so that the oldest documents are first and the newest documents are last.

Next we create another class that holds a list or a map with these LockedDocuments. This class must also have a thread (implement Runnable) that will check all documents every five minutes or so, I did not test this yet, but it should work). Any document that was locked thirty to sixty minutes ago (predefined) will be unlocked (deleted from the list). It is important that the list be sorted as described above and that the loop is "broken" when a time less than the locktime is reached in order to prevent unwanted processing.

The next step would be to include the user specific list in the sessionScope. This list is the LockedDocuments that this current user has. It is set when the user changes the document's status to editable, and is checked before the document is set to editable to prevent one document from being opened in multiple tabs by the same user. The lock is once again checked onquerysave(). Once a main page is opened, the lock is automatically released. The onquerysave() must also check to make sure the documents UNID is in the sessionScope list, or if the document is new before allowing a save.

quick recap
Any UNID saved in the applicationScope LockedDocumentList would not be editable by anyone unless it exists in their own sessionScope list.
It is possible to warn a user that their lockedTime is approaching and reset the timer.
The class containing a list with the locked documents must be a singleton
There are probably ways to improve this answer, and I am sure I am missing something. It is just a thought.

There might be a better way to handle this, but it is the best I found.

0
votes

You can remove the Domino lock in window.onunload event:

window.onunload = function(){
    dojo.xhrGet(...
}

No need to reinvent the wheel.