Showing posts with label SWORD2. Show all posts
Showing posts with label SWORD2. Show all posts

Wednesday, 20 March 2013

Sakai Development: Post Ten

So now to work on the presentation side of things, using the velocity files. This is something new to me, so I'm going to start off by reading through a tutorial suggested by Andrew Martin, with reference to the Sakai content tool. The first thing to do is to find the velocity configuration, which is a file listed in the relevant web.xml file, and which turns out to be called (unsurprisingly) velocity.properties. This in turn tells me that the velocity templates are stored where I would expect them to be, in the sakai-src/content/content-tool/tool/src/webapp/vm directory, sorted into two sub-directories, content and resources (which is a directory containing templates for common fragments, judging by the file names). One which we probably want to emulate is content/chef_resources_deleteConfirm.vm, so I'll start by opening that. Basically, the intention is to extend it by adding a metadata form. (I'll also need to make an archiving version equivalent to content/sakai_resources_deleteFinish.vm, I think.)

This is also where I come across the Sakai language support mechanism:

$tlang.getString("del.del")

I'll have to follow that through and add an appropriate entry for archiving, at least for the English language - there aren't all that many languages I could add entries to apart from that one. This, "del.del1", and "gen.cancel" appear several times in the two files. The values in different languages are set in files in config/localization/bundles/src/bundle/org/sakaiproject/localization/bundle/content/ (according to grep -R), so I'll need to sort things there. The file content.properties is the list of default phrases to use, which effectively means American English. So I add in content.properties, which usefully also contains a set of Dublin Core term descriptions:

#Archive Vm
archive.confirm = Archive confirmation and metadata creation
archive.sure = Enter information which describes the collection you have selected, and check the resources listed are correct.
archive.archive = Archive
archive.form = Enter information which describes the collection you have selected, and check the resources listed are correct.
archive.metadatatable = Table allows the input of metadata to describe the collection of items for archiving.
archive.resourcetable = Table holds information about resources selected for archiving. Column headers contain descriptions of contents and links for sorting.

I may need to add more later, when the form itself takes shape properly. Next, the form for confirmation itself. I'll also need to alphabeticise, as that's how the properties file is organised. This will start out as a copy of the delete confirmation form. Most of the changes amount to altering the word "delete" to "archive", to match equivalent updates to the ResourcesAction script which processes these files.

Then the main part of the change, to add the metadata input table. The metadata table needs to come before the file listing, simply because we don't know how many files might be in the listing, and so how far down the page the listing table might reach; confusing if the submitter needs to scroll down several times to find it. It starts with a table tag, like that for the listing, but instantly I see a problem. The file listing table has class "listHier" (presumably hierarchical list), but that is obviously not going to be appropriate for the metadata part of the form. The easiest way to solve this (as the vm file is a fragment of HTML and doesn't have the CSS declarations in it, so finding the right CSS file might be awkward - especially as there are almost 300 of them in the Sakai source code) is to find a table which is the right type, and copy the style information from that. Looking at the tables in the content-tool VMs, there are three types: listHier, itemSummary and no declared class, the first two with or without classes "lines" (not sure what this will do) and labelindnt (indent labels, presumably). I'll try not declaring a class first, so that the table will just inherit the styles current at this point in the page where it is displayed. So I add a table and appropriate DC terms - noticing that Sakai's interpretation of the meaning of a couple of them appears to be slightly different from that of the SWORD library developers, judging by the descriptions of the fields in the vocabulary list.

On to the second form. The delete version of the form seems to be identical, which is sensible - it takes the reader back to the file listing, which would now be missing the deleted items. At least, I think that's what happens; the form has submission buttons, which is a bit strange if this is the case. What I think I want to see displayed here for the archiving is the result of the submission (and a summary of what has been entered by the user on the previous page). So I add appropriate context.put statements to ResourcesAction to display the metadata which has been sent off, and display the results as text.

It would be possible to display the status of the submission in real time, using JQuery and Ajax. But I decided that I probably don't have the time to set this up, and most deposits should be pretty quick. So I just want to get the receipt, check the status, and then (assuming that the status indicates a successful deposit) display the information in the receipt - or at least, some of it, as there are quite a lot of fields which might be included in a returned receipt by the SWORD2 server. The swordapp lists the following fields which may be set in a DepositReceipt object:

receipt.getStatusCode();
receipt.getLocation();
receipt.getDerivedResourceLinks();
receipt.getOriginalDepositLink();
receipt.getEditMediaLink();
receipt.getAtomStatementLink();
receipt.getContentLink();
receipt.getEditLink();
receipt.getOREStatementLink();
receipt.getPackaging();
receipt.getSplashPageLink();
receipt.getStatementLink("application/rdf+xml");
receipt.getStatementLink("application/atom+xml;type=feed");
receipt.getSwordEditLink();
receipt.getTreatment();
receipt.getVerboseDescription();

Without going back to the SWORD2 specification it is not immediately apparent what all these fields mean. First, though, is a field which is not strictly speaking part of the receipt, but tells us whether the deposit itself was successful (if it has the value 200, 201, or 202), or not. The SWORD2 specification lists a small number of error codes which a server could send, so I only need to process these (and provide a default response, just in case - I could understand the web server returning 500 or 404, neither of which are listed part of the specification but would indicate respectively a problem with the way the SWORD server is set up on the web server or an incorrect URL respectively). Some are provided for SWORD2 clients, rather than direct access by a human being using a web browser - to allow, for example, the editing of the metadata for the deposited item at the location of the client rather than needing to go through the repository interface; such things would not be relevant for our purposes. So I need to add code to the script to set appropriate information into the context, so it be available to Velocity, and then add code to the archiveFinish Velocity template to display them.

I'm now going to finally attempt a compilation. This is a make or break moment, as the outcome of the process will be used to decide whether it is worth going on to finish off the work or whether it will need to be left in an incomplete state while I go on to other things that the Research360 project needs.  First time, it complains about the missing swordapp library, which I installed. Then there's the irritating missing end bracket:

sakai-src/content/content-tool/tool/src/java/org/sakaiproject/content/tool/ResourcesAction.java:[6796,73] error: ')' expected

Easily fixed. At least, once I realise I'm looking at line 7696 not line 6796...

And then again, and I get hundreds of errors from files I've not even changed -
could not parse error message:   symbol:   variable Menu
  location: class AttachmentAction
/home/simon/work/sakai-src/content/content-tool/tool/src/java/org/sakaiproject/content/tool/AttachmentAction.java:339: error: cannot find symbol
  state.setAttribute(MenuItem.STATE_MENU, bar);
                     ^

Wednesday, 10 October 2012

Sakai Development: Post Nine

Before actually starting writing the code to do the deposit itself, I need to set up and include the SWORD2 Java client libraries. If you're not used to github, you might take a while to see the  button, which you can use to download the library as a zip file. Unzip it, cd to the created directory, and run

mvn clean package

to compile (and download a large number of new library files). This should hopefully end up with:

[INFO] [jar:jar {execution: default-jar}]
[INFO] Building jar: /home/simon/work/swordapp-JavaClient2.0-420485d/target/sword2-client-0.9.2.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3 minutes 24 seconds
[INFO] Finished at: Wed Sep 26 15:14:32 BST 2012
[INFO] Final Memory: 27M/121M
[INFO] ------------------------------------------------------------------------

and then it's a question of copying the jar file to somewhere where it can be picked up by Sakai. This requires two things (assuming my understanding of how maven POM files work is correct):

  • Add a dependency to the relevant pom.xml file, which will be picked up on compilation, so that maven will attempt to download the relevant jar file, and, if it can't, will ask for it to be installed locally by hand. The relevant file is in the content/content-tool/tool directory, and needs the following added (with line numbering):

  • Import the necessary classes into the ResourcesAction.java file so that the library can be used in this script. This is a simple pair
import org.swordapp.client.SWORDClient;
import org.swordapp.client.SWORDClient.*;
at line 135 of the file.
The code which will use this is based on doFinalizeDelete (lines 6545-6675), which follows doDeleteConfirm when the confirmation is selected. I haven't yet worked out where the actual display of the confirmation request happens, so this is not the last change by any means. The confirmation step could also include the opportunity to select from a list of repositories, and from the collections which exist at the chosen repository (as obtained from the repository SWORD2 service document). But that is a complication I don't really want to get into at this stage, I just want to be able to carry out a simple deposit. So I'm going to have both the repository and the collection set in the configuration file for the servlet.

The process is relatively simple; however, I should point out that I have just noticed in the SWORD2 client documentation that multi-part deposit is not supported, and the way I have been thinking has assumed that it is. So I will have to make a zip file or something of the items to be deposited (as their nature as a collection is important). java.util.zip is a core package, but not one I've ever used before; I'll start by adding the import for the package at the top of the file (new line 52).

The steps for producing a SWORD2 deposit from the selected files are:

  • Get archive details from configuration (some will always need to be obtained from configuration, but a service description could be downloaded on the fly to get information about collections etc. - just not in this version as I'm already overrunning the schedule);
  • Prepare metadata, using data from the confirmation form, which should basically be a subset of DC terms for use in the deposit (bearing in mind that it's possible for the depositor to go to the repository and change the metadata later if necessary);
  • Prepare files - create a zip as a file stream;
  • Authenticate to repository using userid/password provided in confirmation form;
  • Make deposit and (hopefully) get back a receipt.
While the information given in the swordapp documentation at first looks pretty complete, it is missing some details which I need, as I discover on starting to put the code together. I'll need to look at the source code for the app to get them.

The first issue is with the metadata. Dublin Core is not the only metadata used in SWORD2; there is some basic information which is part of the Atom profile: title, author, last modification date, and so on, as seen in the example here. The documentation gives no information about how to set this, and in fact I can't find anything useful in the source code (the string "updated", which is the element name for the last modification date, does not appear anywhere in the client code). I'm not particularly familiar with Atom, so it is possible that these are optional. I'll ignore this for the moment and see what happens. I'm also going to assume that in order to give multiple entries, I just repeatedly add a term: this needs to be supported for DC contributor - I think this should work, but I haven't actually gone through the apache Abdera library .which the swordapp code uses to check this.

Just at this point Andrew Martin put up a useful blog post which details his journey to working with the SWORD2 java library. He's not doing exactly the same thing, though we have already been in contact. While I need to go deeper into the coding, his post is probably a very useful resource for anyone reading this one.

The next thing to sort out is creating a ZIP file (virtually) to contain the items selected for archiving. I've not done this before, and the ZIP creation stuff I can find online, as well in my ancient Java book,   concentrates on making a ZIP from files (this looks pretty  useful for that, and is likely to form the framework I'll use) rather than from the Sakai content platform where the content may not even be stored in a filesystem. So I need to work out how to get the content out of the files as a Java input stream. I'll start by looking through the ResourcesAction.java code, and then move on to other files in the same directory if I can't find anything. All the input streams in ResourcesAction are for making changes to content rather than reading it - makes sense, as reading is not an action whcih affects the resource. But this code from FilePickerAction.java (lines 1710-13) makes it look very simple:

InputStream contentStream = resource.streamContent();
String contentType = resource.getContentType();
String filename = Validator.getFileName(itemId);
String resourceId = Validator.escapeResourceName(filename);

I just need to work back through the context to be sure that this code is doing what it appears to be doing. Although it doesn't appear to be (because it's in a method for up dating the resource), this is what is is in fact doing, as I eventually discover when I find the relevant javadocs (ContentHostingService, BaseContentService, and ContentResource - not from the current release, though). To re-use this code, the ContentResource class needs to be loaded, which it already is, and the content service needs to be set up (outside the loop which runs through the selected items):

ContentHostingService contentService = (ContentHostingService) toolSession.getAttribute (STATE_CONTENT_SERVICE);

The first problem, then, is that what I have is a ListItem object, when what I want is the itemID (which is a String); this is simple, as id is a property of the ListItem object, so I can just get it. I'll also need to protect against the itemid being null, which I don't think should happen. I'm not quite sure what the correct thing would be to do if it does, so I'll just log a warning if that happens. So the code I add is (lines 6741-4):

String itemid = item.id;
ContentResource resource = null;
if (itemid != null)
{

and then in the conditional block (6752-6774),

resource = contentService.getResource(itemId);
InputStream contentStream = resource.streamContent();

byte[] buf = new byte[1024];

//get filename and add to zip entry
String fileName = item.getName();

if (fileName != null)
{
  zip.putNextEntry(new ZipEntry(fileName);
}
else
{
  zip.putNextEntry(new ZipEntry("Unnamed resource with ID " + itemid);
}

int len;
while ((len = contentStream.read(buf)) > 0) {
  zip.write(buf, 0, len);
}
zip.closeEntry();     
contentStream.close(); 

Some preparation has to happen before this, using piped streams to turn the zip output into the input for the SWORD library, calculating the MD5 hash we want for the deposit stage on the way:

MessageDigest md = MessageDigest.getInstance("MD5");
DigestInputStream dpins = new DigestInputStream(pins, md);

And now we should be in a position to set up the deposit with the SWORD2 library..

It's also occurred to me that the solution to the problem of multiple archives is to embed them in the confirmation web form - the user selects the archive there from a drop down list, and the script reaps the URL used for deposit. So the URL to use is then just a form parameter. Except - the sword client readme file suggests that a collection object (derived from the collection description in a service document) is needed for deposit, so I need to check in the source code to see if there's a method with a deposit URL as an alternative. Turns out that there is, so I'll use that. So we have (ignoring a whole load of exceptions which will surely need to be caught, for the moment):

// Set up authentication
AuthCredentials auth = new AuthCredentials(params.getString("userid"),params.getString("password"));

// Make deposit and (hopefully) get back a receipt
Deposit deposit = new org.swordapp.client.Deposit();
deposit.setFile(dpins);
dpins.close();
pins.close();
pouts.close();
byte[] fileMD5 = md.digest();

deposit.setMimeType("application/zip");
deposit.setFilename(params.getString("title") + ".zip");
deposit.setPackaging(UriRegistry.PACKAGE_SIMPLE_ZIP);
deposit.setMd5(fileMD5);
deposit.setEntryPart(ep);
deposit.setInProgress(true);

// And now make the deposit
DepositReceipt receipt = client.deposit(params.getString("depositurl"), deposit, auth);

The next issue is what to do with the receipt. And how to alert the user to the success or failure of the deposit. The confirmation web page should still be available (especially if it has an indication that the status of the archiving will be displayed there). So it be displayed there, dynamically. So for the moment, I'll just hold on to the receipt and revisit this code when I've written the appropriate Velocity file.

There's just one final bit of code to add to this file, which is to add a call to the confirm delete method   as a new state resolution function (lines 7083-6):

else if(ResourceToolAction.ARCHIVE.equals(actionId))
{
  doArchiveconfirm(data);
}

Then I can start work on the Velocity file. I should say at this point that I don't expect this code to compile without error. I'm absolutely certain there will be exceptions which haven't been caught, and I may well have confused some of the variable names in the course of this. But I want to get on to the next stage before coming back here.

Wednesday, 3 October 2012

Sakai Development: Post Eight

This post follows straight on from the last, especially as I've missed a constant setting which goes with the ones listed at its end, a new line 857:

ACTIONS_ON_MULTIPLE_ITEMS.add(ActionType.ARCHIVE);

Looking at the next section I might need to change, where permissions are sorted out at lines 1721-1801, I don't think anything needs to be altered, because this draws in constant values which I have already altered. However, things do need to be changed where permissions are set for items. This is done with new lines 2203 and 2209:

boolean canArchive = ContentHostingService.allowArchiveResource(id);
item.setCanArchive(canArchive);

Of course, there will need to be corresponding alterations in the ContentHostingService class. I just need to find it - there is no ContentHostingService.java file in the source code. And searching online doesn't find anything useful. It's time to email those in the know, but meanwhile I can carry on with other bits of code. But I got a really quick answer - before I had a chance to do so, which tells me that I'm on the right lines:

"Yeah this one is in the kernel. The CHS Api lives there.
Sounds like what you are doing is correct. You'll need to add a method there that, most likely, checks some permission and returns true or false. If you want it set via a permission of course. You could just make it always available if that is what you wanted to, then you may not need to mod the kernel.

Then you could do item.canaddarchive(true)"

Thinking a bit more about this, I feel that perhaps I don't need a new kernel method, but I can be more sophisticated than making the service always available. To archive an item is basically making a copy, so what I really should do is to tie the archiving permission to the copy permission. So instead of the lines 2203 and 2209 above, I'll just check the canRead permission which is already there. (It strikes me, though, that in this world of DRM, read and copy are not necessarily the same thing, but never mind.) At least, I'm now at the end of the setting of permission booleans - the next bit of code should actually do something. (And no, I still have no idea why none of the kernel appears to be in the source code I downloaded, but this difficulty is one of the factors in my decision.)

What I want to work on now is to build the three archiving intermediate pages, which should be almost precisely like existing pages. The code for this starts at line 3961, which is where the delete confirmation page building code begins. To remind you, the three intermediate pages are the confirmation, collection metadata entry, and finish; the first and third will basically be copies of the equivalent delete pages and will therefore be built much in the same way. (I expected that most of the work for this little project would consist of copying and then amending existing code, nothing terribly difficult, and this is exactly what is being done here.) Since the code being copied is quite long, I'm not going to quote it all here. The two routines are very similar, so it looks as though copy and paste has already been used. I don't think I have time to look into the lines which are commented "//%%%% FIXME". I'll also need a third copy for MODE_ADD_ARCHIVE_METADATA. The new code becomes lines 4063 to 4207.

The code to call one of these new routines comes next, also a simple copy and modification of existing code. This comes in the context for the main page being re-displayed, as it will be on MODE_ARCHIVE_FINISH: lines 4823-7:

else if (mode.equals (MODE_ARCHIVE_FINISH))
{
  // build the context for the basic step of archive confirm page
  template = buildArchiveFinishContext (portlet, context, data, state);
}

This gives a total of about 200 lines of boilerplate code copied, modified slightly, and inserted back into the class. From this point onwards, we start getting into more exciting development (though there is still a little bit more to add, to call the code to create the context for the metadata form and for the confirmation page - which, it has just occurred to me, could sensibly be the same thing...I may want to revisit some of the above changes to do this, but for the moment I'll just leave things as they are, as it shouldn't do any harm to create constants but not use them).

The model I have used so far for these changes is the existing code for item deletion. Now, there is a slight problem: the actual deletion code appears to delete items one at a time when more than one is selected for deletion, and we can't do this for the archiving; we want one SWORD2 transaction whether there are one or more items. We now need to copy and modify two routines - doCopy, and doDeleteconfirm - which set the application state to doCopy or Deleteconfirm respectively. The first will set the doArchive state, and the second will set Archiveconfirm state, in both cases processing the list of items selected for archiving into a vector format, and these states should then be processed to make the actual archiving or the display of the confirmation page happen. This gives another hundred or so lines of code modified which doesn't really do much.

The next place where something needs to be added is now line 6288. The doDispatchItem method, of which this switch block forms part, is, like most of the rest this class, sparsely commented, but appears to be the part which determines what to do - hence the switch block, with cases corresponding to the different actions. The question is, whether the ARCHIVE case should be like the COPY case or the DELETE case? It's hard to tell without more documentation. The COPY case basically adds the ID of a selected item to a list of items to copy, while the DELETE case actually calls deleteItem - a method we have already decided isn't appropriate to copy directly (as we don't want to break down the archiving of a collection of items into a sequence of archiving actions on the individual items). So the ARCHIVE case needs to be something in between, something like this (I hope):

case ARCHIVE:
  List items_to_be_archived = new ArrayList();
  if(selectedItemId != null)
  {
    items_to_be_archived.add(selectedItemId);
  }
  state.removeAttribute(STATE_ITEMS_TO_BE_MOVED);
  state.setAttribute(STATE_ITEMS_TO_BE_ARCHIVED, items_to_be_archived);
  if (state.getAttribute(STATE_MESSAGE) == null)
  {
    // need new context
    state.setAttribute (STATE_MODE, MODE_ARCHIVE_FINISH);
  }
  break; 

which forms the new lines 6288 to 6301.

Wednesday, 4 July 2012

Sakai Development Diary - Prelude

My next major job will be to write an extension to the Sakai research environment software which enables the deposit of material from Sakai to some SWORD2 compliant repository. (SWORD2 is a protocol for the deposit of digital materials into repositories, so this is a reasonably sensible project to undertake.) This work is funded by JISC through the Research360 project based at the University of Bath, but this is a personal blog and the content does not represent the opinions of either JISC or the University of Bath, and any mistakes are my own.

My Sakai background is negligible, so I will be starting from scratch. I have had a look round the Sakai website, and had a chat to some people who know the system better than I do, and have not been left much wiser than when I started. This is really the reason behind this diary: it does not look easy to pick up the information that is needed to start developing for Sakai, and documenting how I went about it and what worked should therefore be helpful to the Sakai community. With that in mind, I will try not to be too critical, curbing my natural inclination to sarcasm.

First Impressions


The website for Sakai is very corporate-looking for an open source project. Clearly, that is a good thing in many ways - it certainly convinces the reader that the tool is one which has been thought through and which has had a lot of work put into it. The emphasis on the front page is on the community, which is one of the main concerns of open source software projects. So it looks healthy - at least from the point of view of a potential installer.

But the community of researchers who use, or who might be persuaded to use, Sakai are not the only people who will be going to the website. An open source project is likely to attract people who are interested in contribution, as I am. What information does the website hold for developers?

The front page itself tells me that Sakai is not just one tool, but two: the CLE (Community Learning Environment) and the OAE (Open Academic Environment); from the names, it is clear that the latter is what interests me. There is a big difference in what is available for the CLE and the newer OAE; it is possible to try out a hosted installation of the CLE, but not of the OAE. At the moment, I don't have a development environment suitable for setting up a development installation of Sakai, so I tried out the hosted CLE - an interesting experience. The hosted environment comes courtesy of a choice of commercial affiliates to the Sakai project; I chose Unicon, mainly because it is the first in the list. (The other link of interest on the Sakai home page, Getting Started, which leads to a page from which I chose Technical Contributor information, ends up suggesting that trying a hosted version of the CLE is the first step.)


Clearly, Unicon have been generous to support such a free service, so that is to be applauded - but I still felt there were some issues with the trial system, apart from it being the less relevant of the two Sakai tools. At the Unicon site, the potential user needs to create an account. The form for this has quite a lot of required information (at least, I assumed that fields with a red star next to them were required, as the sign up page doesn't actually say so), including some which, as a consultant rather than an academic, were a little difficult to choose how to complete - I don't have an academic department, for example. But I'm only doing this to have a chance to play around on the system for a short while, just to get a feel for the sort of things that Sakai can do. What is more irritating is to receive a confirmation email which contains my password in clear text. (See here for why this is a bad thing for security.) However, the account is temporary, only working for 90 days, so it is not a disaster, even if it is off-putting for a software developer - does this mean that Sakai is going to store user passwords in unencrypted form?


Trying the software without clear goals in mind is less productive than it might be. The main thing about it is that it does leave me with a positive impression of the ease of use and potential for versatility of the software.


What's the second step? Joining the appropriate Sakai mailing list. The only information that is given on the Sakai website is the list of the 50+ mailing lists, with a minimal description of each - the longest of them is just 12 words. I decided to join announcements, the programmers' cafe (I'm not quite sure whether this is a mailing list for informal discussion between developers, or about a specific part of Sakai called the Programmers' Cafe - but I can always unsubscribe later), and oae-dev, and sakai-dev.


The Technical Contributor page now suggests setting up a jira (bug tracker) account. This done, I have a quick look round, and start to find the kind of information I need to get started. That is, I now know that there is a Sakai OAE Widget Library (for example): this sounds relevant, but all I can see here are bug reports - the jira project listing does not give a website, as it does for the majority of projects. Now I know that it exists, I should be able to find out more. It's a bit annoying to have to go to everyclick (the search engine I am currently using, so that every search I do donates to charity) to find the website for the project. A bookmark to go back to later, though I do have time to discover that there isn't single sign on between any of the things I have registered for today, and I'll need a new userid and password to use the library website. If  I had not used jira before, I might have found it quite hard to actually discover the projects, as the tab to click on is not particularly obtrusive.


From this point, the Technical Contributor page starts to get quite CLE specific, so I think that my next task - once I have a development server - will be to download and install the OAE.


SWORD is a smaller scale project, and this is reflected in a far plainer website (and in the geekiness of their logo!). But then, I am a developer, and not so interested in being sold the product as in finding out how to use it. What do I want to know?


I have some idea of what SWORD is already, but not much about the differences between version 1 and version 2. The site has prominent links to the specifications for both versions of the protocol, and I bookmark the version 2 definition for later perusal - it's going to be extremely important when I start working on it, but is not light reading, and I'll probably forget most of the important details between now and when I actually need to know them. Following a link to SWORD2 implementations takes me to a much more immediately important list. There are client toolkits which would help me write my Sakai extension in Java, Ruby, python or PHP, and a list of servers known to support version 2 of the protocol, which will be important when I come to testing it out. I have written code in all four languages, but I have yet to find out what restrictions Sakai will place on what I can use. Not only that, but the different client toolkits all have decent looking documentation. I feel that finding this out is actually my biggest achievement so far: well done Stuart Lewis, the author of the page!