Creating Alfresco Share sites with JavaScript

The first article I wrote about creating Alfresco Share sites with JavaScript has been the most viewed of all my articles in the blog. It has a problem though, the sites you create with the script(s) described in the article does not live over server re-starts. This is because the required Site configuration in the AVM store is never created correctly. So I thought I’d give it a go and try and create a new script that is creating the sites properly. Robin Bramley, a colleague of mine, also pointed out that the solution would be more flexible and extensible if the site data for the sites that should to be created by the script were described in JSON. So here we go again…

Source code for this article can be downloaded from here.

Introduction

Some companies or organisations implementing Alfresco Share solutions have more than a couple of Alfresco Share sites that they need to create and manage. In the beginning it is quite natural to create the sites manually via the Alfresco Share UI. However, when the number of sites reaches into the hundreds, and each site has a complex folder hierarchy and many different permission settings etc, then it might make sense to try and manage all the site creation and setup via scripting.

Handling site creation and setup via scripts is also going to be very helpful when you setup the system in different environments (i.e. development, staging, production etc). New developers joining the project will also be able to easily setup the development environment by just running a few scripts.

In the first edition of this article I explored setting up sites with the Alfresco JavaScript API. The API has a method called createSite on the siteService that can be used to create a site, but it does not take you all the way. We had to also run a Web Script that would setup site preset in the Spring Surf sitedata object. This worked fine as long as the Alfresco Tomcat server was not restarted, in which case the site would not be accessible. This was because the necessary content in the avm://sitestore had not been created as in the following picture:

The content in the picture above can be accessed via the Node Browser and a path similar to:

  avm://sitestore/-1|alfresco|site-data /{http://www.alfresco.org/model/content/1.0}alfresco/{http://www.alfresco.org/model/content/1.0}site-data/{http://www.alfresco.org/model/content/1.0}components

I am running the examples in this article on Alfresco Community 3.4d. So I had to figure out some other way to create the sites then just using the JavaScript API alone.

Creating sites with curl from the command line

When trying to figure out how to create Alfresco Share sites via scripting I had a look at how the Share web application does it. I used HttpFox (a Firefox development tool) to investigate what takes place when you create a site from the Share UI.

The following URL and Headers are sent:

So we can see that the URL that is used is http://localhost:8080/share/service/modules/create-site and it is a POST of content of type JSON. The JSON content specifies data for the site we want to create and it looks like this:

  {       "visibility" : "PUBLIC",       "title" : "Test",       "shortName" : "test",       "description" : "Testing",       "sitePreset" : "site-dashboard"  }

See previous article for an explanation of these properties.

There are also authentication headers alfLogin, JESSIONID, and alfUsername2alfLogin and alfUsername2 cookies are Alfresco specific and are not necessary when creating sites.

The question now was – where do we get the JSESSIONID cookie from? It turns out you can get it from the Share login request.

The login request looks like this in HttpFox:

The login request URL is http://localhost:8080/share/page/dologin and it too is a POST request with the username and password as data. The response contains the needed JSESSIONID Cookie. So now we can easily create new sites from the command line, with for example curl, and they will persist after a server restart.

The following example illustrates, first we login to get needed cookie:

  X:\tools\curl7.21>curl -v -d @login.txt -H "Content-Type:application/x-www-form-urlencoded" http://localhost:8080/share/page/dologin    * About to connect() to localhost port 8080 (#0)  *   Trying 127.0.0.1... connected  * Connected to localhost (127.0.0.1) port 8080 (#0)  > POST /share/page/dologin HTTP/1.1  > User-Agent: curl/7.21.1 (i386-pc-win32) libcurl/7.21.1 OpenSSL/0.9.8o zlib/1.2.5  > Host: localhost:8080  > Accept: */*  > Content-Type:application/x-www-form-urlencoded  > Content-Length: 29  >   < HTTP/1.1 302 Moved Temporarily  < Server: Apache-Coyote/1.1  < Set-Cookie: JSESSIONID=E12C8523A8A8D19BEBD68FE84FDB2170; Path=/share  < Set-Cookie: alfLogin=1327935313; Expires=Mon, 06-Feb-2012 14:55:13 GMT; Path=/share  < Set-Cookie: alfUsername2="YWRtaW4="; Version=1; Max-Age=604800; Expires=Mon, 06-Feb-2012 14:55:13 GMT; Path=/share  < Location: http://localhost:8080/share  < Content-Length: 0  < Date: Mon, 30 Jan 2012 14:55:13 GMT  <   * Connection #0 to host localhost left intact  * Closing connection #0

In this call I logged in as admin by supplying the login.txt username=admin&password=admin

We now got a JSESSIONID that we need for our call to create the site. The POST data, specified in JSON format, will look like this and be kept in a file called site_data.json:

  {       "visibility" : "PUBLIC",       "title" : "My Test Site",       "shortName" : "mytestsite",       "description" : "My Test Site created from command line",       "sitePreset" : "site-dashboard"  }

The site is then created like this:

  X:\tools\curl7.21>curl -v -d @site_data.json -H "Cookie:JSESSIONID=E12C8523A8A8D19BEBD68FE84FDB2170" -H "Content-Type:application/json" -H "Accept:application/json" http://localhost:8080/share/service/modules/create-site    * About to connect() to localhost port 8080 (#0)  *   Trying 127.0.0.1... connected  * Connected to localhost (127.0.0.1) port 8080 (#0)  > POST /share/service/modules/create-site HTTP/1.1  > User-Agent: curl/7.21.1 (i386-pc-win32) libcurl/7.21.1 OpenSSL/0.9.8o zlib/1.2.5  > Host: localhost:8080  > Cookie:JSESSIONID=E12C8523A8A8D19BEBD68FE84FDB2170  > Content-Type:application/json  > Accept:application/json  > Content-Length: 171  >   < HTTP/1.1 200 OK  < Server: Apache-Coyote/1.1  < Cache-Control: no-cache  < Pragma: no-cache  < Content-Type: application/json;charset=UTF-8  < Content-Language: en-GB  < Transfer-Encoding: chunked  < Date: Mon, 30 Jan 2012 15:01:48 GMT  <   {     "success": true    }* Connection #0 to host localhost left intact  * Closing connection #0

So we can now create sites via scripting as long as we can make HTTP POST requests from the scripting language and manage JSON. If this is all you need then you can pretty much stop here, if you want to do more then read on…

Creating a Share site with a custom Web Script

So all we have to do now is just create a Web Script that makes the above POST requests and we are in business. After quite some time I came to the conclusion that doing these POST requests in a JavaScript controller does not work (believe me, I did a lot of trial and error with both Alfresco Repo Web Scripts and Spring Surf Web Scripts). The solution I came up with is an Alfresco Web Script that uses both a Java Controller and a JavaScript controller. The Java controller makes the two POST requests to create the site and the JavaScript controller sets up the rest of the site such as members and folders.

SITE DATA FORMAT

For this new Web Script I also wanted to implement a more efficient and flexible way of specifying all the information for a site that should be created. This would include the basic site information such as short name, title, description, visibility etc, together with what members should be added to each site and what folder structure should be created for each site. There should also be the possibility to specify more stuff, such as for example data lists, without requiring too much coding or changes. To specify all data for a site I choose to use a JSON structure as it is very flexible, extensive, and works well with JavaScript. The following example shows how it is defined and built up:

  {      "sites" : [          {              "visibility" : "PUBLIC",              "title" : "My Test Site100",              "shortName" : "mytestsite100",              "description" : "My Test Site created from command line 100",              "sitePreset" : "site-dashboard",              "members" : [                  {                      "userName" : "martin",                      "role" : "SiteConsumer"                  },                  {                      "userName" : "veronika",                      "role" : "SiteContributor"                  }              ],              "folders" : [                  {                      "name" : "test",                      "title" : "testTitle",                      "description" : "testDesc",                      "subfolders" : [                          {                              "name" : "subtest",                              "title" : "subtestTitle",                              "description" : "subtestDesc",                              "subfolders" : [                                  {                                      "name" : "subsubtest",                                      "title" : "subsubtestTitle",                                      "description" : "subsubtestDesc"                                  },                                  {                                      "name" : "subsubtest2",                                      "title" : "subsubtest2Title",                                      "description" : "subsubtest2Desc"                                  }                              ]                          },                          {                              "name" : "subtest2",                              "title" : "subtest2Title",                              "description" : "subtest2Desc"                          }                      ]                  },                  {                      "name" : "test2",                      "title" : "test2Title",                      "description" : "test2Desc"                  }              ]            },          {              "visibility" : "PUBLIC",              "title" : "My Test Site101",              "shortName" : "mytestsite101",              "description" : "My Test Site created from command line 101",              "sitePreset" : "site-dashboard"          }      ]  }  

There is a top level property called sites that is an array of all the sites that should be created by the Web Script. Each site entry contains the basic information about the site plus the members that should be added to the site (in the form of an array) and the folders that should be created in the site’s Document Library (also in the form of an array). The folder structure can be specified in any depth by using more nested subfolders properties as demonstrated above.

This JSON structure is then just POSTed to the new site creation Web Script, which parses it and creates the sites based on the data. Each site entry in the JSON structure is also POSTed as is to the /service/modules/create-site Web Script via the Java controller as it has the required properties.

Using curl to call this new Web Script, which will be called createSites by the way, with the above JSON structure stored in a file called site_data.json, would look like this:

    X:\tools\curl7.21>curl -v -u admin:admin -d @site_data.json -H "Content-Type:application/json" "http://localhost:8080/alfresco/service/mycompany/createSites"  * About to connect() to localhost port 8080 (#0)  *   Trying 127.0.0.1... connected  * Connected to localhost (127.0.0.1) port 8080 (#0)  * Server auth using Basic with user 'admin'  > POST /alfresco/service/mycompany/createSites HTTP/1.1  > Authorization: Basic YWRtaW46YWRtaW4=  > User-Agent: curl/7.21.1 (i386-pc-win32) libcurl/7.21.1 OpenSSL/0.9.8o zlib/1.2.5  > Host: localhost:8080  > Accept: */*  > Content-Type:application/json  > Content-Length: 1306  > Expect: 100-continue  >   < HTTP/1.1 100 Continue  < HTTP/1.1 200 OK  < Server: Apache-Coyote/1.1  < Set-Cookie: JSESSIONID=70F2ADAED45119455DAB60D7D453E31A; Path=/alfresco  < Cache-Control: no-cache  < Pragma: no-cache  < Content-Type: text/html;charset=UTF-8  < Content-Length: 994  < Date: Mon, 05 Mar 2012 11:11:11 GMT  <           Created site (My Test Site100[mytestsite100])             Added user (martin) as (SiteConsumer) in site (My Test Site100 [mytestsite100])             Added user (veronika) as (SiteContributor) in site (My Test Site100 [mytestsite100])             Document Library not found (documentLibrary), created it under (/Company Home/Sites/mytestsite100) for site (mytestsite100)             Added folder (test) under (/Company Home/Sites/mytestsite100/documentLibrary)             Added folder (subtest) under (/Company Home/Sites/mytestsite100/documentLibrary/test)             Added folder (subsubtest) under (/Company Home/Sites/mytestsite100/documentLibrary/test/subtest)             Added folder (subsubtest2) under (/Company Home/Sites/mytestsite100/documentLibrary/test/subtest)             Added folder (subtest2) under (/Company Home/Sites/mytestsite100/documentLibrary/test)             Added folder (test2) under (/Company Home/Sites/mytestsite100/documentLibrary)             Created site (My Test Site101[mytestsite101])     * Connection #0 to host localhost left intact  * Closing connection #0  

Now, let’s get on with coding the Web Script.

WEB SCRIPT DESCRIPTOR

First thing we need to do is create a descriptor for the Web Script as follows:

      Create Site(s)    Creates one or more sites as specified in past in POST JSON object    To test: curl -v -u admin:admin -d @site_data.json -H "Content-Type:application/json" "http://localhost:8080/alfresco/service/mycompany/createSites"        /mycompany/createSites    user    required  

Put the descriptor in a file called createSites.post.desc.xml. In my case I have stored the descriptor file in the trunk\_alfresco\config\alfresco\extension\templates\webscripts\com\mycompany\sites directory in my build project. You can store the file in any directory structure you like under /templates/webscripts.

For more information about the build project I use see my previous blog, Setting up a build project for Alfresco Explorer, Share, and WQS extensions.

WEB SCRIPT JAVA CONTROLLER

Next up is the Java controller for the Web Script and it will do the main job of creating the sites by first calling the /page/dologin URL to get a JSESSION id and then call the /service/modules/create-site URL for each site that should be created.

The first part of the Java class looks as follows and defines a bunch of constants that we will need and some setters for the Alfresco site service and the username and password to be used when logging in with the /page/dologin URL:

  public class CreateSitesWebScript extends DeclarativeWebScript {      private static Log logger = LogFactory.getLog(CreateSitesWebScript.class);        private static final String HEADER_CONTENT_TYPE = "Content-Type";      private static final String CONTENT_TYPE_TEXT_PLAIN = "text/plain";      private static final String CONTENT_TYPE_JSON = "application/json";      private static final String CONTENT_TYPE_FORM = "application/x-www-form-urlencoded";      private static final String UTF_8 = "UTF-8";      private static final String BASE_URL = "http://localhost:8080/share";      private static final String LOGIN_URL = BASE_URL + "/page/dologin";      private static final String CREATE_SITE_URL = BASE_URL + "/service/modules/create-site";        private SiteService siteService;        private String alfrescoUsername = "admin";      private String alfrescoPwd = "admin";        public void setSiteService(SiteService siteService) {          this.siteService = siteService;      }        public void setAlfrescoUsername(String alfrescoUsername) {          this.alfrescoUsername = alfrescoUsername;      }        public void setAlfrescoPwd(String alfrescoPwd) {          this.alfrescoPwd = alfrescoPwd;      }

Next method is the one that will be called by the Web Script engine and it will setup the sites using the following code, comments inline:

  @Override      protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) {          // Extract the information for the sites that should be created from JSON POST          Content jsonContent = req.getContent();          if (jsonContent == null) {              throw new WebScriptException(Status.STATUS_BAD_REQUEST,                      "Missing POST body with data for creating site(s).");          }            // Create Apache HTTP Client to use for both calls          HttpClient httpClient = new HttpClient();            // Login to Share to get a JSESSIONID setup in HTTP Client          String loginData = "username=" + alfrescoUsername + "&password=" + alfrescoPwd;          makePostCall(httpClient, LOGIN_URL, loginData, CONTENT_TYPE_FORM, "Login to Alfresco Share",                  HttpStatus.SC_MOVED_TEMPORARILY);            // Extract site information from JSON object and create the sites, if they do not already exists          JSONObject json = null;          try {              json = new JSONObject(jsonContent.getContent());              JSONArray sites = json.getJSONArray("sites");                for (int i = 0; i < sites.length(); i++) {                  JSONObject site = (JSONObject) sites.get(i);                    // Get the site's short name (URL)                  String shortName = site.getString("shortName");                    SiteInfo siteInfo = siteService.getSite(shortName);                  if (siteInfo != null) {                      // Site already exists, cannot create it, continue with next one                      logger.warn("Site (" + shortName + ") already exists, cannot create it");                      continue;                  }                    makePostCall(httpClient, CREATE_SITE_URL, site.toString(), CONTENT_TYPE_JSON,                          "Create site with name: " + shortName, HttpStatus.SC_OK);              }          } catch (JSONException jErr) {              throw new WebScriptException(Status.STATUS_BAD_REQUEST,                      "Unable to parse JSON POST body: " + jErr.getMessage());          } catch (IOException ioErr) {              throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR,                      "Unable to retrieve POST body: " + ioErr.getMessage());          }            return new HashMap(); // empty model      }  

The makePostCall is a private custom method that makes the actual HTTP POST call and it looks as follows:

  private void makePostCall(HttpClient httpClient, String url, String data, String dataType,                                String callName, int expectedStatus) {          PostMethod postMethod = null;          try {              postMethod = createPostMethod(url, data, dataType);              int status = httpClient.executeMethod(postMethod);                if (logger.isDebugEnabled()) {                  logger.debug(callName + " returned status: " + status);              }                if (status == expectedStatus) {                  if (logger.isDebugEnabled()) {                      logger.debug(callName + " with user " + alfrescoUsername);                  }              } else {                  logger.error("Could not " + callName + ", HTTP Status code : " + status);              }          } catch (HttpException he) {              logger.error("Failed to " + callName, he);          } catch (AuthenticationFault ae) {              logger.error("Failed to " + callName, ae);          } catch (IOException ioe) {              logger.error("Failed to " + callName, ioe);          } finally {              postMethod.releaseConnection();          }      }

It uses the Apache HTTP Client to setup and make the call. This method also uses another private custom method called createPostMethod that looks like this:

  private PostMethod createPostMethod(String url, String body, String contentType)              throws UnsupportedEncodingException {          PostMethod postMethod = new PostMethod(url);          postMethod.setRequestHeader(HEADER_CONTENT_TYPE, contentType);          postMethod.setRequestEntity(new StringRequestEntity(body, CONTENT_TYPE_TEXT_PLAIN, UTF_8));            return postMethod;      }  

This is all the code for the Java controller. However, it needs to be registered as a Spring bean to be recognized as part of the new Web Script. This is how this is done:

  <bean id="webscript.com.mycompany.sites.createSites.post"            class="com.mycompany.cms.webscript.CreateSitesWebScript" parent="webscript">          <property name="siteService">              <ref bean="siteService"/>          </property>          <property name="alfrescoUsername" value="admin" />          <property name="alfrescoPwd" value="admin" />      </bean>

The important bit is the bean id which has to start with webscript and has to end in createSites.post as the Web Script id is createSites and it is called with a POST method.

So we now got the sites created with proper content in the AVM store etc. We can now continue and process them in the JavaScript controller.

WEB SCRIPT JAVASCRIPT CONTROLLER

The JavaScript controller for the Web Script will handle setting up the site members, folders, and any other data that we would like to pre-populate the new site with. It’s easy to manipulate and read JSON in JavaScript so we should do as much as possible of the site manipulation in JavaScript code.

Here is how it looks like; it starts off with the JavaScript code that will be executed immediately when the controller is called:

  var activityLog = [];    // Evaluate passed in JSON string to JavaScript object  var requestObject = null;  if (requestbody.mimetype == "application/json") {      requestObject = eval('(' + requestbody.content + ')');  } else {      log("Was expecting to find JSON in the request body, instead there was: " + requestbody.mimetype);  }    // Loop through all sites that were created by the Java controller  // and create any extra folders and add any extra site members  var sites = requestObject.sites;  for (var i = 0; i < sites.length; i++) {      var site = sites[i];      var siteInfo = checkSite(site.shortName);      if (siteInfo != null) {          // Add members if we got any          if (site.members != null) {              for (var j = 0; j < site.members.length; j++) {                  var member = site.members[j];                  addSiteMember(siteInfo, member.userName, member.role);              }          }            // Add folders if we got any          if (site.folders != null) {              var docLibFolder = getDocLibFolder(siteInfo);              if (docLibFolder != null) {                  for (var k = 0; k < site.folders.length; k++) {                      var folderInfo = site.folders[k];                      var newFolder = addFolder(docLibFolder, folderInfo);                      addSubFolders(newFolder, folderInfo);                  }              }          }      }  }    model.activityLog = activityLog;

The first line defines an array of log texts that will be passed on to the template that will be part of this Web Scripts. Then we evaluate the JSON structure that was POSTed to the Web Script, we can get to it via the requestBody.content property in the same way we could in the Java controller (i.e. req.getContent()). We convert the JSON text to a JavaScript object so we can have easy access to all properties and arrays in the JSON structure.

Note. Any model that we setup in the Java controller is not accessible in the JavaScript controller, don’t know why…

We then loop through all sites defined in the sites array, accessed via the requestObject.sites array. We check that each site was really created by the Java controller, and exists, by using the custom checkSite function. We then setup the members of the site by navigating into the JSON structure via the site.members array and then calling the custom addSiteMember function for each user that should be added as a member of the site.

When we are done adding members we setup the folder structure in the Document Library by accessing the site.folders array. We start by getting the Document Library folder itself by calling the custom getDocLibFolder function. For each folder we call the custom addFolder function, which creates it, and then we call the addSubFolders custom function, which recursively creates all subfolders to the top folder in the Document Library.

Now let’s have a look at the custom JavaScript functions that were used. The first one is the checkSite and it looks as follows:

  function checkSite(shortName) {      var newSite = siteService.getSite(shortName);      if (newSite != null) {          log("Created site (" + newSite.title + "[" + newSite.shortName + "])");      } else {          log("Site (" + shortName + ") was not created, will not create folders or add members to site");      }      return newSite;  }  

Here we can see that we use the Site Service to check if the site exists, we can do this as we are executing inside an Alfresco Repo Web Script, and not a Spring Surf Web Script. Next function that is used is the addSiteMember:

  function addSiteMember(site, userName, siteRole) {      var siteName = site.title + " [" + site.shortName + "]";      if (!site.isMember(userName)) {          site.setMembership(userName, siteRole);          log("Added user (" + userName + ") as (" + siteRole + ") in site (" + siteName + ")");      } else {          log("Did not add user (" + userName + ") to site (" + siteName + "), user is already a member");      }  }

It also makes use of the Site Service to add a user as a member of a site with a particular role. The valid roles to choose from are SiteConsumer, SiteManager, SiteContributor, and SiteCollaborator.

The getDocLibFolder function looks like this:

  function getDocLibFolder(site) {      var componentName = "documentLibrary";        // Have to use Lucene to find the newly created site      var siteNodes = search.luceneSearch('+PATH:"/app:company_home/st:sites/cm:' + site.shortName + '"');      var siteNode = null;      if (siteNodes != null && siteNodes.length > 0) {          siteNode = siteNodes[0];      } else {          log("Unable to create container (" + componentName + ") for site (" + site.shortName +                "). Could not find site folder (i.e. /Sites/" + site.shortName + ")");          return null;      }        var docLibNode = siteNode.childByNamePath(componentName);      if (docLibNode == null) {          docLibNode = siteNode.createNode(componentName, "cm:folder");          if (docLibNode == null) {              log("Unable to create container (" + componentName + ") for site (" + site.shortName +                      "). (No write permission?)");          } else {              var docLibProps = new Array(1);              docLibProps["st:componentId"] = componentName;              docLibNode.addAspect("st:siteContainer", docLibProps);              docLibNode.save();                log("Document Library not found (" + componentName +                      "), created it under (" + docLibNode.displayPath + ") for site (" + site.shortName + ")");          }      }        return docLibNode;  }  

Here we start with a Lucene search to find the site folder under /Company Home/Sites. I could only find the site folder by using a Lucene search, neither the companyhome.childByNamePath function nor the site.getContainer function worked to get to the Document Library folder, do not why – maybe something to do with the site being created in the Java controller that is part of the same Web Script. The Document Library folder is created if we don’t find it.

When we have the Document Library folder we can start creating the folders under it as per the JSON structure.

The addFolder method is used to create a folder and it looks like this:

  function addFolder(parentFolder, folderInfo) {      var newFolder = parentFolder.childByNamePath(folderInfo.name);      if (newFolder == null) {          newFolder = parentFolder.createFolder(folderInfo.name);            newFolder.properties["cm:title"] = folderInfo.

Date Published : 08th of March 2012

Martin Bergljung

Published by : Martin Bergljung

About the Author : Martin Bergljung is a Principal ECM Architect at Ixxus and has been working on Alfresco solutions for the past 7 years. He implemented an email management module for Alfresco called OpsMailmanager, which involved integrating Apache James Email Server and Apache Java Caching System with Alfresco. He is a frequent blogger about Alfresco and Solr and his blog is published at ecmstuff.blogspot.com.He has also written 2 books about Alfresco called Alfresco 3 Business Solutions and Alfresco CMIS. He has presented at Alfresco Summit and Alfresco DevCon in previous years.