03-20-2012 10:22 PM
03-21-2012 03:50 AM
03-21-2012 10:39 AM
03-21-2012 12:03 PM
03-22-2012 05:43 PM
Dynamic Data-Driven Drop Downs for List Properties
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. Seehttp://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.
Implementing Cascading DropDowns For Editing Properties
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.
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).
04-19-2012 08:09 AM
Tags
Find what you came for
We want to make your experience in Hyland Connect as valuable as possible, so we put together some helpful links.