Share This Post

Painless Alfresco Development: Improving Supportability through Source Code Organization

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:

TypicalSource
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:

TypicalDeployed
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:

IdealSource
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:

IdealDeployed
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:

TwoWebApps
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' 
'https://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
ArgonDigitalgroup/share-ext/site-data/components:


<bean id="argondigital.components" 
parent="webframework.classpathstore">
<property name="mustExist">
<value>true</value>
</property>
<property name="classPath">
<value>ArgonDigitalgroup/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="argondigital.components" />
<ref bean="webframework.classpathstore.component.custom" />
<ref bean="webframework.classpathstore.component" />
</list>
</property>
</bean>

Now Share will search for our component definitions in
ArgonDigitalgroup/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' 
'https://www.springframework.org/dtd/spring-beans.dtd'>
<beans>
<!-- Chrome search path overrides -->
<bean id="ArgonDigitalgroup.chrome" parent="webframework.classpathstore">
<property name="mustExist">
<value>false</value>
</property>
<property name="classPath">
<value>ArgonDigitalgroup/site-data/pages/chrome</value>
</property>
</bean>
<bean id="webframework.searchpath.chrome" 
class="org.alfresco.web.scripts.SearchPath">
<property name="searchPath">
<list>
<ref bean="ArgonDigitalgroup.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="argondigital.components" parent="webframework.classpathstore">
<property name="mustExist">
<value>false</value>
</property>
<property name="classPath">
<value>ArgonDigitalgroup/site-data/components</value>
</property>
</bean>

<bean id="webframework.searchpath.component" 
class="org.alfresco.web.scripts.SearchPath">
<property name="searchPath">
<list>
<ref bean="argondigital.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="argondigital.componentTypes" 
parent="webframework.classpathstore">
<property name="mustExist">
<value>false</value>
</property>
<property name="classPath">
<value>ArgonDigitalgroup/site-data/component-types</value>
</property>
</bean>

<bean id="webframework.searchpath.componenttype" 
class="org.alfresco.web.scripts.SearchPath">
<property name="searchPath">
<list>
<ref bean="argondigital.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="ArgonDigitalgroup.configurations" parent="webframework.classpathstore">
<property name="mustExist">
<value>false</value>
</property>
<property name="classPath">
<value>ArgonDigitalgroup/site-data/configurations</value>
</property>
</bean>

<bean id="webframework.searchpath.configuration" 
class="org.alfresco.web.scripts.SearchPath">
<property name="searchPath">
<list>
<ref bean="ArgonDigitalgroup.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="ArgonDigitalgroup.contentAssociations" parent="webframework.classpathstore">
<property name="mustExist">
<value>false</value>
</property>
<property name="classPath">
<value>ArgonDigitalgroup/site-data/configurations</value>
</property>
</bean>

<bean id="webframework.searchpath.contentassociation" class="org.alfresco.web.scripts.SearchPath">
<property name="searchPath">
<list>
<ref bean="ArgonDigitalgroup.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="ArgonDigitalgroup.pages" parent="webframework.classpathstore">
<property name="mustExist">
<value>true</value>
</property>
<property name="classPath">
<value>ArgonDigitalgroup/site-data/pages</value>
</property>
</bean>

<bean id="webframework.searchpath.page" 
class="org.alfresco.web.scripts.SearchPath">
<property name="searchPath">
<list>
<ref bean="ArgonDigitalgroup.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="ArgonDigitalgroup.pageAssociations" parent="webframework.classpathstore">
<property name="mustExist">
<value>true</value>
</property>
<property name="classPath">
<value>ArgonDigitalgroup/site-data/pages</value>
</property>
</bean>

<bean id="webframework.searchpath.pageassociation" class="org.alfresco.web.scripts.SearchPath">
<property name="searchPath">
<list>
<ref bean="ArgonDigitalgroup.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="ArgonDigitalgroup.pageTypes" parent="webframework.classpathstore">
<property name="mustExist">
<value>false</value>
</property>
<property name="classPath">
<value>ArgonDigitalgroup/site-data/pages</value>
</property>
</bean>

<bean id="webframework.searchpath.pagetype" 
class="org.alfresco.web.scripts.SearchPath">
<property name="searchPath">
<list>
<ref bean="ArgonDigitalgroup.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="ArgonDigitalgroup.presets" class="org.alfresco.web.scripts.ClassPathStore">
<property name="mustExist">
<value>false</value>
</property>
<property name="classPath">
<value>ArgonDigitalgroup/site-data/presets</value>
</property>
</bean>

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

<!-- Templates search path overrides -->

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

<bean id="webframework.templates.searchpath" 
class="org.alfresco.web.scripts.SearchPath">
<property name="searchPath">
<list>
<ref bean="ArgonDigitalgroup.webscripts" />
<ref bean="ArgonDigitalgroup.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="ArgonDigitalgroup.templateInstances" parent="webframework.classpathstore">
<property name="mustExist">
<value>false</value>
</property>
<property name="classPath">
<value>ArgonDigitalgroup/site-data/template-instances</value>
</property>
</bean>

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

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

<bean id="ArgonDigitalgroup.templateTypes" parent="webframework.classpathstore">
<property name="mustExist">
<value>false</value>
</property>
<property name="classPath">
<value>ArgonDigitalgroup/site-data/template-types</value>
</property>
</bean>
<bean id="webframework.searchpath.templatetype" 
class="org.alfresco.web.scripts.SearchPath">
<property name="searchPath">
<list>
<ref bean="ArgonDigitalgroup.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="ArgonDigitalgroup.themes" parent="webframework.classpathstore">
<property name="mustExist">
<value>false</value>
</property>
<property name="classPath">
<value>ArgonDigitalgroup/themes</value>
</property>
</bean>

<bean id="webframework.searchpath.theme" 
class="org.alfresco.web.scripts.SearchPath">
<property name="searchPath">
<list>
<ref bean="ArgonDigitalgroup.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="ArgonDigitalgroup.webscripts" class="org.alfresco.web.scripts.
ClassPathStore">
<property name="mustExist">
<value>false</value>
</property>
<property name="classPath">
<value>ArgonDigitalgroup/site-webscripts</value>
</property>
</bean>

<bean id="webframework.searchpath" class="org.alfresco.web.scripts.SearchPath">
<property name="searchPath">
<list>
<ref bean="ArgonDigitalgroup.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!

Painless Alfresco Development: Improving Supportability through Source Code Organization

More To Explore

AI to Write Requirements

How We Use AI to Write Requirements

At ArgonDigital, we’ve been writing requirements for 22 years. I’ve watched our teams waste hours translating notes into requirements. Now, we’ve cut the nonsense with AI. Our teams can spend

ArgonDigital | Making Technology a Strategic Advantage