Review of the Year – 2010

It seems like lots of bloggers do these end of year wrap up posts, and I understand that lots find them boring. But as with all my blogging, I largely write posts for my own benefit. I just spent half an hour reading my previous few years posts and I find it interesting to see how things have moved on in some ways and stayed the same in many others.

Anyway, the year started, as ever, with Lotusphere. A crack team of Elguji and LDC manned the Elguji stand in the product showcase and Tim Clark and I presented our Show ‘n’ Tell session. All in all it was a very good week with lots of new contacts made. The one thing which working in the showcase means is that it’s pretty much impossible to get to see any sessions. So for 2011 we’re not doing the stand and instead I plan to cram as much new techie goodness into my head as possible.

The first half of the year got pretty much taken over with travel to various user groups and training courses. BLUG, DanNotes, XPages training became my life until the end of May. And just to make the travel even more entertaining, Warren Elsmore and I got trapped in Copenhagen trying to get home thanks to the Ash Cloud. This became an epic trip home involving cars and trains, but unfortunately no planes. It also marked, looking back on it, a huge turning point in telecommunications for me. The #GreatGeekEscape hashtag on Twitter provided entertainment for those watching us try to get home, but from our point of view people were able to help us find travel options in pretty close to real time. On the flip side, the image of Warren and I sat in a Fiat 500 (a lovely if small car) with 2 laptops and 3 phones trying to find our way through a new road system in Holland having already driven for 8 hours is one I can happily forget.

The other effect of Twitter on my online life is that I am blogging less and less. With Twitter taking the minutiae of day to day life and XPages101.net taking the majority of my technical posts I’m not sure what’s going to happen with the blog as time goes on but for the moment it will stay here, just getting updated less frequently.

Talking of XPages101, this really took a lot of my time this year and it’s been a very interesting experience. The whole driver came from our session at Lotusphere. Several people said they’d like to see it converted into online video. To make it worthwhile I had to go through the process of breaking it up, creating the supporting material and so had to charge for the whole endeavour. I had absolutely no idea if people would be interested. Well I’m happy to say that well over 150 of you have been interested. I know that it’s all gone quiet on the video front for the last couple of months, but there will be more in the New Year, consider that a resolution. The combination of the video course and the classroom course seems to work rather well. I’ve been into several companies and organised my own training days which people seem to like as a way of getting going with XPages. The plan is to continue with these introductory classes and then supplement those with a new intermediate and advanced course. I just need to decide what that actually means, where the interest lies.

The other work parts of my life – IdeaJam and IQJam with Elguji and consulting with LDC have been keeping me pretty busy (especially over the last few months hence the lack of new videos). The challenge with these three competing streams of work is striking a balance between them all. Overall it’s pretty difficult to complain about having too much work to do.

IdeaJam work has been especially interesting this year with continued growth in sales to companies and governments around the world, but we’re noticing a distinct increase in interest in our hosted offerings so I’ve been working away on infrastructure tools to help us quickly deploy and manage new IdeaJam sites which is why there’s been a slight slow down of late in new features for the public site. But worry not, we have a huge queue of enhancement requests to get through and I am making good progress on the next version of IdeaJam.

The LDC is still a great source of new work, help with existing projects and support from some of the smartest people in the Lotus world. We’re always looking for new projects and most of us will be at Lotusphere, so if you’d like to know more just collar Julian, Mark or myself and we’ll be happy to have a chat with you.

The end of the year was marked, unusually, with ILUG running in Belfast. I think you’ve probably seen the amazing coverage it got on blogs, twitter, video sites etc. It was a great success thanks, mainly, to the efforts of Paul and Eileen. It’s a testament to them that there are so many other user groups using the same model around the world now. Of course the planning for UKLUG is already starting so the focus shifts to Manchester in May.

The most important change in my life this year has been a personal one. I do tend to not get into “real” life stuff on here too much, but let me just say that I’ve had the best year of my life outside of work hours. Those of you who know me, know why and hopefully have noticed how much happier I am. Long may it continue.

I suspect that for many 2010 wasn’t a good year, if so let’s hope that 2011 is better. Always one to buck trends I’ve been very lucky and had a great year with interesting things happening on all fronts. I’m not sure how 2011 can match up but we can but hope.

</2010> <2011>

It’s a small Lotus world (part 2)

As you may have seen over the last week, there are several of us in the Lotus community who are gathering together to organise a fund raising event at Lotusphere 2011.

The inspiration came from Mark Myers and Ben Poole, who, in a rare moment of non bickering, specified and arranged the creation of the “It’s a small Lotus world” pixel art. Bruce then came up with the idea of raffling a very nicely framed version of the art (complete with key of all of the “inside baseball” references hidden away).Then Steve McDonagh very kindly decided to enter the fray with his Map of the Lands of Lotii.

So what’s the deal? Well, over the next few weeks, and at Lotusphere itself, we are selling raffle tickets. They are $10 each. Then at UK Night at Lotusphere (on the Monday night) we will be holding the grand prize draw for the framed pixel art, and Mr McD’s art work. 

You can either click this button to pay online:

or you can collar Bruce, Gayle, Mark, Julian Woodward or myself at Lotusphere to buy a ticket from us direct.

The aim of all of this, apart from being fun, is that we raise as much money as possible for the Children’s Cancer Association, which is a great charity that provides support to seriously ill children and their families.

 

So, dig deep, buy as many tickets as you can afford and you may well be walking away from Lotusphere with some amazing artwork to hang on your office wall. (If you’re not going to Lotusphere then of course we’ll ship the prize to you.)

One of the things we’re doing over the next few weeks is gradually revealing different sections of the pixel art image, so here is this week’s image in which we see the iEd’s store.

I’ll be speaking at Lotusphere 2011

Well the emails went out last night, I had one session rejected but Tim Clark and I are very fortunate to have had two sessions approved for Lotusphere 2011.

XPages Blast

Matt and Tim, will take you on a roller-coaster ride through the best of the best ideas and time saving techniques for creating world class XPages applications. We’re going to provide 30 top tips in just 60 minutes, it will be fast paced and packed with loads of information you will refer to time and again. Everything from simple debugging and dojo controls all the way through to complex Server Side Javascript and jQuery integration. Please fasten your seat belts and keep your hands in the car at all times.

How to build a simple XPages application

Join us as we take you step by step and click by click through building an XPages application. Learn the basics and then grow the complexity as you expand the application. Including XPages, Custom Control, Server Side Javascript, single UI methodology, using existing Notes data and adding unlisted Dojo objects. Watch the application being built live on stage with everything you need to know condensed into two hours.

Looking forward to seeing you there.

Useful new preference in DDE 8.5.2 for Java Agents

Having the proper Eclipse Java editor instead of the old style editor was introduced in 8.5.1, but there was one huge annoyance. When you edited a Java file, you had to save that, but then also remember to save the agent as well. Cue plenty of swearing and head scratching when your change wasn’t showing up.

Well in 8.5.2 there is a new preference setting which allows the agent to be automatically saved when you save the Java file:

Just check the “Autosave Java design element on save of individual Java sources” box to make your Java coding a lot less annoying.

How to get SSO for Facebook working with XPages

So, earlier today I posted a short video about a new feature that I’ve written for IQJam (initially at least, it will be coming to IdeaJam at some point). It allows you to authenticate against a Domino server using Facebook Single Sign On (SSO).

Overall this is not for the faint of heart (or the new XPages developer, hence me not going into detail about how what the code is doing etc), there are a lot of moving parts to get things working properly. That being said there is absolutely no reason exactly the same approach couldn’t be taken with a classic Domino application. All of the server Side Javascript would need to be converted to LotusScript and Java (for the network operations).

1) Register your application with Facebook here: http://www.facebook.com/developers/apps.php

2) In the app home page, add a facebook login graphic with some client side onClick code:

var returl = "http://[Your App Host Name][Your App DB Path]/fblogin.xsp";
var url = "https://graph.facebook.com/oauth/authorize?client_id=[YOUR FACEBOOK APP ID]&redirect_uri=" + returl;
window.open (url, "mywindow","width=500,height=250");

3) Create a new XPage called fblogin

4) In the afterPageLoad event put the following code:

try{
    //Get these from Facebook App registration
    var API_KEY = "YOUR FACEBOOK APP ID";
    var SECRET = "YOUR FACEBOOK SECRET KEY";

    //Get the auth code from url param returned by facebook
    var code = context.getUrlParameter("code");
    
    //Now swap the auth code for the access_token key
    var urltoken = "https://graph.facebook.com/oauth/access_token?client_id=" + API_KEY + 
                    "&redirect_uri=http://[yourhostname]" + 
                    "/" + @ReplaceSubstring(database.getFilePath(), "\\", "/") + "/fblogin.xsp" + 
                    "&client_secret=" + SECRET + 
                    "&code=" + code;
    var urltoken:jav.net.URL = new java.net.URL(urltoken);
    var urltokenconn = urltoken.openConnection();
    var tokenreader = new java.io.BufferedReader(
                            new java.io.InputStreamReader(
                            urltokenconn.getInputStream())
                        );
    var inputLine;
    var accesstoken = "";
    while(    (inputLine = tokenreader.readLine()) != null){
        accesstoken += inputLine;
    }
    tokenreader.close();
    accesstoken = @Right(accesstoken, "=");

    //Now get the user info using the access_token
    var url = "https://graph.facebook.com/me?access_token=" + accesstoken + "&client_id=" + API_KEY;
    var url:java.net.URL = new java.net.URL(url);
    var urlconn:java.net.URLConnection = url.openConnection();
    var reader:java.io.BufferedReader = new java.io.BufferedReader(
                                            new java.io.InputStreamReader(
                                            urlconn.getInputStream())
                                        );
    var inputLine;
    var userjson = "";
    while ((inputLine = reader.readLine()) != null){
        userjson += inputLine;
    }
    reader.close();
    
    //Now we've got a JSON object which contains the user data
    userjson = eval("(" + userjson + ")");
    var firstname = userjson.first_name;
    var lastname = userjson.last_name;
    var userId = userjson.id;
    var fbname = getFBName(firstname, lastname, userId);
    var password = getFBPassword(fbname, SECRET);
    print("FBName = " + fbname);
    var fbreg = new facebookReg();
    if (fbreg.validateUser(fbname.getCanonical())){
        //We need to go and register the user
        fbreg.registerNewFBUser(firstname, lastname, fbname, password);
    }
    //Set the username and password fields so the Ajax login can happen    
    viewScope.username = fbname.getCanonical();
    viewScope.password = password;
}catch(e){
    _dump(e);
}

5) In a supporting script library you’ll need the following functions:

/*
An object which handles authentication / registration of Facebook users
Created By: Matt White
Date Created: October 2010
Version: 1.0
*/
var facebookReg = function(){
    var dbNab:NotesDatabase = null;
    var dbMainNab:NotesDatabase = null;
    var registerNewFBUser = function(firstname, lastname, fbname, password){
        getDbs();    
        if(validateUser(fbname.getCanonical())){
            var registerNewUser = false;
            
            if (!addUserToGroup(fbname)){
                print("Couldn't add " + fbname.getAbbreviated() + " to group");
            }else{
                dbNab.DelayUpdates = false
                dbMainNab.DelayUpdates = false
                
                var docPerson = dbNab.createDocument();
                
                docPerson.replaceItemValue("form", "Person");
                docPerson.replaceItemValue("Type", "Person");
                docPerson.replaceItemValue("LastName", lastname);
                docPerson.replaceItemValue("FirstName", firstname);
                var item = docPerson.replaceItemValue("FullName","");
                item.appendToTextList(fbname.getCanonical());
                item.appendToTextList(firstname + " " + lastname);
                docPerson.replaceItemValue("HTTPPassword", password);
                docPerson.replaceItemValue("accountstatus", "Not Verified");
                
                docPerson.computeWithForm( false, false );
                print("Saving new person doc: " + docPerson.getUniversalID() + " in " + dbNab.getTitle());
                docPerson.save();
                
                var addviews = new Array();
                addviews.push(dbNab.getView("($LDAPCN)"));
                addviews.push(dbNab.getView("($Users)"));
                addviews.push(dbNab.getView("($ServerAccess)"));
                addviews.push(dbNab.getView("($VIMPeople)"));
                addviews.push(dbMainNab.getView("($ServerAccess)"));
                addviews.push(dbMainNab.getView("($VIMGroups)"));
                addviews.push(dbMainNab.getView("($Users)"));
                for(var i=0; i<addviews.length; i++)
                    addviews[i].refresh();
                print("Refreshed views");
                
                //Finally create a profile document for the person
                var dbCurrent = sessionAsSigner.getDatabase(database.getServer(), database.getFilePath());
                var profile = dbCurrent.createDocument();
                profile.replaceItemValue("Form", "person");
                profile.replaceItemValue("Name", fbname.getCanonical());
                profile.computeWithForm(false, false);
                profile.save();
                print("Created profile document");
            }
        }
    }
    
    var validateUser = function(thisname){
        getDbs();
        var people = dbNab.getView("($Users)");
        var collection = people.getAllDocumentsByKey(thisname, true);
        print("Found " + collection.getCount() + " matching people for " + thisname);
        if (collection.getCount() > 0)
            return false;
        else
            return true;
    }
    
    var addUserToGroup = function(nname){
        var group = "[Your Group Name Here]";
        var groups = dbMainNab.getView("Groups");
        var docGroup = groups.getDocumentByKey(group, true);
        
        if (docGroup == null){
            docGroup = dbMainNab.createDocument();
            docGroup.replaceItemValue("Form", "Group");
            docGroup.replaceItemValue("ListName", group);
            docGroup.replaceItemValue("Members",  group & " 1");
            docGroup.replaceItemValue("GroupType", "0");
            docGroup.replaceItemValue("ListDescription", "Do NOT edit this group manually, it is updated via an agent!!!");
            docGroup.computeWithForm( false, false );
            docGroup.save();
        }
        
        var groupMainMembers = docGroup.getFirstItem( "Members" );
        var subGroup = "";
        for (var x=groupMainMembers.getValues().length; i>=0; i--){
            if (@Left(groupMainMembers.getValues()[x], @Length( group  )) == group)
                subGroup = groupMainMembers.getValues()[x];
        }
        
        groupNum = 0;
        
        if (subGroup != "")
            groupNum = @TextToNumber( @Right( subGroup, @Length( subGroup ) - @Length( group ) - 1 ) );
        else{
            groupNum = 1
            subGroup = group + " 1";
        }
        
        while(true){
            var groupSubDoc = groups.getDocumentByKey( subGroup, true );
            
            if (groupSubDoc == null){
                groupSubDoc = dbMainNab.createDocument();
                groupSubDoc.replaceItemValue("Form", "Group");
                groupSubDoc.replaceItemValue("ListName", subGroup);
                groupSubDoc.replaceItemValue("GroupType", "0");
                groupSubDoc.computeWithForm( false, false );
                
                if (!groupMainMembers.containsValue( subGroup )){
                    try{
                        groupMainMembers = docGroup.getFirstItem("Members");
                        groupMainMembers.appendToTextList(subGroup);
                        saveGroupMainDoc = true;
                    }catch(e){
                        _dump(e);
                    }
                }
            }
            var groupSubMembers = groupSubDoc.getFirstItem( "Members" );
            
            if (groupSubMembers.getValueLength() < 10000)
                break;
            
            groupNum = groupNum + 1;
            subGroup = group + " " + groupNum;
        }
        
        groupSubMembers.appendToTextList(nname.getCanonical());
        groupSubDoc.save( false, true );
        docGroup.save( false, true );
        return true;
    }
    
    var getDbs = function(){
        if (dbNab == null || dbMainNab == null){
            dbNab = sessionAsSigner.getDatabase(database.getServer(), "[NAB Where Users Are Stored]");
            dbMainNab = sessionAsSigner.getDatabase(database.getServer(), "[Main NAB]");
        }
    }
    
    return {
        // public methods
        registerNewFBUser:        registerNewFBUser,
        validateUser:            validateUser,
        addUserToGroup:            addUserToGroup, 
        getDbs:                    getDbs
    }
}
/*
Creates a new Notes Name using First Name, Last Name, Facebook User ID
*/
function getFBName(firstname, lastname, uid){
    return session.createName(firstname + " " + lastname + "/" + uid + "/Facebook");
}
/*
Generates a password using a Notes Name and a secret key as a salt
*/
function getFBPassword(fbname:NotesName, seed){
    var result = session.evaluate("@Password(\"" + fbname.getCanonical() + seed + "\")");
    return @ReplaceSubstring(result.elementAt(0), ["(",")"], "");
}

6) Finally you’ll need some AJAX code which logs the user in assuming all of the previous code has worked properly

function doLogin(userNameId, passwordId, facebookmode){
    dojo.xhrPost({
        url: "/names.nsf?login",
        content: {
            username: dojo.byId(userNameId).value, 
            password: dojo.byId(passwordId).value, 
            redirectto: dbPath + "/username.txt?open&rnd=" + Math.random()
        },
        load: function(data) {
            try {
                if( data.indexOf("Anonymous") == -1) { 
                    dojo.byId("loginMsg").style.display = "block";
                    dojo.byId("loginMsg").style.color = "green";
                    dojo.byId("loginMsg").style.backgroundColor = "transparent"; 
                    dojo.byId("loginMsg").innerHTML = "Please Wait";
                    if(location.href.indexOf("register.xsp") > -1){
                        location.href = dbPath;
                    }else{
                        if (facebookmode == true){
                            window.opener.location.href = window.opener.location.href;
                            window.close(); 
                        }else{
                            if (window.location.href.indexOf("#") > -1){
                                window.location.replace( strLeft(window.location.href, "#") );
                            }else{
                                window.location.replace( window.location.href );
                            }
                        }
                    }
                } else { 
                    dojo.byId("loginMsg").style.display = "block";
                    dojo.byId("loginMsg").style.color = "red"; 
                    dojo.byId("loginMsg").style.backgroundColor = "transparent";
                    if ( dojo.cookie('DomAuthSessId') != null || dojo.cookie('LtpaToken') != null ) { 
                        dojo.byId("loginMsg").innerHTML = "You do not have access to this database";
                    } else { 
                        dojo.byId("loginMsg").innerHTML = "Wrong username or password"; 
                    } 
                }
            }catch(e){
                alert(e);
                console.error ('Error: ', error);
            }
        },
        error: function(data) {
            alert(e);
            console.error ('Error: ', error); 
        } 
    });
}

7) That code will need to be triggered by some Javascript which runs when the page loads (so this needs to go in the onClientLoad function)

if (gup("code") != ""){
    doLogin("#{id:username}", "#{id:password}", true)
}else{
    window.close();
}

For the gup function go here: http://www.netlobo.com/url_query_string_javascript.html

 

Facebook SSO to XPages

One of the things I’ve been playing with this week is getting Facebook SSO working with XPages. It’s not a true SSO implementation as Domino doesn’t support OAuth, but from the user’s point of view they are not having to enter a username and password to get authenticated against a Domino app. I think it’s pretty cool…

XPages training videos now available on the iPad

Well I finally caved to the repeated requests to make the videos on XPages101.net available to run on the iPad, iPhone, Android and so on. So I’ve spent the better part of this afternoon transcoding all of the videos and… Voilà.

In the end I also took the opportunity to move the hosting of the videos across to Vimeo so hopefully performance will be better and they’ll also better handle future developments in online video technology meaning I won’t need to worry about it in future!

Anyway, if you’re already a subscriber, log into the site and check out the new videos which I’ve uploaded this week which look at the data table control and the Extensions Library project on OpenNTF. If you’re not already a subscriber then head on over to the site and see what’s on offer, it really is a good way to get started with XPages development.

A small tip for upgrading your XPages apps from 8.5.1 to 8.5.2

If you have been using Stephan Wissel’s “Web Agents, XPages Style” technique for outputting non-HTML content from your XPages, you may run into a problem when you upgrade your server to 8.5.2.

In the original afterRenderResponse sample code, you would use something like…

try{
    var exCon = facesContext.getExternalContext(); 
    var writer = facesContext.getResponseWriter();
    var response = exCon.getResponse();
    response.setContentType("text/plain");
    writer.write("Hello World");
    writer.close();
}catch(e){
    _dump(e);
}

What you may find is running that code on your lovely new 8.5.2 server will result in an Error 500 with no detail of the error itself. To fix the problem, simple remove the

writer.close();

line from the source code and you should be good to go.

What’s new with XPages in 8.5.2

Over on XPages101.net I’ve just posted (yet another) video. This time we’re looking at what’s new in 8.5.2 for the XPages developer.

What’s New With XPages in 8.5.2

As a bit of a teaser, I thought I’d post the audio from that recording here as well so that you can hear what you’re missing. As ever, feel free to subscribe if you want to get a jumpstart on your XPages development. There are 30 different videos up on the site covering all the elements you’ll need to get started and then dig a little deeper into XPages development. The full video listing can be found here.

Heading home

It seems like a long time since I went away on holiday, and it is. Since the middle of July I’ve been to Latitude (great), Madeira (great), IamLUG (great) and run an XPages101 day (seemed to go well). So all in all it’s been a really enjoyable few weeks.

I’m sat in JFK airport in New York waiting to start the final leg of the journey home and much as it’s been a great couple of weeks away, I am *really* looking forward to getting back home and just decompressing for a couple of days before getting back into the real world with a vengeance on Monday morning.

IamLUG was a really superb event, Chris and the rest of the team do put on a good show. Everything was run incredibly smoothly, the content was very good indeed and, as ever, the people were great company. I presented my “Ten XPages Design Patterns” session, probably for the last time as I’ve used it for a year now. So I need to start thinking about future sessions pretty seriously now. (Bet you can’t guess what I plan to speak about next time!).

After IamLUG, we had the inaugural “Tack It On” day, and I’d really like to thank David Leedy for his invaluable assistance during the day. I had been rather lax and not given him a huge amount of guidance before the day itself but he dealt with all of the questions with good grace and humour. Things really wouldn’t have run as smoothly without him there. Overall I think the day went pretty well. I’m sure there’ll be more of these sorts of things happening in the future.