WCM Best Practices: How Many Web Forms?

September 15th, 2008 by

A customer recently asked me how long it would take to convert a site of theirs to be managed by Alfresco. This is something that comes up a lot, so it’s a good opportunity to discuss the issue. I’ll focus here on migrating a site that’s not managed by any CMS into Alfresco (i.e. no migration process), just a static or dynamic pages that need to be now managed by Alfresco.

The salesy answer is as fast as you can import your pages into Alfresco i.e. as quick as few minutes. This is true, in a way – this will allow you to have the site pages inside Alfresco WCM. You can then modify the pages, and deploy the content through Alfresco. However, the goal of any web content management system is to enable nontechnical users make changes on the website, and this won’t quite do. That means that some redesign is usually warranted. Recently I was talking about my colleague, Peter Monks, about making a decision for when a certain page should be converted to a form, and when it doesn’t make sense.

Of course, I am not talking about the obvious if you have 1000 product pages that all look the same, you clearly need forms, and one or more renditions for creating the pages. But, if you have a page that may change a few times a year, or even less frequently, i.e. About Us page, then is it worth to convert it?

Rule Of Thumb

When making this decision, you have to work with the customer and figure out how often certain page or set of pages will likely need to be changed change. Another, secondary, consideration is the technical level of who needs to make the changes to the page. For example, if you have a somewhat technical person who knows HTML reasonably well responsible for a certain page that doesn’t change frequently, then converting it to an XForm is probably an overkill.

Back to non-technical users – if a Web Form is only going to be used for one or two pages (instances of that type) then it’s probably not worth implementing as a Web Form. Another approach you can take is to see whether it’s possible to implement a more generic Web Form that can handle multiple different “types” of pages.

HTML “BLOBs” for Infrequently Changing Pages

Peter also suggested as a best practice what he called “HTML BLOB” Web Form, which should be used for the one-off pages or page fragments. It’s useful in several cases, and as a “safety valve” of sorts. It lets content contributors create those one off pages in a forms based environment, but without too much Web Form development effort. Pages like “About Us”, “Contact Us” etc. are often good examples of “HTML Blob” pages.

The HTML Blob is basically a simple Web Form with a big text field, probably with a title and summary fields, as well as a “related items” repeating element. All of these are optional, except for the content of course (the BLOB). In that text field you can put html, and that gets converted to an html page. You can also have other “catch all” content types, such as “Link”, “Teaser List”, etc. You do need to be aware that you are mixing up content and presentation, but in this case this is probably OK.

Conversion Process

When approaching a site to be converted, you should go through it and analyze all page types to come up with an initial inventory of content types (web forms). Then do a second sweep. This time, focus on combining and reducing the number of web forms to an absolute minimum – the fewer the better. Keep in mind that you can include both static and dynamic XSDs inside other XSDs. This means that you can share some code across multiple XForms, if that makes sense. This is also the time to decide which pages are candidate for the catchall (blob) pages. Once you reduce the total number of forms to a minimal number, then you can start the estimation process for how long it will take you.

Please share your experiences and ideas in the best way to do the migration of a site to Alfresco in comments.

Implementing Cascading DropDowns For Editing Properties

August 18th, 2008 by

his is a followup to a post about dynamic dropdown. Please read it to have the appropriate context.

After dynamically setting dropdown values, another requirement that comes up frequently is ability to do cascading dropdowns, with one dropdown being dependent on another. For example, ability to select a country, and then have another dropdown populated with cities.

This feature requires ability to pass the value of one dropdown to the renderer of another, which, as we’ll see, introduces a few issues we need to resolve. The way i implemented this is through adding ability to insert references to other property values into the query that returns dynamic results. So if in my Lucene query, I’d like to replace

TEXT:”Greece”

with

TEXT:”${my:country}”

where ${my:country} will be substituted by the value of property my:country, a query which presumably will return cities (work with me on this).

In order to do this, I am going to further enhance the constraint code. I introduced a new class called SearchBasedDependencyListConstraint, which sits between SearchBasedListConstraint and LuceneSearchBasedListConstraint. I could have added the methods to existing classes, but wanted to make the code a bit more modular, with clearer responsibilities for each class.

Constraint Class Hierarchy


NOTE: the way the code is written, my constraints don’t actually check whether the submitted value conforms to the constraint. However, the UI should take care of that – further integrity checking is left as an exercise to the reader.

inside SearchBasedDependencyListConstraint, I created some methods that are able to take a string and replace tokens in that string with values from the node. My class has those methods, which I made public static for reusability, and a property that saves the Node object, so I can pull out the current property values. The key method is resolveDependenciesOnProperties. It first pulls out all the property names that need to be looked up by using regexp, then creates a map by pulling out the properties it found from the Node object, and finally replacing the tokens in the query with values.

[sourcecode language="java"]
protected String resolveDependenciesOnProperties(String query)
{
List<String> propNames = getPropertyNames(query, getTokenExpression());
Map<String, String> map = populateNodeValues(propNames, node);
String newQuery = replaceQueryParametersWithValues(query, map);
return newQuery;
}
[/sourcecode]

So far so good. However, there are a few issues that come up.

1. No Access to Node Object from Constraint

The first one is that substituting node values required access to the node object, so you can lookup properties. However, the constraints interface API does not receive a node object. This is a problem. We are going to get around it with a bit of trickery. Like I said before, the rendering of the UI happens in a TextFieldGenerator, which is a Component Generator. Because creation of the UI component happens before the component is asked for the drop down values, that means that if we can intercept that call, we can pass in the node reference to the constraint. We already have the property in the constraint, so we just need to set it before the component UI is rendered.

Now, let’s write our component generator. We will be able to reuse most of TextFieldGenerator, so we need to subclass it. It has a method

protected ListOfValuesConstraint getListOfValuesConstraint(FacesContext context, UIPropertySheet propertySheet, PropertySheetItem item)

which is responsible for returning the constraint. We will copy the original implementation, and add a few lines specific for our needs.  We are only going to set the Node for the constraints that can receive it:

if (constraint instanceof LuceneSearchBasedListConstraint)
{
Node currentNode = (Node)propertySheet.getNode();
// This is a workaround for the fact that constraints do not have a reference to Node.

((LuceneSearchBasedListConstraint)constraint).setNode(currentNode);
lovConstraint = (SearchBasedListConstraint)constraint;
break;
}

Note that I am inserting currentNode, which is the unsaved version of the node. The properties of the node won’t get persisted until save is called.

The second issue to overcome is that Alfresco property sheet was not designed to have dependent properties, so we need to refresh the screen after the country drop down changes. In our component generator, we’ll change the UI box that gets rendered to have a javascript fragment that auto-submits the page. In CustomListComponentGenerator, in method createComponent, I pass in a parameter into the drop down box rendered:

protected UIComponent createComponent(FacesContext context, UIPropertySheet propertySheet, PropertySheetItem item)
{

UIComponent component = super.createComponent(context, propertySheet, item);
if (component instanceof UISelectOne && isAutoRefresh())
component.getAttributes().put(“onchange”, “submit()”);
return component;
}


2. Refreshing the Screen

This introduces another issue – when the page is refreshed, all the UI elements do NOT get re-created from scratch. To work around this one is a bit more difficult.  We need to modify the behavior of the property sheet. Luckily, since we have access to the source through the SDK, we can study the out of the box property sheet implementation, class UIPropertySheet. It has a method encodeBegin which is responsible for creation of the actual UI. I overrode the implementation with my own and will reconfigure Alfresco to use mine.

In this case, I have a crude implementation, where I simply clear out all the existing JSF UI elements, forcing the code to recreate the elements from scratch (which will have a side effect of resolving all the dependencies as the UI elements are getting created). The rest is handled by default implementation in the superclass.

public void encodeBegin(FacesContext context) throws IOException
{
if (getChildren().size() != 0)
this.getChildren().clear();
super.encodeBegin(context);
}


We now need to make Alfresco pick up our new implementation (I called it RefreshableUIPropertySheet). To do this, we need to go into the WAR file, and replace PropertySheet section in faces-config-repo.xml with:

<component>
<component-type>org.alfresco.faces.PropertySheet</component-type>
<component-class>org.alfresco.sample.web.RefreshableUIPropertySheet</component-class>
</component>


Note: There may be a more extensible way to do this – doing it this way means that every time you upgrade, you have to reintroduce this change.

AutoRefresh or Not AutoRefresh?

In the code for CustomListComponentGenerator, you probably noticed a reference to isAutoRefresh() method. I needed to add this since I am reusing the same Constraint for both Country and City properties, but I only need the Country property change to trigger auto-refresh. I handled this through this parameter, and also introducing two components into faces-config-beans.xml. The first one sets autoRefresh to True, the second one, to false.

<managed-bean>
<description>
Bean that generates a custom generator component
</description>
<managed-bean-name>CustomListComponentGeneratorWithRefresh</managed-bean-name>
<managed-bean-class>org.alfresco.sample.web.CustomListComponentGenerator</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>autoRefresh</property-name>
<value>true</value>
</managed-property>
</managed-bean>

<managed-bean>
<description>
Bean that generates a custom generator component
</description>
<managed-bean-name>CustomDependentListComponentGenerator</managed-bean-name>
<managed-bean-class>org.alfresco.sample.web.CustomListComponentGenerator</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>autoRefresh</property-name>
<value>false</value>
</managed-property>
</managed-bean>


Installing It & Packaging it all up.

Building the sample should be easy.Copy the sample into the SDK samples directory, and run ant compile package-jar. Copy the jar file into WEB-INFlib, and config files into extension folder. Also follow instructions in AddToFacesConfig.txt to edit the JSF bean config files.

Unfortunately, because we need to change the bindings of some core components (i.e. PropertySheet, we can’t avoid but to go into the WAR, or editing some files. Additionally, this is not currently packaged up as an AMP file, which means the jar file needs to be manually copied into the WAR. I hope to repackage the example better in a future post.

Conclusion.

We now have some code that does quite a lot. It is able to perform a search query, substitute values in that query based on other parameters, render the UI appropriately, and refresh the UI every time there is a dropdown. Pretty cool. Another way to implement the whole project would have been through a much more AJAX-heavy compontent generator talking to web script components. Possibly in a future post :-) .

P.S. Sorry for the code fragments being a bit ugly. If you know of a good tool that can format Java as HTML for blogging consumption, let me know in comments – in a few searches I did I couldn’t find anything good enough, and the one Eclipse plug-in I did find stopped working for me (spent over an hour figuring it out and gave up).

Dynamic Data-Driven Drop Downs for List Properties

August 8th, 2008 by

Dynamic Dropdowns

A customer request that comes up often is a need to create a property that is dynamically populated through drop down list. Alfresco has ability to set a pre-set list of drop-downs (i.e. apple, orange) through the LIST constraint, but there is no way to have list change dynamically.

For more on constraints, see this wiki page: http://wiki.alfresco.com/wiki/Constraints. Also, this post assumes some understanding of Component Generators – they are UI components that get dynamically rendered in the Document / Space details Property Sheet. See http://wiki.alfresco.com/wiki/Component_Generator_Framework.The code accompanying this article can be found on Community Developer Toolbox here.

There are several approaches to doing this, the first one is using the existing infrastructure and extend the List Constraints, and the second is to create your own component generator from scratch that generates the drop down the any way you like.
In this post, I’ll talk about reusing the existing list infrastructure. I built a custom constraint that receives a Lucene query and renders the drop down based on results of that query.

This allows you to model a node hierarchy somewhere and point to that hierarchy through the constraint. You can then maintain that hierarchy, adding or deleting nodes, which in turn will modify your drop down appropriately. For example, you can put nodes as a set of subspaces in the data dictionary, or use categories. A potentially useful artifact of this implementation is security filtering – if you choose to secure your nodes, you can have the drop downs render different for different people, based on what they have access to.

Creating SearchBasedListConstraint

The way list drop down works is that the TextFieldGenerator component generator has explicit code to detect the LIST constraint, and renders itself appropriately. It calls getAllowedValues method on the constraint to get the values the drop down should be populated with. I extended this ListConstraint, since if I implement the getAllowedValues to be dynamically, then the drop down should be dynamic.

In my implementation, I chose to separate the generic dynamic list capability from the Lucene specific capability. I have a class called SearchBasedListConstraint that extends ListOfValuesConstraint, and has an abstract method getSearchResult, which returns the list that will be passed on as AllowedValues. Implement this method in a subclass to be backed by anything you want, a file, a database, etc. Another thing i added
here is a reference to ServiceRegistry, so that Alfresco Repository Services could be called, such as SearchService, or NodeService, by subclases. There are a few other methods, which I will discuss in a later post.

public abstract class SearchBasedListConstraint extends ListOfValuesConstraint
{

@Override
public List<String> getAllowedValues()
{
List<String> allowedValues = getSearchResult();
super.setAllowedValues(allowedValues);
return allowedValues;
}

protected abstract List<String> getSearchResult() ;

There is a good forum post that talks a bit about implementing this as well.

http://forums.alfresco.com/en/viewtopic.php?f=4&t=11687

Dynamic DropDowns based on Lucene Searches

As far as an example implementation, I thought that allowing getSearchResult to be backed by a Lucene Query would be useful, since then you can store your data inside Alfresco. It also means that you can have dynamic querying based on content already in Alfresco, another potentially useful feature. So I subclass SearchBasedListconstraint with LuceneSearchBasedListConstraint. I’d like this to be parametrized and set from the content model (this is where the constraints are set), so I expose setQuery public method. Because the SearchService API requires a StoreRef to be passed, i introduce another setter called setStoreRef. I already have a reference to ServiceRegistry from superclass, so I can just start using it.

We are now ready to perform the search, the code currently is hardcoded to return the Name property.

protected List<String> getSearchResult()
{
if (logger.isDebugEnabled())
logger.debug(“Original Query ” + query);

StoreRef storeRef = new StoreRef(strStoreRef);
ResultSet resultSet = getServiceRegistry().getSearchService().query(storeRef, SearchService.LANGUAGE_LUCENE, finalQuery);
NodeService nodeSvc = getServiceRegistry().getNodeService();

List<String> allowedValues = new ArrayList<String>();
for (ResultSetRow row : resultSet)
{
allowedValues.add((String)nodeSvc.getProperty(row.getNodeRef(), ContentModel.PROP_NAME));
}

return allowedValues;
}

That’s all it takes. in the sample, there is some additional code to manage dependencies between dropdowns, which once again I will explain later.

Configuring Constraints

I am using the exampleModel that gets supplied with Alfresco in the extension directory, and added a couple of new fields – country and city. Here is the definition of constraint, note the parameter type query being set.

<constraint name=”my:customConstraint”
type=”org.alfresco.sample.constraints.LuceneSearchBasedListConstraint” >
<parameter name=”query”>
<value> TYPE:”{http://www.alfresco.org/model/content/1.0}content”
</value>
</parameter>
</constraint>

And now introduce this constraint to a property.

<property name=”my:country”>
<title>Country</title>
<type>d:text</type>
<index enabled=”true”>
<atomic>true</atomic>
<stored>false</stored>
<tokenised>false</tokenised>
</index>
<constraints>
<constraint ref=”my:customConstraint” />
</constraints>
</property>

Bootstrapping Alfresco Node Services

Here we have a bit of an issue. The constraints code does not by default get injected through Spring framework, it in fact gets dynamically instantiated through reflection. However, we need to use Alfresco’s foundation services in it, such as NodeService and SearchService, so we need a reference either to them, or to the ServiceRegistry. The solution is to initialize the Constraint from Spring through a use of static internal variables. Then you can use spring to inject ServiceRegistry to the setter, which in turn sets the internal static variable. From then any instances of the class will have access to the static variable.

Here is the code:

private static ServiceRegistry registry;

public ServiceRegistry getServiceRegistry()
{
return registry;
}

public void setServiceRegistry(ServiceRegistry registry)
{
SearchBasedListConstraint.registry = registry;
}

And now we can configure this, so that the Spring configuratoin takes care of setting the static variable.

<bean id=”LuceneSearchBasedListConstraintInitializer”
class=”org.alfresco.sample.constraints.LuceneSearchBasedListConstraint”>

<property name=”serviceRegistry”>
<ref bean=”ServiceRegistry”/>
</property>
</bean>

And the corresponding configuration:

<bean id=”LuceneSearchBasedListConstraintInitializer”
class=”org.alfresco.sample.constraints.LuceneSearchBasedListConstraint”>

<property name=”serviceRegistry”>
<ref bean=”ServiceRegistry”/>
</property>
</bean>

This should do it.

An even more interesting question is how to create dependencies between properties, i.e. so you can choose a country, and then cities of that country get populated automatically. This is part of the sample I provided, but will be explained in a future post.

Update 05/03/2009 – The Source code is now on the new share site (share.alfresco.com), in Developer Toolbox here.

Blogged with the Flock Browser

Learning Alfresco – Intermediate Sample Applications for Document Management

June 7th, 2008 by

Are you new to Alfresco, have been learning some of the concepts, studied the SDK and other simple examples out there, but are not quite sure how to bring it all together?

There are two great sample applications that you should check out. They are a gold-mine of useful information on the core repository capabilities, and Document Management. They also represent best practices in developing some types of applications that are possible with the platform.

The samples are the Records Management Module, which is a module in the main source tree that gets packaged up as a separate AMP, and the Expense Report application my colleague Yong Qu recently put together to make our own internal expense submission process easier. Records Management module is a great example of an advanced content modeling, and all the capabilities around behaviors, scheduled jobs, extending the Alfresco Web Client, and the like. The Expense application contains a lot of logic embedded in JavaScript, and is a Web 2.0-like solution that uses web scripts heavily to tie to a backend workflow.

I am going to outline what each has to offer in terms of samples you can study (along with links to the Wiki for more information).

Both:

  • Packaging solution up as AMP
  • Bootstrap some data and scripts into the repository
  • Introduce new Space Templates for easy reuse

The Records Management Module Learning Points

Expense Report Application Learning Points

Unfortunately, the publicly available documentation is currently pretty sparse. Both of them require understanding of core Alfresco concepts ahead of time, but can be very rewarding to study.

I hope to highlight where to look for within the source of the two applications in a future post.

You can download Expense Application from the Alfresco Content Community (registration required). You can uncompress the AMP into a separate directory to view the code, as well as browse through it after you install it.

Tags: , ,


Alfresco Home | Legal | Privacy | Accessibility | Site Map | RSS  RSS

© 2012 Alfresco Software, Inc. All Rights Reserved.