Print Header

Related Topics

Related Case Studies

Contact Blue Fish

Blue Fish Development Group
701 Brazos St. #700
Austin, TX 78701
(512) 469-9300

Painless Alfresco Development: Improving Supportability through Source Code Organization

Author Photo

February 3, 2010 - Article by Joshua Toub

Robust software begins with solid development practices, and few are as fundamental as having a well laid out source code organizational structure. Unfortunately, when building solutions with Alfresco, many people simply follow the out-of-the-box product’s source structure, inadvertently casting aside source code organization best practices. This article shows you how you can enhance the supportability and maintainability of your Alfresco solutions by using Spring configuration techniques to enable Alfresco to find your source in the source tree of your choosing.

Introduction

Note: This article assumes that you already know the ins and outs of basic Spring configuration and that you know how to build basic Alfresco extensions. If you need an introduction to these concepts, I recommend you start with the Alfresco development wiki .

Alfresco is great. It’s a rock solid content management and collaboration platform with an exciting roadmap. Although it’s only a handful of years old, it can go toe to toe with the legacy players in the field and often come out ahead. It has an immensely flexible architecture that makes it pretty much as easy as possible to extend and adapt the base Alfresco offerings to meet specific requirements. I love having access to the source. I love that you can “try before you buy.” I love Alfresco’s enormous community that does everything from supporting end users to evolving core products. I love that Alfresco is a disruptive player poised to drastically alter the landscape of the otherwise stodgy world of Enterprise Content Management.

Ok, so what don’t I love about Alfresco? Well, the commonly practiced methods for extending Alfresco sometimes force me to violate certain development best practices, like encapsulating and isolating custom source code. For many types of extensions, out-of-the-box Alfresco requires you to distribute source code across many different directories, and those directories are typically nested inside of stock Alfresco directories. For example, a simple custom dashlet might include the following files:

Fig. 1: Typical source code organization for an Alfresco Share extension.

Figure 1: Conventional source code organization for an Alfresco Share extension.

And these files would get deployed alongside stock Alfresco source files within the web application like so:

Fig. 2: Conventionally organized Alfresco Share extension files are deployed alongside out-of-the-box files, making it difficult to easily identify what is custom and what is stock.

Figure 2: Conventionally organized Alfresco Share extension files are deployed alongside out-of-the-box files, making it difficult to easily identify what is custom and what is stock.

As you can see, the extension source files live underneath the alfresco directory right alongside stock Alfresco files and potentially files from every other extension that might be deployed. This makes it difficult to quickly identify which files make up the extension. From a development best practices perspective, it is simply bad hygiene to pollute Alfresco’s directories with dozens of custom files and directories.

Thankfully, Alfresco’s flexible Spring-based configuration architecture makes it easy to structure an Alfresco development project in such a way that cleanly separates custom files from everything else. Before we get into the details of these Spring configuration mechanics, let’s take a look at what I’d like my source tree to look like.

Preferred Source Code Organization

There are many ways you can organize your source such that it is isolated from the out-of-the-box distribution. To make my source trees feel familiar to seasoned Alfresco developers, I like to create source structures that closely mirror the out-of-the-box Alfresco and Share web applications. Revisiting the source for our initial extension example, I would structure it like so:

Fig. 3: My preferred source code organizational structure for Alfresco extensions.

Figure 3: My preferred source code organizational structure for Alfresco extensions.

I find that isolating my files like this makes it easier to debug, package, and distribute my extensions because they readily stand out from the stock Alfresco files. It also tends to make it easier for other developers to understand how my extensions work because they can quickly identify all of the files that comprise each extension. Here’s what my deployed dashlet source looks like with this organizational structure:

Fig. 4: With my preferred source code organizational structure, all of the custom files get deployed under a single folder.

Figure 4: With my preferred source code organizational structure, all of the custom files get deployed under a single folder.

Look at that—all of my source files are grouped under a common folder!

A Tale of Two Web Apps

You may have noticed that I begin my source tree with a folder named share. This folder maps directly to the Share web application deployed by Alfresco. Similarly, if my extensions required that files be deployed to the Alfresco web application, I would organize those files under a root folder called alfresco.

If you’re wondering why an extension might require that files be deployed to multiple web applications, just remember how Alfresco is architected: the Alfresco web application hosts the core repository, all of the webscripts that provide access to the core repository, and the Java Server Faces-based Alfresco Explorer web client; and the Share web application hosts the Spring Surf-based Alfresco Share user interface and its supporting webscripts.

If you take a look at your application server’s webapp directory after installing Alfresco, you should see something like this:

Fig. 5: Alfresco is typically deployed as two (or more) web applications. In this example, "alfresco" hosts the Alfresco repository, and "share" hosts the Share UI.

Figure 5: Alfresco is typically deployed as two (or more) web applications. In this example, “alfresco” hosts the Alfresco repository, and “share” hosts the Share UI.

The Share web application interacts with the Alfresco repository by calling the webscripts published by the Alfresco web application. It is important to remember this because your Alfresco customizations may reside in one web app, the other, or both.

Resolving Custom Resources

In order for Alfresco to be able to find the files in my preferred folder structure, we need to tell Alfresco where to look. Resources in Alfresco—be they XML configuration files, Freemarker templates, webscript implementations, and others—are resolved using Spring’s configuration framework. Furthermore, Alfresco knows to look for a handful of specific custom Spring configuration files to pick up custom values that override default values defined in Alfresco’s default Spring configuration. This means that the particular methods for how Alfresco resolves resources can be easily changed simply by adding a few <bean> entries to one of these custom Spring configuration files. The specific files that need to be edited are:

  • /WEB-INF/classes/alfresco/web-extension/custom-slingshot-application-context.xml (for the Share webapp)
  • /WEB-INF/classes/alfresco/extension/custom-web-context.xml (for the Alfresco webapp)

You should be aware that these files may not yet exist in your Alfresco implementation, particularly if you have not built any extensions yet. If necessary, go ahead and create them with the following empty Spring bean configuration:

            
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'>
<!-- Top-level imports of XML configuration -->
<beans>

</beans>

        

Now comes the tough part—finding out which Spring beans need to be overridden to pick up your particular resources. Thankfully, Alfresco follows a nice naming convention for their Spring beans and has named the various search paths with names containing the string “searchpath.” This makes it easy to find all of the default search path configurations—just use your favorite IDE to search for the string id='*searchpath' across all xml files in the Alfresco source tree. In the current 3.2 Community source, this yields 14 distinct matches for the Share webapp:

  • webframework.searchpath
  • webframework.templates.searchpath
  • webframework.searchpath.chrome
  • webframework.searchpath.component
  • webframework.searchpath.componenttype
  • webframework.searchpath.configuration
  • webframework.searchpath.contentassociation
  • webframework.searchpath.page
  • webframework.searchpath.pageassociation
  • webframework.searchpath.pagetype
  • webframework.searchpath.templateinstance
  • webframework.searchpath.templatetype
  • webframework.searchpath.theme
  • webframework.presets.searchpath

And one match for the Alfresco webapp:

  • webscripts.searchpath

To override one of these search paths, start by copying and pasting the appropriate bean definition into your custom configuration (custom-slingshot-application-context.xml for the Share webapp or custom-web-context.xml for the Alfresco webapp). For example, let’s assume that you have created a new Share component whose source files you would like to keep separate from the out-of-the-box Share source. You would copy this from slingshot-application-context.xml into custom-slingshot-application-context.xml:

            
   <bean id="webframework.searchpath.component" class="org.alfresco.web.scripts.SearchPath">
      <property name="searchPath">
         <list>
         	<ref bean="webframework.classpathstore.component.custom" />
            <ref bean="webframework.classpathstore.component" />
         </list>
      </property>
   </bean>

        

Next, create a new bean that knows how to find your resources. Most of the time, you will be want to simply extend Alfresco’s webframework.classpathstore bean so that it looks in specific paths within the classpath. Continuing the above example, let’s create a bean that looks for component definitions in bluefishgroup/share-ext/site-data/components:

            
	<bean id="bluefishgroup.components" parent="webframework.classpathstore">
		<property name="mustExist">
			<value>true</value>
		</property>
		<property name="classPath">
			<value>bluefishgroup/site-data/components</value>
		</property>
	</bean>

        

Finally, update the bean definition you copied to reference your new bean:

            
   <bean id="webframework.searchpath.component" class="org.alfresco.web.scripts.SearchPath">
      <property name="searchPath">
         <list>
			<ref bean="bluefishgroup.components" />
            <ref bean="webframework.classpathstore.component.custom" />
			<ref bean="webframework.classpathstore.component" />
         </list>
      </property>
   </bean>

        

Now Share will search for our component definitions in bluefishgroup/share-ext/site-data/components in addition to the default locations. Wasn’t that easy? To make it even more convenient, I use a template custom-slingshot-application-context.xml that overrides all of the default search path beans so that all I need to do when I start a new Alfresco development project is add the specific paths. Here is the template configuration for you to use in your own projects:

            
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'>

<beans>

	<!-- Chrome search path overrides -->

	<bean id="bluefishgroup.chrome" parent="webframework.classpathstore">
		<property name="mustExist">
			<value>false</value>
		</property>
		<property name="classPath">
			<value>bluefishgroup/site-data/pages/chrome</value>
		</property>
	</bean>

	<bean id="webframework.searchpath.chrome" class="org.alfresco.web.scripts.SearchPath">
		<property name="searchPath">
			<list>
				<ref bean="bluefishgroup.chrome" />
				<ref bean="webframework.remotestore.chrome" />
				<ref bean="webframework.classpathstore.chrome.custom" />
				<ref bean="webframework.classpathstore.chrome" />
			</list>
		</property>
	</bean>

	<!-- Component search path overrides -->

	<bean id="bluefishgroup.components" parent="webframework.classpathstore">
		<property name="mustExist">
			<value>false</value>
		</property>
		<property name="classPath">
			<value>bluefishgroup/site-data/components</value>
		</property>
	</bean>

	<bean id="webframework.searchpath.component" class="org.alfresco.web.scripts.SearchPath">
		<property name="searchPath">
			<list>
				<ref bean="bluefishgroup.components" />
				<ref bean="webframework.remotestore.component" />
				<ref bean="webframework.classpathstore.component.custom" />
				<ref bean="webframework.classpathstore.component" />
			</list>
		</property>
	</bean>

	<!-- Component types search path overrides -->

	<bean id="bluefishgroup.componentTypes" parent="webframework.classpathstore">
		<property name="mustExist">
			<value>false</value>
		</property>
		<property name="classPath">
			<value>bluefishgroup/site-data/component-types</value>
		</property>
	</bean>

	<bean id="webframework.searchpath.componenttype" class="org.alfresco.web.scripts.SearchPath">
		<property name="searchPath">
			<list>
				<ref bean="bluefishgroup.componentTypes" />
				<ref bean="webframework.remotestore.componenttype" />
				<ref bean="webframework.classpathstore.componenttype.custom" />
				<ref bean="webframework.classpathstore.componenttype" />
			</list>
		</property>
	</bean>

	<!-- Configuration search path overrides -->

	<bean id="bluefishgroup.configurations" parent="webframework.classpathstore">
		<property name="mustExist">
			<value>false</value>
		</property>
		<property name="classPath">
			<value>bluefishgroup/site-data/configurations</value>
		</property>
	</bean>

	<bean id="webframework.searchpath.configuration" class="org.alfresco.web.scripts.SearchPath">
		<property name="searchPath">
			<list>
				<ref bean="bluefishgroup.configurations" />
				<ref bean="webframework.remotestore.configuration" />
				<ref bean="webframework.classpathstore.configuration.custom" />
				<ref bean="webframework.classpathstore.configuration" />
			</list>
		</property>
	</bean>

	<!-- Content associations search path overrides -->

	<bean id="bluefishgroup.contentAssociations" parent="webframework.classpathstore">
		<property name="mustExist">
			<value>false</value>
		</property>
		<property name="classPath">
			<value>bluefishgroup/site-data/configurations</value>
		</property>
	</bean>

	<bean id="webframework.searchpath.contentassociation" class="org.alfresco.web.scripts.SearchPath">
		<property name="searchPath">
			<list>
				<ref bean="bluefishgroup.contentAssociations" />
				<ref bean="webframework.remotestore.contentassociation" />
				<ref bean="webframework.classpathstore.contentassociation.custom" />
				<ref bean="webframework.classpathstore.contentassociation" />
			</list>
		</property>
	</bean>

	<!-- Page search path overrides -->

	<bean id="bluefishgroup.pages" parent="webframework.classpathstore">
		<property name="mustExist">
			<value>true</value>
		</property>
		<property name="classPath">
			<value>bluefishgroup/site-data/pages</value>
		</property>
	</bean>

	<bean id="webframework.searchpath.page" class="org.alfresco.web.scripts.SearchPath">
		<property name="searchPath">
			<list>
				<ref bean="bluefishgroup.pages" />
				<ref bean="webframework.remotestore.page" />
				<ref bean="webframework.classpathstore.page.custom" />
				<ref bean="webframework.classpathstore.page" />
			</list>
		</property>
	</bean>

	<!-- Page association search path overrides -->

	<bean id="bluefishgroup.pageAssociations" parent="webframework.classpathstore">
		<property name="mustExist">
			<value>true</value>
		</property>
		<property name="classPath">
			<value>bluefishgroup/site-data/pages</value>
		</property>
	</bean>

	<bean id="webframework.searchpath.pageassociation" class="org.alfresco.web.scripts.SearchPath">
		<property name="searchPath">
			<list>
				<ref bean="bluefishgroup.pageAssociations" />
				<ref bean="webframework.remotestore.pageassociation" />
				<ref bean="webframework.classpathstore.pageassociation.custom" />
				<ref bean="webframework.classpathstore.pageassociation" />
			</list>
		</property>
	</bean>

	<!-- Page type search path overrides -->

	<bean id="bluefishgroup.pageTypes" parent="webframework.classpathstore">
		<property name="mustExist">
			<value>false</value>
		</property>
		<property name="classPath">
			<value>bluefishgroup/site-data/pages</value>
		</property>
	</bean>

	<bean id="webframework.searchpath.pagetype" class="org.alfresco.web.scripts.SearchPath">
		<property name="searchPath">
			<list>
				<ref bean="bluefishgroup.pageTypes" />
				<ref bean="webframework.remotestore.pagetype" />
				<ref bean="webframework.classpathstore.pagetype.custom" />
				<ref bean="webframework.classpathstore.pagetype" />
			</list>
		</property>
	</bean>


	<!-- Presets search path overrides -->

	<bean id="bluefishgroup.presets" class="org.alfresco.web.scripts.ClassPathStore">
		<property name="mustExist">
			<value>false</value>
		</property>
		<property name="classPath">
			<value>bluefishgroup/site-data/presets</value>
		</property>
	</bean>

	<bean id="webframework.presets.searchpath" class="org.alfresco.web.scripts.SearchPath">
		<property name="searchPath">
			<list>
				<ref bean="bluefishgroup.presets" />
				<ref bean="webframework.classpathstore.presets.custom" />
				<ref bean="webframework.classpathstore.presets" />
			</list>
		</property>
	</bean>

	<!-- Templates search path overrides -->

	<bean id="bluefishgroup.templates" parent="webframework.classpathstore">
		<property name="mustExist">
			<value>false</value>
		</property>
		<property name="classPath">
			<value>bluefishgroup/templates</value>
		</property>
	</bean>

	<bean id="webframework.templates.searchpath" class="org.alfresco.web.scripts.SearchPath">
		<property name="searchPath">
			<list>
				<ref bean="bluefishgroup.webscripts" />
				<ref bean="bluefishgroup.templates" />
				<ref bean="webframework.store.templates.custom" />
				<ref bean="webframework.store.webscripts.custom" />
				<ref bean="webframework.store.templates" />
				<ref bean="webframework.store.webscripts" />
				<ref bean="webframework.store.system-templates" />
			</list>
		</property>
	</bean>

	<!-- Template instances search path overrides -->

	<bean id="bluefishgroup.templateInstances" parent="webframework.classpathstore">
		<property name="mustExist">
			<value>false</value>
		</property>
		<property name="classPath">
			<value>bluefishgroup/site-data/template-instances</value>
		</property>
	</bean>

	<bean id="webframework.searchpath.templateinstance" class="org.alfresco.web.scripts.SearchPath">
		<property name="searchPath">
			<list>
				<ref bean="bluefishgroup.templateInstances" />
				<ref bean="webframework.classpathstore.templateinstance.custom" />
				<ref bean="webframework.classpathstore.templateinstance" />
			</list>
		</property>
	</bean>

	<!-- Template types search path overrides -->

	<bean id="bluefishgroup.templateTypes" parent="webframework.classpathstore">
		<property name="mustExist">
			<value>false</value>
		</property>
		<property name="classPath">
			<value>bluefishgroup/site-data/template-types</value>
		</property>
	</bean>

	<bean id="webframework.searchpath.templatetype" class="org.alfresco.web.scripts.SearchPath">
		<property name="searchPath">
			<list>
				<ref bean="bluefishgroup.templateTypes" />
				<ref bean="webframework.remotestore.templatetype" />
				<ref bean="webframework.classpathstore.templatetype.custom" />
				<ref bean="webframework.classpathstore.templatetype" />
			</list>
		</property>
	</bean>

	<!-- Template processor search path overrides -->
	<!--
		We need to set these to ensure that the template processors can find
		our web scripts.
     -->

	<bean id="webframework.webscripts.templateprocessor" class="org.alfresco.web.scripts.PresentationTemplateProcessor">
		<property name="searchPath" ref="webframework.searchpath" />
		<property name="updateDelay">
			<value>0</value>
		</property>
		<property name="defaultEncoding">
			<value>UTF-8</value>
		</property>
	</bean>

	<bean id="webframework.templateprocessor" class="org.alfresco.web.scripts.PresentationTemplateProcessor">
		<property name="searchPath" ref="webframework.templates.searchpath" />
		<property name="defaultEncoding">
			<value>UTF-8</value>
		</property>
		<property name="updateDelay">
			<value>0</value>
		</property>
	</bean>

	<!-- Theme search path overrides -->

	<bean id="bluefishgroup.themes" parent="webframework.classpathstore">
		<property name="mustExist">
			<value>false</value>
		</property>
		<property name="classPath">
			<value>bluefishgroup/themes</value>
		</property>
	</bean>

	<bean id="webframework.searchpath.theme" class="org.alfresco.web.scripts.SearchPath">
		<property name="searchPath">
			<list>
				<ref bean="bluefishgroup.themes" />
				<ref bean="webframework.remotestore.theme" />
				<ref bean="webframework.classpathstore.theme.custom" />
				<ref bean="webframework.classpathstore.theme" />
			</list>
		</property>
	</bean>

	<!-- WebScripts search path overrides -->

	<bean id="bluefishgroup.webscripts" class="org.alfresco.web.scripts.ClassPathStore">
		<property name="mustExist">
			<value>false</value>
		</property>
		<property name="classPath">
			<value>bluefishgroup/site-webscripts</value>
		</property>
	</bean>

	<bean id="webframework.searchpath" class="org.alfresco.web.scripts.SearchPath">
		<property name="searchPath">
			<list>
				<ref bean="bluefishgroup.webscripts" />
				<ref bean="webframework.store.webscripts.custom" />
				<ref bean="webframework.store.webscripts" />
				<ref bean="webscripts.store" />
			</list>
		</property>
	</bean>

</beans>

        

Caveats

Although I recommend taking the above steps to enable flexible organization of your source, there are a couple things you should watch out for.

The first caveat is common to most Spring-based applications. Because you are overriding existing configuration elements (as opposed to extending), you may run into forward compatibility issues when you upgrade to a newer version/build of Alfresco. To stay on top of this, make it a habit to always examine the source of those configurations you override each time you update your Alfresco source. If something changes, simply copy the new element into your custom configuration file and reinsert your custom bean reference(s).

The second caveat is that it is not always best for all resources to be resolved from the classpath. While this may be the most straightforward way to deploy resources (because you can easily see them within your file system) you can run into consistency problems if you need to maintain these resources across multiple web applications (as in a clustered environment). In those cases, you should consider distributing your resources via the Alfresco repository itself. You can do this by using instances of the webframework.remotestore bean. Alfresco already does this for a handful of resources (search the XML files in the Alfresco source for instances of webframework.remotestore.component for an example), and it should be straightforward to add repository lookup capability for other resources by creating new beans in your Spring configuration.

Parting Thoughts

I like sharing my Alfresco development experiences so that others may be able to build upon them to continuously improve the quality of contributions to the Alfresco ecosystem. Along those lines, I hope you’ve found this article helpful, and I encourage you to ask questions or leave feedback by adding a comment below. Thanks for reading!

 Votes | Average: 0 out of 5 Votes | Average: 0 out of 5 Votes | Average: 0 out of 5 Votes | Average: 0 out of 5 Votes | Average: 0 out of 5 (0 votes, average: 0 out of 5)
Loading ... Loading ...

3 Comments

[...] Josh Toub leads the Alfresco team here at Blue Fish, and he’s just written a new article that can help Alfresco developers improve the maintainability and supportability of their Alfresco solutions. [...]

Blue Fish Development Group » Blog Archive » New Alfresco Article on Improving the Way Custom Code is Organized | February 3rd, 2010 1:45 pm

[...] Blue Fish - Alfresco Source Code Organization [...]

Blue Fish Development Group » Blog Archive » Making Alfresco Share Dashlets Configurable | April 6th, 2010 11:02 am

Well done! I do appriciate.

Edward Seliga | May 26th, 2010 8:10 am

Comment on this article:

You must be logged in to post a comment.

Notification

Subscribe to our newsletter to be notified when new articles are posted. You can unsubscribe at any time.