Wednesday, May 26, 2010

Alfresco: Developing the Content Model

In a traditional system where you start building from scratch, you can generally break up a system into individual components once you have a reasonable idea of the way they are going to fit together, and then proceed with the development of each component in relative isolation. With a packaged system which you are trying to extend for your own purposes, I find that the development cycle is slightly different - you first have to figure out how the system works, and fit your application around it so you don't paint yourself into a corner.

In order to quickly get through the "learn how the system works" stage, I recommend reading both Munwar Sharif's Alfresco Enterprise Content Management Implementation and Jeff Pott's Alfresco Developer's Guide, preferably in that order (although I read them in reverse order). I found both books enormously helpful and informative. Of course, there is still no guarantee that I won't paint myself into a corner, but hopefully the chances are less :-).

The Content Model

Alfresco is at its core a Document Management (DM) System, its main storage unit is a file - you upload a file and either enter the metadata or build/use extractors that extract metadata from it. Contrast this with something like Drupal, which is really a Web Content Management System (WCM) - it provides forms to enter both content and metadata. Depending on your point of view, this could be an advantage or a disadvantage.

My projected use case is for a WCM, and I needed content objects to store user-entered metadata for blog posts, so I had to build my own model. Here is what I came up with.

XML Definition Files

The custom content model is defined using Spring configuration and Alfresco's own content model definition XML. First we create a Spring configuration file in the config/alfresco/extension directory, called mycompany-model-context.xml. When deployed, the contents of the config/alfresco/extension directory ends up under WEB-INF/classes/alfresco/extension in the alfresco webapp, where it knows to look for files with the pattern *-context.xml.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<!-- Source: config/alfresco/extension/mycompany-model-context.xml -->
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 
    'http://www.springframework.org/dtd/spring-beans.dtd'>
<beans>
  <bean id="mycompany.dictionaryBootstrap" 
      parent="dictionaryModelBootstrap" 
      depends-on="dictionaryBootstrap">
    <property name="models">
      <list>
        <value>alfresco/extension/model/myModel.xml</value>
      </list>
    </property>
  </bean>
</beans>

As you can see, we define a model file in here for the "models" property. This model file contains our custom model. This is basically the diagram above written out in XML.

There is a lot to explain here. However, this seems to be one of the first things people try to do with Alfresco, so there are lots of blog posts and wiki pages that do so already. I will point out some things, but for the rest, you may want to take a look at the Alfresco Data Dictionary Guide referenced below.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
<?xml version="1.0" encoding="UTF-8"?>
<!-- Source: config/alfresco/extension/model/myModel.xml -->
<model name="my:mymodel" xmlns="http://www.alfresco.org/model/dictionary/1.0">

  <description>My Model</description>
  <author>Sujit Pal</author>
  <version>1.0</version>

  <!-- import base 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>

  <namespaces>
    <namespace uri="http://www.mycompany.com/model/content/1.0" prefix="my"/>
  </namespaces>

  <constraints>
    <constraint name="my:pubStates" type="LIST">
      <parameter name="allowedValues">
        <list>
          <value>Draft</value>
          <value>Review</value>
          <value>Published</value>
        </list>
      </parameter>
    </constraint>
  </constraints>

  <types>
    <!-- ==================================================== -->
    <!-- Represents the base document for this application    -->
    <!-- ==================================================== -->
    <type name="my:baseDoc">
      <title>Base Document</title>
      <description>Abstract Base Document for this application</description>
      <parent>cm:content</parent>
      <mandatory-aspects>
        <aspect>cm:ownable</aspect>
      </mandatory-aspects>
    </type>
    <!-- ==================================================== -->
    <!-- Represents a blog written by a user (represented by  -->
    <!-- a my:profile content). This has a 1:1 mapping to a   -->
    <!-- profile, ie, a blog must have an associated profile  -->
    <!-- It has a 0:n mapping to posts, ie, a blog can have 0 -->
    <!-- to n posts associated with it. Fields for it are     -->
    <!-- title, description and creation date.                --> 
    <!-- ==================================================== -->
    <type name="my:blog">
      <title>Blog Information</title>
      <description>Blog Level Information</description>
      <parent>my:baseDoc</parent>
      <properties>
        <property name="my:blogname">
          <type>d:text</type>
        </property>
        <property name="my:byline">
          <type>d:text</type>
        </property>
        <property name="my:user">
          <type>d:noderef</type>
        </property>
      </properties>
      <associations>
        <child-association name="my:posts">
          <title>Posts</title>
          <target>
            <class>my:post</class>
            <mandatory>false</mandatory>
            <many>true</many>
          </target>
        </child-association>
      </associations>
    </type>
    <!-- ==================================================== -->
    <!-- Represents a single blog post.                       -->
    <!-- ==================================================== -->
    <type name="my:post">
      <title>Blog Post</title>
      <description>Single Blog Post</description>
      <parent>my:baseDoc</parent>
      <properties>
        <property name="my:blogRef">
          <type>d:noderef</type>
        </property>
      </properties>
      <mandatory-aspects>
        <aspect>cm:titled</aspect>
        <aspect>my:tagClassifiable</aspect>
        <aspect>my:publishable</aspect>
      </mandatory-aspects>
    </type>

  </types>

  <aspects>

    <aspect name="my:publishable">
      <title>Publishable</title>
      <properties>
        <property name="my:pubState">
          <type>d:text</type>
          <multiple>true</multiple>
          <constraints>
            <constraint ref="my:pubStates"/>
          </constraints>
        </property>
        <property name="my:pubDttm">
          <type>d:datetime</type>
          <index enabled="true">
            <atomic>true</atomic>
            <stored>true</stored>
            <tokenised>both</tokenised>
          </index>
        </property>
        <property name="my:unpubDttm">
          <type>d:datetime</type>
          <index enabled="true">
            <atomic>true</atomic>
            <stored>true</stored>
            <tokenised>both</tokenised>
          </index>
        </property>
        <property name="my:furl">
          <type>d:text</type>
        </property>
      </properties>
    </aspect>

    <aspect name="my:tagClassifiable">
      <title>Category Tag</title>
      <parent>cm:classifiable</parent>
      <properties>
        <property name="my:tags">
          <title>Tags</title>
          <type>d:category</type>
          <mandatory>false</mandatory>
          <multiple>true</multiple>
          <index enabled="true">
            <atomic>true</atomic>
            <stored>true</stored>
            <tokenised>false</tokenised>
          </index>
        </property>
      </properties>
    </aspect>
  </aspects>

</model>

In my model, the central content type is my:post. It is publishable, so inherits from my:publishableDoc, which contains the properties necessary for specifying its current workflow state. It has a n:1 relation with my:blog, which is a container for a set of my:post objects. A my:post can also have a set of tags, which is modeled as an aspect (I copied this from the Classification and Categories page referenced below).

To test the model, you will need to run "ant deploy" and restart Tomcat. The "ant deploy" basically zips up the project and unzips it into the exploded Alfresco war file on Tomcat. Jeff Potts has made the code for his book available for direct download, which contains the build.xml, if you want it.

In addition, there is a quicker way (suggested by the Data Dictionary Guide wiki page), which just calls a Java class in the Alfresco JAR files. The Ant snippet is shown below. You still need to deploy and restart Tomcat once you are happy with your model, but this is good for iterative testing.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  <target name="test-model" depends="setup" description="check model">
    <java dir="." fork="true" 
        classname="org.alfresco.repo.dictionary.TestModel">
      <classpath refid="classpath.server"/>
      <classpath refid="classpath.build"/>
      <classpath path="${alfresco.web.dir}/WEB-INF/classes"/>
      <classpath path="config"/>
      <arg line="alfresco/extension/model/myModel.xml"/>
    </java>
  </target>

Verfying the changes

You are not done yet, though... At this point, you will still not actually "see" your custom model on the Alfresco UI. For that, you will need to set up the property sheets for each of the content types. This is done in yet another XML file in the config/alfresco/extension directory called web-client-config-custom.xml (this file name is hardcoded in the Alfresco's config, so it looks for overrides here). Here is the file, without too much explanation - basically you are telling the UI to show certain fields for certain content types.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<alfresco-config>
<!-- Source: config/alfresco/extension/web-client-config-custom.xml -->

  <config evaluator="aspect-name" condition="my:tagClassifiable">
    <property-sheet>
      <show-property name="my:tags" display-label-id="tags"/>
    </property-sheet>
  </config>

  <config evaluator="node-type" condition="my:blog">
    <property-sheet>
      <show-property name="my:blogname" display-label-id="blogname"/>
      <show-property name="my:byline" display-label-id="byline"/>
      <show-property name="my:user" display-label-id="user"/>
      <show-child-association name="my:posts"/>
    </property-sheet>
  </config>

  <config evaluator="node-type" condition="my:post">
    <property-sheet>
      <show-property name="my:pubState" display-label-id="pubState"/>
      <show-property name="my:pubDttm" display-label-id="pubDttm"/>
      <show-property name="my:unpubDttm" display-label-id="unpubDttm"/>
      <show-property name="my:furl" display-label-id="furl" read-only="true"/>
    </property-sheet>
  </config>

  <config evaluator="string-compare" condition="Content Wizards">
    <content-types>
      <type name="my:blog" display-label-id="blog"/>
      <type name="my:post" display-label-id="post"/>
    </content-types>
  </config>

  <config evaluator="string-compare" condition="Action Wizards">
    <aspects>
      <aspect name="my:tagClassifiable"/>
      <aspect name="my:publishable"/>
    </aspects>
    <subtypes>
      <type name="my:baseDoc"/>
      <type name="my:blog"/>
      <type name="my:post"/>
    </subtypes>
    <specialize-types>
      <type name="my:post"/>
    </specialize-types>
  </config>

</alfresco-config>

There is also an associated property file which puts human readable names for the display-label-id attributes in the file above, that looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# Source: config/alfresco/extension/webclient.properties

#my:tagClassifiable
tags=Category Tags

#my:blog
blogname=Blog Name
byline=Byline
user=User Information

#my:post
pubState=Current Workflow State
pubDttm=Scheduled Publish Date
unpubDttm=Scheduled Unpublish Date
furl=Consumer Friendly URL

# content wizard
blog=Blog
post=Post

Once you run "ant deploy" and restart Tomcat, you will see your custom content if you try to Add Content. On the second page, it prompts for Type, and I can ee Blog and Post in the dropdown. Here is the screenshot for this page, although I could not grab a screenshot with the dropdown opened.

Useful Links

Here are some Wiki Entries I found useful, along with the two books mentioned above.

Conclusion

Compared to Drupal, custom content model creation in Alfresco is a lot harder. Some of it probably has to do with the fact that Alfresco is written in Java, and servlet containers need to be restarted for changes to take effect, unlike LAMP systems. Also the use of XML for configuration more common in Java..

Alfresco's model, however, is better when it comes to deployment of the same model across multiple environments, a fairly typical scenario in most Java (and probably PHP/Drupal) shops. Because configuration is just a set of files, they can be developed once and applied to multiple environments.

Update - 2010-06-12

When uploading the blog and post data from my Atom dumps, I discovered that I had made several goof-ups when designing the content model. Specifically, these were:

  • Using d:path for friendly URL in my:post - The d:path value contains the Alfresco specific unique URI, not a web-friendly URL I was looking for. So I changed the name to furl and type to d:text.
  • 1:n association from my:blog to my:post - Not totally sure about this, but I seem to have made a mistake in configuring this. So I followed the example on this wiki page to define the 1:n relation by removing the source declaration. The fix works (in the sense that Alfresco does not complain), but I am still unable to use nodeService.createAssociation() to link a my:post to a my:blog. Need to investigate this further.

I have updated all the relevant bits of XML and properties files in this post for the above.

Update - 2010-06-25

I wanted to extend my model with some more types, and realized that having my:post extend my:publishableDoc was a bit restrictive, so decided to make an aspect my:publishable instead, which I then applied to my:post. I have updated the model XML file above.

One thing I noticed is that aspect properties are not indexable by default. I guess its because an aspect is like an interface, so meant to be applied as a marker during search. So in case you need you want to search on an aspect property as well, you will need to make it indexable.

6 comments (moderated to prevent spam):

Steve Greenley said...

That's a great article, thank you. Some readers might also find the following article interesting. It shows how to deploy a content model as an Alfresco AMP file. Link. Thanks again.

Sujit Pal said...

Thanks Steve, and thanks for the link. At some point I would like to take all my customizations and package it into an AMP which can then be applied to a stock Alfresco installation, so this link is quite useful for me - thanks for figuring this stuff out and writing this post.

franzformator said...

That is a great tutorial. But I have tried to use this on Alfresco 4 with share.
It is possible to set the child associations but there is no delete propagation in case of deleting the parent node.
Do you have an example for the share-config-custom.xml?

Sujit Pal said...

Thanks franzformator. I haven't used share at all. I did this work as a competing POC against the initial Drupal POC for CMS selection at my company, because I felt Alfresco is better for our use case. However, by that time, Drupal had been adopted and there was a lot of momentum already, so we opted to go with Drupal. I don't do much work in the CMS space nowadays, but in hindsight, Drupal appears to have worked out fine.

Anonymous said...

This is a great article. I am a newbie to Alfresco and trying to create a POC. I saved all the files as you mentioned in the extension directory. Now when I try to login to share my credentials are no longer working. Am I missing any setting. I get this error "The remote server may be unavailable or your authentication details have not been recognized.". But when I remove the extension directory everything works perfectly.

Sujit Pal said...

Hi Anonymous, its been a while since I wrote this post, and I no longer work with the Alfresco CMS, so I am just guessing here. I suspect that your modifications may have override the cm:person class somehow which caused Alfresco's authentication system to get messed up, hence the error.