Sunday, December 23, 2012

Mixing Bootstrap with JSP

A few days ago I wrote a blog post about my first contact with Twitter's Bootstrap UI framework. What I showed was how to create a simple static web site which uses java script in order to set a content area. The article can be found here: http://ecmgeek.blogspot.de/2012/12/first-contact-with-bootstrap.html

This article will show how to mix Bootstrap with JSP pages to bring a bit more dynamic into the page. Here some requirements:

  • A simple HTML template which uses Bootstrap should be used to show a web site.
  • A site should have multiple pages. The site navigation should allow to navigate to the pages.
  • It should be possible to navigate through a page by using a simple navigation menu.
  • A page should show some HTML content.
So we can derive the following Java classes :
  • Site: A site has a name and a description. One site is associated to multiple pages.
  • Page: A page has a name, an id and a target URL. It also has exactly one SubMenu and one HTMLContent item.
  • SubMenu: A sub menu has multiple menu items.
  • SubmenuItem: A sub menu item has a  name and a target URL.
  • HTMLContent: Has just a String property to store the HTML. May be used as Base class for other Content items.

 So what can now do is to create a Site instance (which is indeed kept on the server side - and so a servlet container is required to host the application) in order to provide our site template with the required content.

package de.ecmgeek.bootstrap;

public class Demo {

    private Site site;
    
    public Demo() {
       
     site = new Site("Demo", "This is a demo page");
    
     Page home = new Page("Home",new HTMLContent("<b> Home ... </b>"));
     Submenu homeSubMenu = new Submenu();
     SubmenuItem homeItem1 = new SubmenuItem("HomeSection1","#section1");
     SubmenuItem homeItem2 = new SubmenuItem("HomeSection2","#section2");
     SubmenuItem homeItem3 = new SubmenuItem("HomeSection3","#section3");
     homeSubMenu.addItem(homeItem1);
     homeSubMenu.addItem(homeItem2);
     homeSubMenu.addItem(homeItem3);
     home.setSubmenu(homeSubMenu);
    
     Page about = new Page("About",new HTMLContent("<b> About ... </b>"));
     Submenu aboutSubMenu = new Submenu();
     SubmenuItem aboutItem1 = new SubmenuItem("AboutSection1","#section1");
     SubmenuItem aboutItem2 = new SubmenuItem("AboutSection2","#section2");
     SubmenuItem aboutItem3 = new SubmenuItem("AboutSection3","#section3");
     aboutSubMenu.addItem(aboutItem1);
     aboutSubMenu.addItem(aboutItem2);
     aboutSubMenu.addItem(aboutItem3);
     about.setSubmenu(aboutSubMenu);
    
     Page contact = new Page("Contact",new HTMLContent("<b> Contact ... </b>"));
     Submenu contactSubMenu = new Submenu();
     SubmenuItem contactItem1 = new SubmenuItem("ContactSection1","#section1");
     SubmenuItem contactItem2 = new SubmenuItem("ContactSection2","#section2");
     SubmenuItem contactItem3 = new SubmenuItem("ContactSection3","#section3");
     contactSubMenu.addItem(contactItem1);
     contactSubMenu.addItem(contactItem2);
     contactSubMenu.addItem(contactItem3);
     contact.setSubmenu(contactSubMenu);
    
     site.add(home);
     site.add(about);
     site.add(contact);
    
    }
    
    public Site getSite() {
        return site;
    };
}

The class Demo is only used for demonstration purposes. Usually we would use a persistence layer like Hibernate or JPA to read the site data from a database.

The next step is to  change our more or less static web site to use a Site which is provided from the server. For this purposes we use J(ava)S(erver)P(ages). It's just easy to use and to learn and you can access your beans directly via Scriptlets and expressions. Another, the more new school way, would be to export the site data via a RESTFul Web Service which talks J(ava)S(ript)O(bject)N(otation). The advantage of the second way is that you can use the data out of the box after you got it from the server with Java Script without the need to use something like JSP scriptlets to "inject" the whole data into your HTML. However, let's go with JSP here.

So our previous HTML page becomes now a JSP page, which looks like the following one:

<!-- (0) Create an empty HTML page -->
<!DOCTYPE html>
<%@page import="de.ecmgeek.bootstrap.SubmenuItem"%>
<%@page import="de.ecmgeek.bootstrap.Page"%>
<%@page import="de.ecmgeek.bootstrap.Demo"%>
<%@page import="de.ecmgeek.bootstrap.Site"%>
<%@page import="de.ecmgeek.bootstrap.HTMLContent"%>
<html>
    <!-- (1) Some basic header info -->
    <head>
        <meta charset="utf-8">
            <title>BootstrapMeetsJSP</title>
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <meta name="description" content="">
            <meta name="author" content="">

        <!-- (2) Import the provided CSS file -->
        <link href="css/bootstrap.min.css" rel="stylesheet" media="screen">    
    </head>
    
    <body>
        <!-- Do some JAVA initialization work -->
        <%
            //Build some demo data
            Site site = new Demo().getSite();
        %>
    
         <!-- (3) Import jQuery and Bootstrap -->
        <script src="js/jquery-latest.js"></script>
        <script src="js/bootstrap.min.js"></script>
        
        <!-- (5) At last add some JavaScript (jQuery) here to fill the page with life -->
        <script>
        
        $(document).ready(function(){
          
            
            //Inject some JAVA code into the JavaScript calls
            <%
              for (Page p : site.getPages())
              {
                      out.println("$('#"+ p.getId()  +"').click(function(){");    
                      out.println("$('div.my-content').empty();");
                      out.println("$('ul.nav-list').empty();");
                      out.println("$('div.my-content').html('"+p.getContent().getHtml()+"');");
                      
                    //Set the content and the submenu    
                    for (SubmenuItem sm : p.getSubmenu().getItems())
                    {
                        out.println("$('ul.nav-list').append('<li><a href=\"" + sm.getTarget() +"\"><i class=\"icon-chevron-right\"></i>"+ sm.getName() +"</a></li>');");
                    
                    }
                    
                    out.println("});");
              }
            %>            
               
         });      
        

        </script>

        
        <!-- (4) Bootstrap decorated HTML here -->
        <div id="container">

           <!-- A navigation header bar which is fixed to the top -->
           <div class="navbar navbar-fixed-top navbar-inverse">
                   <div class="navbar-inner">
                       <a class="brand" href="#"><%=site.getName()%></a>
                       
                       <ul id="header" class="nav">
                       
                           <%
                               for (Page p : site.getPages())
                               {
                                   out.print("<li><a href='"+p.getTarget()+"'id='"+p.getId()+"'>"+p.getName()+"</a></li>");                       
                               }
                           %>
                    
                    </ul>
                         </div>
               </div>
        
             <!-- (4.1) A simple web site header -->        
           <div class="hero-unit">
                <h1><%=site.getName()%></h1>
                <p><%=site.getDesc()%></p>
                <p>
                </p>
               </div>
          

           <!-- (4.2) A HTML grid with 12 columns, in this case we -->
             <div class="row">
            <!-- 4 columns are used for the left hand side navigation bar -->
                <div class="span4 bs-docs-sidebar">
                <ul class="nav nav-list bs-docs-sidenav">
                </ul>
            </div>
                
            <!-- (4.3) 8 columns are used for the right hand side content area -->
            <div class="span8">
                
                <!-- (4.4) The content area which can be decorated by own css -->
                <div class="my-content">
                </div>
            </div>
               </div>
            </div>    
    </body>
</html>
The result looks quite identical to the static page, but with the difference that the site data is now provided from the server to the client.



In summary the following happens:

  • Java code is used to generate the JavaScript code dependent on the site data which is provided by the server
  • The generated JavaScript code is used in order to manipulate the web site's HTML code dependent on the actions (click on the header menu item)
  • The HTML will be rendered on the client side (browser, ...)

It's easy to see that these are the basics to build a very simple W(eb)C(ontent)M(anagement) system which is based on Bootstrap. A next article will follow where I will describe how to add a database connection in order to retrieve the site data from a database. I can even imagine to create a simple Bootstrap based authoring web application which can be used to add data to this database.


Saturday, December 22, 2012

Site tracking with Piwik

I used Piwik in a previous Web Community project. So when I was asked how to add web statistics to Alfresco, my first idea was to integrate it with Piwik. So let's start to try it out:

At first a Piwik installation is required. My target test system is an OpenSuse 12.1 and so all the dependencies are available from the software repository. They are further described here: http://piwik.org/docs/requirements/ .

So after you installed an Apache, all required PHP modules and a MySQL database (BTW: This is something which I do not like regarding Piwik. The only supported database is MySQL. I would like to see at least Postgres support. ) you can begin to install your Piwik instance. Therefore you can follow the following instructions: http://piwik.org/docs/installation/ .

The most interesting part may be the database setup:

mysql> CREATE USER 'piwik'@'localhost' IDENTIFIED BY '${Your pwd here}';
mysql> CREATE DATABASE piwikdb;
mysql> GRANT ALL PRIVILEGES ON piwikdb.* TO 'piwik'@'localhost' WITH GRANT OPTION;


Then unzip the latest Piwik zip to your Apache2 web server and open the URL 'http://localhost/piwik'. For OpenSuse the htdocs folder is located at '/srv/www'.

If calling Piwik the first time it will prompt you to perform the following file permission changes:

chown -R www-data:www-data /srv/www/htdocs/piwik
chmod -R 0777 /srv/www/htdocs/piwik/tmp
chmod -R 0777 /srv/www/htdocs/piwik/tmp/templates_c/
chmod -R 0777 /srv/www/htdocs/piwik/tmp/cache/
chmod -R 0777 /srv/www/htdocs/piwik/tmp/assets/
chmod -R 0777 /srv/www/htdocs/piwik/tmp/tcpdf/

It's maybe required to create the user, group or directories above. You should also enable a temp. write access to '/srv/www/htdocs/piwik/config/'.

An error occurs that your php installation need zlib-suppot. You can find 'php5-zlib' in the OpenSuse software repository. Do not forget to restart Apache after installing this extension.

The next steps are quite easy. Just follow the Installation Wizard by entering your database connection details.

Also part of the installation is the generation of the tracking code. The installer says: "Here is the JavaScript Tracking code to include on all your pages, just before the </body> tag"

<!-- Piwik --> 

<script type="text/javascript">

var pkBaseURL = (("https:" == document.location.protocol) ? "https://localhost/piwik/" : "http://localhost/piwik/");

document.write(unescape("%3Cscript src='" + pkBaseURL + "piwik.js' type='text/javascript'%3E%3C/script%3E"));

</script><script type="text/javascript">

try {

var piwikTracker = Piwik.getTracker(pkBaseURL + "piwik.php", 1);

piwikTracker.trackPageView();

piwikTracker.enableLinkTracking();

} catch( err ) {}

</script><noscript><p><img 
src="http://localhost/piwik/piwik.php?idsite=1" style="border:0" alt="" 
/></p></noscript>

<!-- End Piwik Tracking Code -->

Important is the 'idsite' property. The value '1' is the id of the site which I configured as 'http://localhost:8080/share'. OK, Piwik is now up and running. Now let's investigate how to customize Alfresco in order to use it.

So to enable global tracking we can search a header or footer element which is used by every page of Alfresco. So let's check which site web scripts are available and let's see if we can put our snippet to one of the freemarker templates.

One good candidate seems to be '${WEBAPPS}/share/WEB-INF/classes/alfresco/site-webscripts/org/alfresco/components/header'.  So I just placed the script above after the first other '<script>' block.

A test showed the following result:


Piwik can do a lot more. But for now this shows exactly what we required. It answers the question which site was accessed how often.







Thursday, December 20, 2012

First contact with Bootstrap

Insprired by Thomas Glaser and Jan Pfitzner, I thought it could be a good idea to get in contact with Twitter's Bootstrap framework. So here a simple skeleton page which uses Bootstrap components to reflect a simple web site. jQuery is used to interact with menu and the content area.

Here the result:



And finally the code:

<!-- (0) Create an empty HTML page -->
<!DOCTYPE html>
<html>
    <!-- (1) Some basic header info -->
    <head>
        <meta charset="utf-8">
            <title>Basic web site</title>
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <meta name="description" content="">
            <meta name="author" content="">

        <!-- (2) Import the provided CSS file -->
        <link href="css/bootstrap.min.css" rel="stylesheet" media="screen">   
    </head>
   
    <body>
         <!-- (3) Import jQuery and Bootstrap -->
        <script src="js/jquery-latest.js"></script>
        <script src="js/bootstrap.min.js"></script>
       
        <!-- (5) At last add some JavaScript (jQuery) here to fill the page with life -->
        <script>
        $(document).ready(function(){
         
           $("#header-home").click(function() {
            $('div.my-content').html('<p>Home ...</p>');
            });

          $("#header-contact").click(function() {
            $('div.my-content').html('<p>Contact ...</p>');
            });


          $("#header-about").click(function() {
            $('div.my-content').html('<p>About ...</p>');
            });

          $("#header-contact").click(function() {
            $('div.my-content').html('<p>Contact ...</p>');
            });

         });     
            </script>

       
        <!-- (4) Bootstrap decorated HTML here -->
        <div id="container">

           <!-- A navigation header bar which is fixed to the top -->
           <div class="navbar navbar-fixed-top navbar-inverse">
                   <div class="navbar-inner">
                       <a class="brand" href="#"> Basic web site</a>
                       <ul id="header" class="nav">
                           <li class="active"><a href="#" id="header-home">Home</a></li>
                        <li><a href="#about" id="header-about">About</a></li>
                        <li><a href="#contact" id="header-contact">Contact</a></li>
                    </ul>
                         </div>
               </div>
       
             <!-- (4.1) A simple web site header -->       
           <div class="hero-unit">
                <h1>Basic web site</h1>
                <p>This site explains the Twitter Bootstrap a little bit.</p>
                <p>
                </p>
               </div>
         

           <!-- (4.2) A HTML grid with 12 columns, in this case we -->
             <div class="row">
            <!-- 4 columns are used for the left hand side navigation bar -->
                <div class="span4 bs-docs-sidebar">
                <ul class="nav nav-list bs-docs-sidenav">
                     <li><a href="#Marker1"><i class="icon-chevron-right"></i> Marker 1</a></li>
                     <li><a href="#Marker2"><i class="icon-chevron-right"></i> Marker 2</a></li>
                     <li><a href="#Marker2"><i class="icon-chevron-right"></i> Marker 3</a></li>
                </ul>
            </div>
               
            <!-- (4.3) 8 columns are used for the right hand side content area -->
            <div class="span8">
               
                <!-- (4.4) The content area which can be decorated by own css -->
                <div class="my-content">
                      <p> Home ...</p>
                </div>
            </div>
               </div>
            </div>   
    </body>
</html>

Friday, December 14, 2012

About the anatomy of ArchiveLink

Preamble

I just played arround with ArchiveLink, and so I decided to read the specification to inform myself how it internally works. What you can use in order to archive your SAP documents (invoices, ...) is an ArchiveLink speaking HTTP Content Server. So here a short summary of what I understood from the ArchiveLink specification so far.

Terms

We want to store documents inside a content repository. I guess it is not required to explain this term further. ArchiveLink knows the term 'component', whereby a component represents a content unit on a administrative level. Several component types are used (for instance 'data'),  Components are summarized via a document header. So here a scenario:

  • One Content Repository contains multiple document headers
  • One document header references multiple components
  • One component contains one content unit
A document header (id, status, date, ...) and a component (content type, status, ...) has some adminstrative meta data attached.

So an archive needs to reflect that terms somehow. The most intuitive way seems to reflect it by using folders. So the content repository contains a folder of the type header which contains component folders those are containing documents to keep the content. Important is that ArchiveLink does by design not transfer any SAP business object related meta data. Only administrative meta data is transfered to the archive.

The protocol

HTTP is used to exchange date on a lower level. So we are speaking about a kind of RESTFul service access. The format is 'http://${server}:${port}/${service name}/${command}?${command parameters}. If we take security into account (more about it later) and take a spot on the get command by retrieving the data component then the URL has the following format:

http://${host}:${port}/${service name}?get&contRep=${repo id}&docId=${doc id}&compId=data&accessMode=r&authId=${user id}&expiration=${time}?secKey=${base64 encoded security key}.

The following commands are available:

  • get: get a content unit
  • info: get info about a document
  • docGet: get the whole content
  • create: Create a new document
  • update: Modify an existing document
  • append: Append data to a content unit
  • search: Search by using a pattern inside a content unit (full text search)
  • attrSearch: Search for a specific attribute value
  • mCreate: Create new documents
  • serverInfo: Retrieve information about the content server
  • putCert: Transfer the client certificate
The resonse is a little bit old scool (the specification document is from 2001) and so does not return JSON, but Multipart Form Data.

Here an example response from the publically available specification document:

HTTP/1.1 200 (OK)
Server: Microsoft-IIS/4.0
Date: Wed, 04 Nov 1998 07:41:03 GMT
Content-Type: multipart/form-data; boundary=A495ukjfasdfddrg4hztzu...
...some more header informations...
Content-Length: 32413
X-dateC: 1998-10-07
X-timeC: 07:55:57
X-dateM: 1998-10-07
X-timeM: 07:55:57
X-contRep: K1
X-numComps: 2
X-docId: ID
X-docStatus: online
X-pVersion: 0045
--A495ukjfasdfddrg4hztzu898aA0jklmAxcvla12319981147528895
Content-Type: application/x-alf; charset=
Content-Length: 2591
X-compId: descr
X-Content-Length: 2591
X-compDateC: 1998-10-07
X-compTimeC: 07:55:57
X-compDateM: 1998-10-07
SAP AG SAP ArchiveLink (BC-SRV-ARL)
docGet
April 2001 265
X-compTimeM: 07:55:57
X-compStatus: online
X-pVersion: 0045
...component data ...
--A495ukjfasdfddrg4hztzu898aA0jklmAxcvla12319981147528895
Content-Type: application/x-alf; charset=
Content-Length: 29313
X-compId: data
X-Content-Length: 29213
X-compDateC: 1998-10-07
X-compTimeC: 07:55:57
X-compDateM: 1998-10-07
X-compTimeM: 07:55:57
X-compStatus: online
X-compStatus: online
X-pVersion: 0045
...component data ...
--A495ukjfasdfddrg4hztzu898aA0jklmAxcvla12319981147528895--
Beim docGet-Kommando auf ein leeres Dokument steht im Response-Body beispielsweise:
--A495ukjfasdfddrg4hztzu898aA0jklmAxcvla1231999102562159269
--A495ukjfasdfddrg4hztzu898aA0jklmAxcvla1231999102562159269--
Summary

This is the furst article of several ones those are focusing on ArchiveLink. What we should have learned is the following: An ArchiveLink archive can be realized by using a HTTP Content Server. The Content Server then could speak with a simple File System or even better an DMS. The content server provides several commands and functions to store and access content items. There are several kinds of content items, wrapped by components. A specific URL pattern is used on side of the HTTP Content Server. The response of the HTTP Content Server has the Multipart Form Data format.


Monday, December 3, 2012

How to setup an Alfresco Maven project in a few steps

 Install the tools and libraries
  1.  Install Maven (E.G. use 'sudo apt-get install maven2')
  2. Install Subversion (E.G. use 'sudo apt-get install subversion')
  3. Install the Subversion Java bindinds (E.G. use 'sudo apt-get install  libsvn-java)
 Install Eclipse and the required plug-ins (optional)
  1. Install Eclipse (you will need it as the I(ntegrated) D(evelopment) E(nvironment)
  2. Install the Eclipse Maven plug-in (The download site is 'http://download.eclipse.org/technology/m2e/releases'
  3. Install the Subversion plug-in (The easisest is to enter subversion in the Eclipse Market Place)
  4. In Eclipse create a new workspace
Create the project for the repository extensions
  1.  Change the directory to the Eclipse workspace
  2. Then run the following command
mvn archetype:generate -DarchetypeGroupId=org.alfresco -DarchetypeArtifactId=maven-alfresco-amp-archetype \
-DarchetypeVersion=3.9.1 -DgroupId=${Your domain here, E.G. de.ecg} -DartifactId=${Your project name -repo here, E.G. my-repo} -Dversion=1.0-SNAPSHOT \
-DarchetypeRepository=https://artifacts.alfresco.com/nexus/content/repositories/releases -DinteractiveMode=false
 
 The above command contains 2 place holders. One specifies your domain the other one should name the project. For example the project 'my-repo' will be created.

Create the project for the Share extensions
  1.  Change the directory to the Eclipse workspace
  2. Then run the following command

mvn archetype:generate -DarchetypeGroupId=org.alfresco.maven -DarchetypeArtifactId=maven-alfresco-share-archetype \
-DarchetypeVersion=3.9.1 -DgroupId=${Your domain here} -DartifactId=${Your project name -share here} -Dversion=1.0-SNAPSHOT \
-DarchetypeRepository=https://artifacts.alfresco.com/nexus/content/repositories/releases -DinteractiveMode=false
 
Create the Eclipse projects
  1. Open Eclipse
  2. Click on 'Import -> Maven -> Existing Maven Project into Workspace'
  3. Navigate into the folder of the repo project and confirm
  4. The download of the several dependent artifacts / libraries to your local Maven cache may take a while
  5. Do the same for the Share project
A really cool and easy way to get an Alfresco project set up quite simpler than with ANT script. In the pom.xml you can see that the referenced Alfresco version is 4.0.2b which seems to be an older Community Edition release. An open question is how to setup a similar project for the Alfresco Enterprise Edition.

Wednesday, July 25, 2012

How to enable DQL tracing

I am currently analyzing some Documentum performance issues. To be able to reconstruct the problem maybe more easier or to check the database indexes it is useful to identify the problematic DQL queries. DQL is Documentum's Query Language. In fact a DQL statement is translated into an SQL one. The Documentum Administrator can return you the SQL for every running DQL. So what you need to get the DQL is to add the following lines to your dfc.properties file on side of your Content Server installation (E.G. ./product/6.5/shared/config/dfc.properties):
dfc.tracing.enabled = false
dfc.tracing.recordParameters = on
dfc.tracing.recordReturnValue = on
dfc.tracing.stackDepth = 100
dfc.tracing.combineDMCL = on
dfc.tracing.dir = /tmp/dfclogs
Any further ideas how to trace the DQL? Then just post a comment to this article!

Wednesday, June 20, 2012

A simple pinboard dashlet

0.) Preamble

This example shows how you could develop simple Alfresco Dashlets. So here some requirements:

  • A dashlet is required to see and post short messages
  • Everybody should be able to post messages to the specific site where the dashlet is available.
1.) Define the Content Model 

At first we need to define the content model for our pinboard entry type. The content model looks as the following:


<?xml version="1.0" encoding="UTF-8"?>

<!-- Model definition -->
<model name="ecg:pinboardmodel" xmlns="http://www.alfresco.org/model/dictionary/1.0">

    <!-- Optional meta-data about the model -->
    <description> The pinboard model </description>
    <author> David Maier </author>
    <version> 1.0 </version>

    <!-- Imports are required to reference definitions in other models -->
    <imports>
           <import uri="http://www.alfresco.org/model/dictionary/1.0" prefix="d" />
           <import uri="http://www.alfresco.org/model/content/1.0" prefix="cm" />
    </imports>

    <!-- The name space of our model -->
    <namespaces>
           <namespace uri="http://www.ecmgeek.de/model/content/1.0" prefix="ecg" />
    </namespaces>
   

    <!-- Content types -->
    <types>
   
           <!-- Default types -->
          <type name="ecg:document">
            <title>Document</title>
            <parent>cm:content</parent>
         </type>
        
         <type name="ecg:folder">
            <title>Folder</title>
            <parent>cm:folder</parent>        
         </type>

           <!-- Specific types -->
           <type name="ecg:pinboardentry">
              <title>PinboardEntry</title>
              <parent>ecg:document</parent>
              <properties>
                 <property name="ecg:subject">
                    <type>d:text</type>
                 </property>
                 <property name="ecg:description">
                    <type>d:text</type>
                 </property>
              </properties>
           </type>         
</types>
</model>

You can see that we just extended the default types by deriving the type 'ecg:pinboardentry'.

I deployed the model by just adding it to the Data Dictionary.

2.) Prepare the site 

The next step is to prepare the site which you would like to use. This I just created a subfolder 'pinBoard' inside the site's folder.

3.) Create Data Web Scripts

Two Web Scripts are required. One to add a pinboard entry to a specific site and another one to get all pinboard entries from a specific site.

3.1.) Add entry Web Script 

Here the descriptor:
pinboardadd.get.desc.xml
<?xml version="1.0" encoding="UTF-8"?>
<webscript>
    <shortname>Add Pinboard entry</shortname>
    <description>To add a pinboard entry</description>
    <url>/alfintra/pinboard/add?name={nameArgument}&amp;desc={descArgument}&amp;site={siteArgument}</url>
    <format default="xml">extension</format>
    <authentication>user</authentication>
    <transaction>required</transaction>
</webscript>

And the Java Script controller:

pinboardadd.get.js
/**
 * Arguments
 */
//The target site name
var siteName = args["site"];

//The name of the future entry
var entryName = args["name"];

//The description of the future entry
var entryDesc = args["desc"];


/**
 * Argument validation
 */
var siteFolder = companyhome.childByNamePath("Sites/" + siteName + "/pinBoard" );

if (siteFolder == undefined )
{
   status.code = 404;
   logger.log(status.code);
   status.message = "The site's pinBoard folder was not found. Did you prepare your site in order to use the pinboard?";
   logger.log(status.message);
   status.redirect = true;
}
else
{
    addPinboardEntry(siteFolder, entryName, entryDesc);
}


/**
 * Script logic
 */
function addPinboardEntry(siteFolder, entryName, entryDesc)
{
    logger.log("Entering addPinboardEntry");
   
    logger.log("Setting properties");
      var props = new Array();
    props["ecg:subject"] = entryName;
    props["ecg:description"]= entryDesc;
       
    logger.log("Creating node");
      siteFolder.createNode(entryName,"ecg:pinboardentry", props);

    logger.log("Setting model");
     model.created = "true";

    logger.log("Leaving addPinboardEntry");
}

Finally the presentation template. This template shows nothing. It just redirects to the page from which the html page was called.

pinboardadd.get.html.ftl
<script type="text/javascript">

  var ref = document.referrer;
  location.replace(ref);
</script>

3.2.) Get entries Web Script

Here the descriptor:

pinboardlist.get.desc.xml 
<?xml version="1.0" encoding="UTF-8"?>
<webscript>
    <shortname>List pinboard entries</shortname>
    <description>To list the pinboard entries of a site</description>
    <url>/alfintra/pinboard/list?site={siteArgument}</url>
    <format default="xml">extension</format>
    <authentication>user</authentication>
    <transaction>required</transaction>
</webscript>

 The Java Script controller:

pinboardlist.get.js
/**
 * Arguments
 */

//The target site name
var siteName = args["site"];

/**
 * Argument validation
 */
var siteFolder = companyhome.childByNamePath("Sites/" + siteName + "/pinBoard" );

if (siteFolder == undefined )
{
   status.code = 404;
   logger.log(status.code);
   status.message = "The site's pinBoard folder was not found.";
   logger.log(status.message);
   status.redirect = true;
}
else
{
    listPinboardEntries(siteFolder);
}

function listPinboardEntries(siteFolder)
{
    logger.log("Entering listPinboardEntries");
      logger.log("Getting all childs");
    model.entries = siteFolder.children;
    logger.log("Leaving listPinboardEntries");   
}

The JSON output:

pinboardlist.get.json.ftl
<#escape x as jsonUtils.encodeJSONString(x)>
{
"pinboard":
  {
   "entries":
    [
        <#list entries as node>
              <#if node.properties["ecg:subject"]?exists>
                   <#if node.properties["ecg:description"]?exists>  
        {"name":"${node.properties['ecg:subject']}", "desc":"${node.properties['ecg:description']}", "site":"${args.site}"}
       <#if node_has_next>,</#if>                  
                  </#if>
            </#if>
    </#list>
    ]
 }
}
</#escape>

4.) Create the Dashlet
The idea is now that that the controller of the Web Script accesses the data which is provided by the data web script. The controller passes the data to the model and the presentation template renders it inside a dashlet. The content is presented within the dashlet's body container.

To create a new entry the URL of the 'Add Entry' Web Script is called as a form action. Here we do not use AJAX, instead a simple HTML form is used to call the entry creating Web Script. After this Web Script is called, it just redirects back to the previous page. By using additional Java Script inside the presentation template, it should be also possible to perform such an action without the need to redirect to the previous called page by refreshing this one. However, the dashlet shows how the interaction with the Data Web Scripts can basically work. The form is presented within the dashlet' toolbar container.

So what we need at first is a Dashlet Descriptor:

.../site-webscripts/de/ecmgeek/components/dashlets/pinboardlist.get.desc.xml
 <webscript>
    <shortname>List pinboard entries</shortname>
    <description>To list pinboard entries</description>
    <family>site-dashlet</family>
    <url>/components/dashlets/pinboardlist</url>
</webscript>

We also need a controller which gets the data from our Data Web Script and passes it to the model:

.../site-webscripts/de/ecmgeek/components/dashlets/pinboardlist.get.js
var siteName = page.url.templateArgs.site;
var data = remote.call("/alfintra/pinboard/list.json?site=" + siteName);
var results = eval('(' + data + ')');

model.entries = results.pinboard.entries;
Finally we need to create the dashlet's UI. You could basically add any HTML which you want to the dashlet's html template. But to integrate better with Alfresco, you should at least define the following containers:
  • dashlet
    • toolbar
    • body
 If you want that your dashlet is resizable you should also instantiate an Alfresco Dashlet Resizer by using the following line inside a JavaScript block:
  •  new Alfresco.widget.DashletResizer("${args.htmlid}", "${instance.object.id}");)
Interesting to know is that Alfresco Share comes with a Proxy servlet which allows you to access the Data Web Scripts of the Alfresco Repository. This proxy can be also used to avoid problems with the same origin policy if using client side Java Script to access the Data Web Script.

Our dashlet code looks now as the following:

 .../site-webscripts/de/ecmgeek/components/dashlets/pinboardlist.get.html.ftl

<script type="text/javascript">//<![CDATA[
   
   //Make the dashlet resizeable
   new Alfresco.widget.DashletResizer("${args.htmlid}", "${instance.object.id}");
//]]>
</script>
 
<div class="dashlet">
    <div class="title">
        ${msg("title")}
    </div>
     
    <div class="toolbar">
        
           <form name="postToPinboardForm" method="get" action="/share/proxy/alfresco/alfintra/pinboard/add.html">
             <table>
                <tr>
                   <td> ${msg("subject")} </td>
                   <td>  ${msg("desc")} </td>
                   <td> &nbsp; </td>        
                </tr>
 
                <tr> 
                   <td> <input type="text" name="name"> </td>
                   <td> <input type="text" name="desc"> </td>
                   <td> <input type="hidden" name="site" value="${page.url.templateArgs.site}"></td>
                   <td> <input type="submit" value="Post"> </td>
                </tr>
             </table>
            </form>        
    </div>
     
 
    <div class="body  scrollableList" id="${args.htmlid}-body">
  
        <table>
       
        <#list entries as e>
              <tr> <td> <b> ${e.name} </b> </td> </tr>
              <tr> <td> ${e.desc} </td> </tr>
              <tr> <td> &nbsp; </td> </tr>
        </#list>
 
        </table>
     
    </div>
</div>

The template reads some properties from the following properties file (or message bundle):

 .../site-webscripts/de/ecmgeek/components/dashlets/pinboardlist.get.properties
title=Pinboard
subject=Subject
desc=Description

Even if this is not the most complicated dashlet, I hope that it may help you to get started with dashlet development. Finally the dashlet looks this way:








 

Tuesday, June 12, 2012

Theme it

Today I had to investigate how to add a custom theme to my Alfresco 4.x installation. Some parts were a bit tricky, but the most of it was quite simple. Here some useful steps:

  1. Navigate to $WEBAPPS/share/WEB-INF/classes/alfresco/site-data/themes . BTW: $WEBAPPS is the deployment folder of your servlet container.
  2. Copy the file yellowTheme.xml to myTheme.xml
  3. Edit the file myTheme.xml by setting the title and the id. The id has to be 'theme.myTheme'
  4. Navigate to $WEBAPPS/themes!
  5. Make a copy of the complete folder 'yellowTheme' by naming it to 'myTheme'.
  6. Naviagate to $WEBAPPS/themes/myTheme/images!
  7. Open all the images by using E.G. the G(nu) I(mage) M(anipulation) P(rogram) . The Command is 'gimp'. Then colorize the images to fit your required base color. I just colorized every image which had a yellow color before. The main loge image is named 'app_logo.png' and can be replaced by your prefered one.
  8. Navigate back to $WEBAPPS/themes/myTheme!
  9. Edit the file presentation.css by setting the required colors. Again, I just copied the color codes to GIMP by only replacing the yellow color tones with my prefered ones.
  10. Now the tricky part. There are some css dependencies those need to be changed inside this file. So just replace every occurrence of 'yellowTheme' with 'myTheme' inside this file.
  11. Naviagete to $WEBAPPS/themes/myTheme/yui/assets
  12. Inside the file 'skin.css', also replace every occurrence of 'yellowTheme' with 'myTheme'.
  13. Restart Alfresco
  14. Open Alfresco Share, create a new site and choose the entry 'myTheme' as the site theme.
  15. Perform some CSS changes by being able to test your changes immediately by refreshing the just created site.
  16. Open the Administration Console and set the theme 'myTheme' as the gloabl one.
I hope this article helps you to get quickly started with customizing your Alfresco theme.

Friday, April 27, 2012

Alfresco cloud service

Alfresco started a cloud service. More detailed it is is an instance of Alfresco Share which is avaiable in the WWW. The smallest offering is for FREE and it allows you to use Share functions like:
  • The document library
  • The dashboard
  • Collaborate on documents
  • Manage site members
Further features and space is available for monthly fees. However, if you want to try it out then it is possible to register here: http://cloud.alfresco.com .

I just created an instance for magicable.de and created two user accounts. However both have to log-in to be able to see the content. So this service is not suitable for sharing your content accross the web. It's more for companies or cloesed communities those need to access and collaborate on their documents inside the web. So there seems to be no guest access to your Share instance in the cloud.

Unfortunately the free instance does not allow to access it via an API. So you can't use it as content repo in the web. As far as I understand this functionality will be available later for the paid version of it.

However, if you want to store some documents online (including meta data - this is something which can't drop box do for you), then Alfresco in the Cloud may be an alternative for you.
 

Are we brain-washed?

In fact I am coming from the database world. This means I worked as an Engineer for the vendor of an relational database and also did Consulting for a No-SQL company. In both cases I had contact with ECM and related systems. So as I worked for Ingres I did Partner Certification work for systems like Alfresco and Liferay. In the past the system borders of an ECM system were very clear to me. So everything beyond the storage of just Meta Data was out of the scope of the Database System itself. I became a 'border liner' during my time as a Senior Consultant for a Graph Database System. The vertices could store not just meta data but also provide you a Content Stream. The system aimed to support unstructured data very well (as mentioned it supported Content streams and also something that was named Undefined Attribute). There were even Document Management use cases those were covered by this system. However ... . During my work with ECM, I realized that ECM companies understand themself more and more as Data Management companies. (Another point seems to be that today everything has to be Social or in the Cloud.) We tend to see everything as document. By definition a document is a set of properties thoses are used to store the meta data and the content itself. This raises questions like:
  • Is a document without content really a document?
  • Is really everything which has meta data and content a document?
I am sure the answer is 'It depends', but I think we should go back to the roots by not using ECM-Systems like Databases. We should ask questions like:
  • Is there a real paper document involved in a given use case?
  • Are there some real workflows with real persons those are handling real documents?
  • What should the person know about the document?
If the answer is 'Yes', then we can be sure that  we are doing right by storing a digital document which has the right set of properties regarding the given processes and requirements.

But what's if the answer is 'No'. In my opinion we should then not use an ECM/DM system to store such data, even if we want to do it because it seems to be close because there is already one available. Just use a Database System (NoSQL or Relational, whatever the use cases are requiring) to store the data regarding the required data model. Be not brain-wahsed! Do not belive that ECM solves all your Data Management problems and the answers to the above questions will be often (but not every time):
  • A document without content should be an object in a datbase (row, vertex, ...).
  • Not everything which has meta data and content is really a document!
What do you think about?


Monday, April 16, 2012

Alfresco Web Scripts

A few days ago I talked about 'Server side Java Script'. This post describes a technique which is named 'Web Script' and explains for which purpose it could be useful. With Web Scripts Alfresco allows to easily deploy content oriented RESTFul Web Services to the Repository. The logic behind these services is then often realized by using Java Script.

A Web Script contains 4 pieces:
  • A descriptor file
  • A Java Script (or a Java Class)
  • A freemarker template
  • Language bundles
The descriptor describes how (which URL) and who can run the Web Script. The Java Script file works as a kind of Controller by setting a model. The Freemarker template contains the presentation logic and the language bundles are allowing multi language support.

Let's begin by investigating the Desctiptor file in detail. Such an descriptor contains:
  • The short name (shortname)
  • The description (description)
  • The url by including the parameters (url)
  • The default format
  • The authentication method
Here some example code:

<webscript>
  <shortname>Hello World</shortname>
  <description>Greet a user</description>
  <url>/helloworld?to={name?}</url>
  <url>/hello/world?to={name?}</url>
  <format default="html">extension</format>
  <authentication>user</authentication>
</webscript>
 Above you can see a Web Script which has the name 'Hello Word'. It 'Greets a user'. The URL is mapped to '/helloworld', whereby one Parameter 'to' is expected. The default response should be 'html' and a user authentication is required in order to execute the script. The value 'extension' means that the requested format is determined by the extension which is used as part of the URL. So for instance 'world.html' or 'world.txt'. As authentication the following values are possible: 'none', 'guest', 'user' or 'admin. Possible formats are: 'html', 'text', 'xml', 'atom', 'rss' or 'json'. There is an optional parameter which is named 'runas'. This parameter is used to execute the Web Script inside the context of another user's session. To be able to fully use this feature it is required that the Web Script is deployed in a static way. (We will talk about the deployment options later in this article.)

What we need next is to investigate the Java Script file. There are specifc naming conventions. So a Java Script which is accessible via HTTP GET should have an Descriptor and a Java Script file which is named as the following:
  • ${id}.${http method}.desc.xml
  • ${id}.${http method}.js
For instance: helloworld.get.desc.xml and helloworld.get.js .



The Java Script file can contain basic Java Script (or even Java because it is interpreted by a Java based engine) and calls of the Alfresco Java Script API.  Let me give you a short overview about this API. It is even useful if you want to implement Actions or Workflows for the Alfresco System. You can think about this Java Script API like a built in language to access Content. If you know Procedural Languages for Database Systems, then you know what I mean. Indeed, the possibilities those are provided by this API are quite beyond the ones of for instance PL/SQL. So even if you are not a Java Script guy, it should be possible to use the API. There are multiple versios of the API available. This article focuses on the 3.4 one.

A script can import another one:

 <import resource="/Company Home/Data Dictionary/Scripts/library.js">
The example above shows a 'dynamic' import. Which means that the imported script is stored in Alfresco itself. It's also possible to just import from the classpath by using "classpath:${path to js}".

 The API provides some root objects. Those objects are available out of the box.

  • companyhome: The company home root folder
  • userhome: The current user's home folder
  • person: The current user object
  • search: The search API
  • people: The people API
  • actions: The action API
  • logger: A logger
  • session: Provides session specific information
  • classification: The classification API
  • utils: Some helpers
  • groups: To access group authorities
  • stites: The sites API
As you can see the API-s can often be accessed by using root object. Often the result of an API call is a document (Or at least the reference to one). If you use the Java Script API, then this is named 'Script Node'. For instance a 'userhome', 'companyhome', 'document', 'space' (Is äquivalent to a folder in Alfresco) or a 'person' is script node. So let's investigate the API-s a little bit more detailed!

The Script Node API has the following important methods:
  • properties
  • children
  • assocs
  • aspects
  • content
  • id
  • nodeRef
  • name
  • type
  • parents
  • childByNamePath
  • getSite
  For all of you who are not such familar with Alfresco: A node has a specific type. A type defines a set of properties. So Properties can be inherited by creating sub-types. An aspect is a bag of properties which can be assigned to a type independent from the inheritance hierarchy.  A node can also have associations to other nodes. A specific association is the child association (if you delete the parent then the childs will become deleted, too).

The properties attribute is an associative array. You can access it by using 'node.properties.propx' or 'node.properties["propx"]'.

Beyond the Script Node API there is also the "Modify and Creating" API. Here some important methods:
  • createFile
  • createFolder
  • createNode
  • addNode (as child)
  • removeNode (remove child assocs)
  • createAssociation
  • move
  • copy
  • addAspect
Also as an extension of the Script Node API, you can think of the Check-in/out and Versioning API
  • checkout
  • checkin
  • cancelCheckout
  • isVersioned
  • versionHistory
  • getVersion
  • createdDate
  • creator
  • label
  • type
  • descrption
  • nodeRef
  • node
The methods are used the same way as the Script node API ones.

The content of the node was accessed by using the content attribute. This one provides the following API:
  • mimetype
  • encoding
  • size
  • url
  • downloadURL
So a node can also have content of a specific mime type.

The search API allows you to search for specific documents. The following methods are part of the search API:
  • luceneSearch
  • xpathSearch
  • savedSearch
  • findNode (by given node reference)
  • query (by using an object which consists a search method and the search query)
The following query is a CMIS one:

var query = {query: "SELECT e.* FROM ecg:mytype AS e WHERE e.ecg:myprop=" + myvar, language: "cmis-alfresco"};

 The people API allows to handle users those are stored inside Alfresco. Here the methods of it:
  • createPerson
  • deletePerson
  • getGroup
  • createGroup
  • parentGroup
  • getMembers
  • isAdmin
  • changePassword
  • setPassword
 The Actions API is used to create an Action object. Then the actions parameters can be set. Afterwards it is possible to execute the action:

var myaction = create("myaction");
myaction.parameters.myparam = 'myvalue';
myaction.execute;
 The classification API provides the following methods:
  • getAllCategoryNodes
  • getAllClassificationAspects
  • createRootCategory
  • isCategory
  • categoryMembers
  • subCategories
  • createSubCategory
  • removeCategory
This section covered not all available API-s. Further more there are the Permission API, the Transformation API, the Thumbnailing API, the Tagging API, the Workflow API, the AVM API and the WCM API.

A special root object for Web Scripts is the one which is named 'model'. It allows you to set the result of the execution of you web script. Here an example:

var username = person.properties.userName;
model.username = username;
The model object is used to transfer data from the controlling Web Script to the Freemarker template. So let's take a closer look on Freemarker templates!

Basically a freemarker template can be anything which is textual. So you can write it as html, as text as xml. The idea behind it is to use some static text with some dynamic text from the model. There is a whole language about this freemarker stuff. We just look on the most important parts:
  • Place holders
  • Data model
  • Variables and Types
  • Sequences
  • Directives
  • Comments
  • Built-ins
Place holders can be found as ${myPlaceholder} inside the template. Such a placeholder will then become replaced regarding the data from the model.

The data model is a Tree and can be set. Every value which you access via a place holder is stored inside the data model. Everything which is not a leaf inside the data model's tree is named 'Hash'. The variables inside the tree those are storing single values (the leafs of the tree) are named 'Scalars'. You can navigate through the model's tree by using indexes. For instance 'animals[0].name'.  Scalars are typed: String, Number, Date/Time, Boolean .

Sequences are lists of strings:

["myval1", ... , "myvaln"]

There are several directives. The 'if' directive looks as the following:

<#if myvar = "mystringval">  My text <#else> My other text </#if>
Possible operators are: <, >, !=, = .

The list directive is used to itterate over elements of the data model.

<# list myhashvar as myvar> ${myvar.myscalarvar} </#list>
 <# list myseq as myvar> ${myvar} </#list>

The include directive can be used to insert content from another file to the template:

<#include "/myfile.html">
It is possible to create user defined directives the following way:

<@mydirective myparam1=myval1, ... myparamn=myvaln>
    <#--   Some commands --#>
</@mydirective>
 Comments are used the following way:
<#-- My comment --#>
The following built in functions are available:

  • myvar!"My optional value": Uses the optional value if  myvar is not set
  • myvar??: Returns true if myvar is set
  • myvar?upper_case: Returns the variable value in upper case letters
  • myvar?cap_first: Converts the first letter to upper case
  • myvar?trim
  • myvar?size
  • myvar?int: The integer part of a number (E.G. 5 in case of 5.432)
  • myvar?string_short: Date to String conversion
  • myvar?foo:string : Boolean to Strng conversion 
Alfresco extended the capabilities of Freemarker templates a bit, so that you can access some specific API elements directly from the Freemarker Template without the need of a Java Script as the controller. However, I would recommend to stay with the MVC-pattern!

Let's now investigate how multiple languages are supported. The default message bundle is a properties-file with the following name:
  • ${id}.${http method}.properties
 To support multiple languages, you can create a new file which is named:
  • ${id}.${method}_${locale}.properties
The messages are then accessible directly from your Freemarker template via a function call:

 ${msg("myprop")}
So the final question of this article is: How can I deploy and use such a Web Script. There are multiple ways to deploy a web script:
  • Dynamic deployment: To the Content Repository itself. Which mean you need to import it to 'Data Dictionary/Web Scripts' or 'Data Dictionary/Web Script Extensions' (The second folder should be used to override scripts from the first folder for testing purposes.)
  • Static deployment by copying the folder which contains the Web Script files  to ${CATALINA_HOME}/webapps/alfresco/WEB-INF/classes/alfresco/templates/webscripts. This can be simplified by bundling the Web Script into an Alfresco Module Package.
  • Extension deployment by copying the folder which contains the Web Script files to ${CATALINA_HOME}/shared/classes/alfresco/extension/templates/webscripts
Afterwards you have to refresh the list of available Web Scripts. No server restart is required:

  • Navigate to 'http://${myhost}:8080/alfresco/service/index'
  • Click on the 'Refresh Web Scripts' button

Sunday, March 11, 2012

Server side JavaScript

I really do not like the idea to use server side Java Script. In fact I am really hating Java Script even on the client side. I everytime worked arround even the client side one by using the GWT framework. (GWT allows you to write Java code which will become translated to JavaScript. So my question was "Why the hell should anybody like to have server side Java Script?". There two different answers from two different perspectives:
  • As a Developer for Alfresco, you just need (not really you can also use the more complicated Java API) to know that it is existent, because they invented something which is called WebScript.
  • Because it is interpreted code, it is really more flexible. So you can just change it on runtime. A JavaScript engine then is used to just execute your code. Such an engine is E.G. Rhino

WebScripts
A WebScript is just a JavaScript file which can be accessed as a RESTFul service. It sets some output model which is then used together with a Freemarker template to generate the response. So it is following a Model View Controller concept. The WebScript takes the role of the controller. It has access to a model. A freemarker template is used for the View. (This could be HTML or just JSON or plain TXT) A WebScript has access to Alfresco-s JavaScript API. (http://wiki.alfresco.com/wiki/3.4_JavaScript_API). So it can access content by providing some output (E.G. the content properties as XML). The advantage is that you can just deploy content oriented RESTFul Web Services without the need to restart your Servlet-Container or Application server. Alfresco is using it quite often, even for their Share-Dahslets. Meanwhile it also became added to the Spring Web-Framework (http://www.springsource.org/extensions/se-surf)

Rhino
Rhino is a JavaScript engine which is implemented in Java. You can just use it to extend your own Java application to support JavaScript. Further information are available here: http://www.mozilla.org/rhino/

What's your opinion about server side JavaScript? Just give me some comments!

CMIS

This article gives answers regarding the following questions:
  • What is CMIS?
  • Which purpose has CMIS?
  • How to use it?
  • Does it realize what it promises?

What CMIS is
 The Acronym CMIS stands for Content Management Interoperability Services. I think that the 'Content Management' part of it is quite clear to you. So let's focus on the IS part of CMIS. Interoperability means that a system which provides CMIS should be compatible regarding the Content Access to another one which provides CMIS. So it means that two CMIS capable systems are compatible regarding the CMIS access. The Service part means that CMIS has to be provided as a Web Service by using either a RESTFul or a SOAP one. So in theory CMIS was invented to make the content acccess more independent from the Content Managing System. It's a standard access layer realized as Web Service-s for accessing a Content Repository.

What CMIS not is
It's implemented by following an open standard. So specific features of a specific ECM-System or Content Repository may not be covered by CMIS. Let's understand it as the smallest subset of functionality which should be provided by any ECM-System or Content-Repository.

Some history
There is a consortium named O(pen) and A(dvanced) S(tandards) for I(nformation) S(ociety) which created the CMIS standard in May 2010. All bigger ECM players were part of this consortium (Adobe, Alfresco, ASG, Booz Allen Hamilton, Day Software, dotCMS, EMC, FatWire, fme AG, IBM, ISIS Papyrus, Liferay, Microsoft, Nuxeo, Open Text, Oracle, SAP, Saperion, WeWebU). So you can see that CMIS is quite a young standard.

Explaining the standard
So before we will get our hands on by testing CMIS with some typical vendors (EMC Documentum and Alfresco) let's point a spot light on the specification. The whole specification is available here: http://docs.oasis-open.org/cmis/CMIS/v1.0/os/cmis-spec-v1.0.pdf ,
The official definition is "The Content Management Interoperability Services (CMIS) standard defines a domain model and set of bindings that include Web Services and ReSTful AtomPub that can be used by applications to work with one or more Content Management repositories/systems."
The mentioned Domain model includes a Data Model. Some Meta data about the Repository (Capabilities, ...) is part of the Domain Model, too. There are four base types of objects:
  • Document Objects (entities)
  • Folder Objects (container)
  • Relation Objects (directional, optional)
  • Policy Objects (optional)
Additional sub-types may be defined in the repository as subtypes of these types. (BUT these object types must not extend or alter the behavior or semantic of the CMIS service. So there can exists constraints regarding the Object type underneeth CMIS.) Each object has indeed an object id (unique and constant). Every object has a set of named (not ordered) properties(, whereby the repository should return the properties always in a consistent order). A document may also have a content stream. In fact a document can have multiple content streams by using renditions.(CMIS allows to expose renditions BUT it provides no capability to create or update renditions those were accessed through the rendition services). Objects may have Access Controll Lists.

Properties are typed key value pairs. Whereby a property can be a single value or a multi-value one. There is no NULL value, instead a property is just not set. The following types are supported:
  • string
  • boolean
  • decimal
  • integer
  • datetime
  • uri
  • id (type of the object id)
  • html
Each property needs to have a non empty queryName attribute (simple string value).

Inheritance should work the following way:
  • A base type does not have a base type
  • Object type attributes (aspects not properties - fileable, queryable, ...) are not part of the inheritance
  • The property definitions are inheritad
  • The scope of a query on a given object-type is automatically expanded to include all descendat types
At least the following attributes are required for an OBJECT TYPE definition:
  • id:id
  • localName:string
  • localNameSpace:string
  • queryName:string
  • displayName:string
  • baseId:Enum (indicates the object type)
  • parentId:id
  • description:string
  • creatable:boolean
  • fileable: boolean (when it is a child of folder)
  • queryable: boolean (can occur in a FROM clause of a query statement)
  • controllablePolicy: boolean (can be controlled via policies)
  • fullTextIndexed:boolean
  • includedInSupertypeQuery: boolean
An object type definition may contain multiple PROPERTY DEFINITIONS:
  • id: id
  • localName: string
  • localNameSpace: string
  • queryName: string
  • displayName: string
  • description: string
  • propertyType: Enum
  • cardinality: Enum (single, multi)
  • updateability: Enum (readonly, readwrite, whencheckedout, oncreate)
  • inheritad: boolean
  • required: boolean
  • queryable: boolean (can occur in the WHERE clause)
  • orderable: boolean
  • choices: <ProperyChoiceType list> (allowed values)
  • openChoice: boolean (regarding the choices attribute)
  • defaultValue: <PropertyType>
Dependent of the type, you also have other attributes: minValue, maxValue (for numbers), resolution (for dates - Year, Date, Time), precision (for decimal), maxLength (for String).

Content has the following properties:
  • streamId: id
  • mimeType: String
  • lenght: integer (lenght in bytes)
Additionally a renditon has the following ones:
  • kind: string (cmis:thumbnail OR a repository specific kind)
  • title: string
  • height: integer (for images)
  • width: integer (for images)
  • renditionDocumentId: id (if the rendition should be also accessed as a document object)
Additionally the CMIS standard describes which properties have to be assigned to a which base object type. Here a short diagram:


CMIS supports also Access Control. An Access Control List is a list of Access Control Entries. An ACE holds the 'principalId' and one or more Strings with the names of permissions. An ACE additional has a attribute 'direct' which indicates if the ACE is directly assigned to the object or if it was somehow derived. The permissions are 'cmis:read','cmis:write' and 'cmis:all'. CMIS allows to check if a specifc action would be allowed even by not applying the action. It's possible to call methods like 'can{$Action identifier}' (E.G. canUpdateProperties).

CMIS supports versioning. This means Folders, Documents, Relationships and Policies can be versioned. Each version of an object is itself an object. A version series is a list (order is relevant) of versions of a object.  It's possible to get the latest version. CMIS makes not a semantical difference between Major and Minor versions in a version series. The repository is responsible to apply additional constraints regarding major versions. A new version of the document is created when a Check-in operation is performed. To be able to perform a Check-in it is required to prevously Check-out the object. A Check-out creates a Private Working Copy which then will be checked in later. After the Check-in operation the Private Working Copy has to disappear. CMIS supports to query for multiple versions. HOWEVER, the repository specific implementation has just to indicate if this is supported.

CMIS provides a typed based query service to discover objects by specific criterias. The query language is made of a subset of the SQL-92 grammar plus some extension (E.G. the INTREE function). You can imagine a Relational View on top of the Object oriented one. A virtual table is equivalent to a queryable object type. A virtual column is equivalent to a object property. The 'queryName' attribute is used to perform queries. There is just one virtual table for all object types those are part of one object hierarchy (Only true for user defined types, because instead there would be just one single table, right?). A virtual table does only provide access to the Meta Data and not the Content-Strems of an object.
  • SELECT [virtual columns] FROM [virtual table names] WHERE [conditions] ORDER BY [sort specification]
The specification contains the whole grammar as BNF rules. CMIS supports Joins BUT the repository's implementation can indicate if it is really supported.
The specification does in detail describe how the service interfaces are looking. So the following is just an overview:
  • RepositoryServices
    • getRepositories
    • getRepositoryInfo
    • getTypeChildren
    • getTypeDescendants
    • getTypeDefinition
  • Navigation Services
    • getChildren
    • getDescendants
    • getFolderTree
    • getFolderParent
    • getObjectParents
    • getCheckedOutDocs
  • Object services
    • createDocument
    • createDocumentsFromSource
    • createFolder
    • createRelationship
    • createPolicy
    • getAllowableActions
    • getProperties
    • getObjectByPath
    • getContentStream
    • getRenditions
    • updateProperties
    • moveObject
    • deleteObject
    • deleteTree
    • setContentStream
    • deleteContentStream
  • Multi-filing service
    • addObjectToFolder
    • removeObjectFromFolder
  • Discovery services
    • query
    • getContentChanges
  • Versioning services
    • checkOut
    • cancelCheckOut
    • checkIn
    • getObjectOfLatestVersion
    • getPropertiesOfLatestVersion
    • getAllVersions
  • Relationship services
    • getObjectRelationships
  • Policy Services
    • applyPolicies
    • removePolicy
    • getAppliedPolicies
  • ACL services
    • getACL
    • applyACL
There are two ways how you can expose a CMIS Web Service. The first one is using RESTFul access, which means it is a resource based access via HTTP. The usual HTTP operations are used (GET, POST, DELETE) In this case the resources must have a specific format. (protocol), The resources format is based on the AtomPub one. An entry could look as the following:

 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<atom:entry xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200908/"
xmlns:cmism="http://docs.oasis-open.org/ns/cmis/messaging/200908/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:app="http://www.w3.org/2007/app"
xmlns:cmisra="http://docs.oasis-4479 open.org/ns/cmis/restatom/200908/">
<atom:author>
  <atom:name>Al Brown</atom:name>
  </atom:author>
  <atom:id>urn:uuid:efe0542e-8933-4b3e-93f2-4d1caa3fc2d9</atom:id>
  <atom:title type="text">CMIS Example Document</atom:title>
  <atom:updated>2010-01-25T10:20:58.318-08:00</atom:updated>
  <atom:content type="text">some text</atom:content>
  <cmisra:object> 4488 <cmis:properties>
    <cmis:propertyId localName="rep-cmis:objectTypeId"
    propertyDefinitionId="cmis:objectTypeId">
      <cmis:value>invoice</cmis:value> 4492 </cmis:propertyId>
    <cmis:propertyString localName="rep-cmis:name"
    propertyDefinitionId="cmis:name">
      <cmis:value>CMIS Example Document</cmis:value>
    </cmis:propertyString>
    </cmis:properties>
  </cmisra:object>
</atom:entry>

The specification explains the CMIS AtomPub protocol in detail.

The other way is to use a SOAP-based Web Service. This is a message based approach, whereby the SOAP protocol is used to exchange XML messages. In detail such a Web Service is follwing the guidelines of the W(eb) S(ervice) I(nteroperability) Organization.

Intermediate summary
As we can see CMIS is not suitable to cover everything which you would like to to with your Content Repository. Features like for instance 'Create a Policy' could not be covered by a standard which aims to be System independent. In the specification, there are a lot MAY-s (may or may not) which means that two different CMIS implementations may even not have the same set of features available. The specification then allows to just set the related 'featureSupported' flag to 'false'. The following table may give you an idea what I am meaning:


 MAY SHOULD   MUST
 More than one Repo x

 Further Implementaion specific Meta Data x   

 Additional object types x

 Content streams x

 Renditions x

 ACL-s x

 Multi value properties x

 Referential constraints x

 Policies x

 Relationships x

 Value Constraints x


 ...           

 Permanent and unique id-s
 x
 Consistent Property order
 x
 Meaningful error messages
 x
 Property inheritance
 x
 Normalized String identifier (E.G. camel case)
 x
Preserved order of multi values
 x


 ...
 Get a list of repositories via service method

 x
 Unique Repository id

 x
 Provide information for each object type def. if Contentstreams are supported or required

 x
Empty set as multiple value set disallowed

 x
 queryName Property

 x
 folder and document base types

 x



 ...

Hands on
Alfresco is an Open Source ECM-System (http://www.alfresco,com). So let's try out Alfresco's CMIS implementation. Alfresco itself states that it has a full implementation of the specification (whatever that means regarding the fact that even a not fully featured Repository could be 100% compliant), So Alfresco 3.3 or higher is required.

(BTW: Alresco mentions also an Repository to Repository use case: http://wiki.alfresco.com/wiki/CMIS)

There are some Alfresco specific things. A custom Aspect in Alfresco is handled like a Policy which causes that they can not be created via CMIS. There are further examples as well, but you could say that Alfresco was just extending the standard by still beeing compliant. It seems that Alfresco uses the "OpenCMIS" framework from Apache. However, Jeff Potts from Alfresco wrote a good tutorial about CMIS and Alfresco, so this blog article will just follow parts of his tutorial.

The tutorial covers:
  • How to authenticate against the repository (not CMIS specific)
  • Getting basic repository information
  • Create a folder and content via CMIS
To use CMIS with Alfresco an HTTP request to E.G. http://localhost:8080/alfresco/s/cmis can be done. On Linux (or Cygwin) just use the following command to perform such a call:

 curl -uadmin:admin http://localhost:8080/alfresco/s/cmis

What you get is a lot of XML back. Actually what you are getting back here is the result of the 'getRepositoryInfo' method. This entry point explains where to find the specific resources to interact with.

There are collection elements those are providing a set URL-s those are buildung the RESTFul service.

...
<collection
href="http://localhost:8080/alfresco/s/cmis/s/workspace:SpacesStore/i/4
eb6a431-3c56-4767-816a-4ceca2295ae2/children">
<atom:title>root collection</atom:title>
<cmisra:collectionType>root</cmisra:collectionType>
</collection>
...
<collection href="http://localhost:8080/alfresco/s/cmis/types">
<atom:title>type collection</atom:title>
<cmisra:collectionType>types</cmisra:collectionType>
</collection>
...

The capabilities element tells you which parts of the CMIS specifications are implemented:

...
<cmis:capabilityAllVersionsSearchable>false</cmis:capabilityAllVersions
Searchable>
<cmis:capabilityChanges>none</cmis:capabilityChanges>
<cmis:capabilityContentStreamUpdatability>anytime</cmis:capabilityConte
ntStreamUpdatability>
...

The root collection above gives information about the root directory of your repository. So if you take the url of it then you will get the entries those are corresponding to objects inside the root folder of the repository.

The type collection above gives information about the types those are available inside the repositoy. You can also copy the URL of it to a CURL call to get the result back.

To create a new folder you need to create an xml file and to post it to the right URL:

 <?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://www.w3.org/2005/Atom"
xmlns:cmisra="http://docs.oasis-open.org/ns/cmis/restatom/200908/"
xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200908/">
<title>Someco</title>
<cmisra:object>
<cmis:properties>
<cmis:propertyId
propertyDefinitionId="cmis:objectTypeId"><cmis:value>cmis:folder</cmis:
value></cmis:propertyId>
</cmis:properties>
</cmisra:object>
</entry>

Here like such a POST could look via CURL:

 curl -X POST -uadmin:admin
"http://localhost:8080/alfresco/s/cmis/p/children" -H "Content-Type:
application/atom+xml" -d @/${path}/testCreateSomecoFolder.atom.xml

This creates a folder named Someco inside the root folder of the repository,

You also can upload content by using a POST statement. The content has to be part of the xml file, whereby it must be Base64 encoded:

 openssl base64 -in ./sample-a.doc -out ./sample-a.doc.base64

At this point we stop following the tutorial by requesting an easier way to access the repository via CMIS. Indeed there is one, all we need is a CMIS client. So the first step is to load the client libraries from the OpenCMIS web site. Afterwards you can create a Java Project (E.G. by using Eclipse) which references the client libraries and it's dependencies. So let's do some examples:
  • Connect to the repository
  • Create a folder
  • Create a document
  • Get an object and it's properties
The following code snippet shows how to connect by getting the list of repositories:

 package de.ecmgeek.opencmis;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


import org.apache.chemistry.opencmis.client.api.Repository;
import org.apache.chemistry.opencmis.client.api.SessionFactory;
import org.apache.chemistry.opencmis.client.runtime.SessionFactoryImpl;
import org.apache.chemistry.opencmis.commons.SessionParameter;
import org.apache.chemistry.opencmis.commons.enums.BindingType;

public class ConnectionExample {

  
    public static void main(String[] args) {
      
        SessionFactory sessionFactory = SessionFactoryImpl.newInstance();
      
        Map<String, String> params = new HashMap<String, String>();
        params.put(SessionParameter.USER, "admin");
        params.put(SessionParameter.PASSWORD, "admin");
        params.put(SessionParameter.ATOMPUB_URL, "http://192.168.190.129:8080/alfresco/s/cmis");
        params.put(SessionParameter.BINDING_TYPE, BindingType.ATOMPUB.value());
      
        List<Repository> repositories = new ArrayList<Repository>();
        repositories = sessionFactory.getRepositories(params);
      
        for (Repository r : repositories)
        {
            System.out.println("Id: " + r.getId() + ";Name: " + r.getName());
        }
       
        Session session = repositories.get(0).createSession();
    }
  
  
}

Let's just assume that we have a helper method that can return us the params. The the following code can be used to get the root folder:

 package de.ecmgeek.opencmis;

import java.util.Map;

import org.apache.chemistry.opencmis.client.api.CmisObject;
import org.apache.chemistry.opencmis.client.api.Folder;
import org.apache.chemistry.opencmis.client.api.Repository;
import org.apache.chemistry.opencmis.client.api.Session;
import org.apache.chemistry.opencmis.client.api.SessionFactory;
import org.apache.chemistry.opencmis.client.runtime.SessionFactoryImpl;

public class RootFolderExample {

    /**
     * @param args
     */
    public static void main(String[] args) {
      
      
         SessionFactory sessionFactory = SessionFactoryImpl.newInstance();
         Map<String, String> params = Helper.createParams("admin", "admin", "http://192.168.190.129:8080/alfresco/s/cmis");
        
         //By assuming that only one Repository is existent
         Repository repo = sessionFactory.getRepositories(params).get(0);
         Session session = repo.createSession();
        
         Folder root = session.getRootFolder();
      
         System.out.println("+-" + root.getName());
        
         for (CmisObject child : root.getChildren())
         {
             System.out.println("+--" + child.getName());
         }
    }

}

This prints out the following in my case:

+-Firmen-Home
+--Sites
+--Datenverzeichnis
+--Besucher-Home
+--Benutzer-Homes
+--Test

Localization problem?: I tried to use the parameter 'params.put(SessionParameter.LOCALE_ISO639_LANGUAGE, "en");' to get the english name of the document, but this seems not to work. So the primary name of the document in Alfresco seems to be German because my JVM was configured to German as started the Alfresco the first time (Before the first startup you can set the following java arguments to have the names by default set to english:Duser.country=US -Duser.language=en ). Alfresco then supports multilingual titles.

Now let's try to get some Alfresco specific properties. It turns out that this is not simple as it could be. A folder can have a 'cm:title' attribute in Alfresco. Surprisingly there is no such a property in the property list of the above mentioned root folder. So what's wrong here? The answer seems to be the following:
  • OpenCMIS parses the XML by taking the 'cmis:properties' tag into account. For Alfresco this XML node has also a subnode which is named 'alf:aspects'. The 'cm:title' property is part of an aspect. CMIS does not support the Alfresco aspects.
There is an extension library available 'http://code.google.com/a/apache-extras.org/p/alfresco-opencmis-extension/'. So you can set the 'OBJECT_FACTORY_CLASS' parameter to use the specific Alfresco object factory.

parameter.put(SessionParameter.OBJECT_FACTORY_CLASS, "org.alfresco.cmis.client.impl.AlfrescoObjectFactoryImpl");

However, you can imagine that you just can't do that for multiple repositories. Here is the code:

 package de.ecmgeek.opencmis;

import java.util.Map;

import org.alfresco.cmis.client.AlfrescoFolder;
import org.apache.chemistry.opencmis.client.api.CmisObject;
import org.apache.chemistry.opencmis.client.api.Folder;
import org.apache.chemistry.opencmis.client.api.Repository;
import org.apache.chemistry.opencmis.client.api.Session;
import org.apache.chemistry.opencmis.client.api.SessionFactory;
import org.apache.chemistry.opencmis.client.runtime.SessionFactoryImpl;

public class AlfrescoRootFolderExample {

    /**
     * @param args
     */
    public static void main(String[] args) {
      
      
         SessionFactory sessionFactory = SessionFactoryImpl.newInstance();
         Map<String, String> params = Helper.createParams("admin", "admin", "http://192.168.190.129:8080/alfresco/s/cmis");
        
         //By assuming that only one Repository is existent
         Repository repo = sessionFactory.getRepositories(params).get(0);
         Session session = repo.createSession();
        
         //Casting is not required at this point, just for demo purposes
         AlfrescoFolder root = (AlfrescoFolder) session.getRootFolder();
        
         System.out.println("+-" + root.getPropertyValue("cm:title"));
        
         for (CmisObject child : root.getChildren())
         {
             System.out.println("+--" + child.getPropertyValue("cm:title"));
            
         }
    }

}

The result is:

 +-Firmen-Home
+--Sites
+--Datenverzeichnis
+--Besucher-Home
+--Benutzer-Homes
+--

The last entry is empty because the Folder 'Test' got not assined a title.

So let's continue the tutorial by creating a folder and a document!

 package de.ecmgeek.opencmis;

import java.util.HashMap;
import java.util.Map;

import org.apache.chemistry.opencmis.client.api.Document;
import org.apache.chemistry.opencmis.client.api.Folder;
import org.apache.chemistry.opencmis.client.api.Repository;
import org.apache.chemistry.opencmis.client.api.Session;
import org.apache.chemistry.opencmis.client.api.SessionFactory;
import org.apache.chemistry.opencmis.client.runtime.SessionFactoryImpl;
import org.apache.chemistry.opencmis.commons.enums.VersioningState;

public class CreateFolderAndDocExample {

    /**
     * @param args
     */
    public static void main(String[] args) {
      
      
         SessionFactory sessionFactory = SessionFactoryImpl.newInstance();
         Map<String, String> params = Helper.createParams("admin", "admin", "http://192.168.190.129:8080/alfresco/s/cmis");
        
         //By assuming that only one Repository is existent
         Repository repo = sessionFactory.getRepositories(params).get(0);
         Session session = repo.createSession();
        
         Folder root = session.getRootFolder();
        
         //The new folder's properties
         Map<String,String> newFolderProps = new HashMap<String, String>();
         newFolderProps.put("cmis:objectTypeId", "cmis:folder");
         newFolderProps.put("cmis:name", "Test Folder 1");
        
         Folder newFolder = root.createFolder(newFolderProps);
        
         System.out.println("Created folder with name: " + newFolder.getName());
        
         //The new document' properties
         Map<String,String> newDocProps = new HashMap<String, String>();
         newDocProps.put("cmis:objectTypeId", "cmis:document");
         newDocProps.put("cmis:name", "Test Doc 1");
      
         Document newDoc = newFolder.createDocument(newDocProps, null, VersioningState.NONE);
        
         System.out.println("Created document with name: " + newDoc.getName());
    }

}

The last part of the tutorial shows how to basically use the CMIS Query Language. So let's at first assume that you already created a new document type inside your Alfresco repository. I named it 'ecg:document'. Then you can query all documents of this specific type by using the following source code:

 package de.ecmgeek.opencmis;

import java.util.Map;

import org.apache.chemistry.opencmis.client.api.QueryResult;
import org.apache.chemistry.opencmis.client.api.Repository;
import org.apache.chemistry.opencmis.client.api.Session;
import org.apache.chemistry.opencmis.client.api.SessionFactory;
import org.apache.chemistry.opencmis.client.runtime.SessionFactoryImpl;

public class QueryExample {

    public static void main(String[] args) {
      
         SessionFactory sessionFactory = SessionFactoryImpl.newInstance();
         Map<String, String> params = Helper.createParams("admin", "admin", "http://192.168.190.129:8080/alfresco/s/cmis");
         Repository repo = sessionFactory.getRepositories(params).get(0);
         Session session = repo.createSession();
        
         String query = "SELECT * FROM ecg:document";
      
         for (QueryResult r : session.query(query, false))
         {
             System.out.println(r.getPropertyValueById("cmis:name"));
         }
        
    }   
}

BTW: Alfresco has built in types like 'cm:content'. It's not possible to query directly for 'cm:content' because it is mapped to 'cmis:document'.

Outlook
This was only a small tutorial, and so it did not cover all facets of CMIS. However, I hope it helped to understand it a bit. A furher subject could be the Query Language. It seems that it provides enough stuff for another blog entry ;-).