Archive for the ‘UI Customization’ Category

XML Metadata Extraction for WCM

Monday, December 1st, 2008

While the XSDs in the WCM (AVM) are the equivalent of content models in DM, there is no effective way to search them. More specifically, it’s useful to be able to search based on a specific metadata elements in the generated XMLs, something that you need to do frequently in highly dynamic sites. In this post, we’ll discuss the sample I created for this that’s in the content community here (registration required).

In this example, I have a WCM content type defined through XSD press_release.xsd.  I want to extract some metadata from it, for example, expiration date of the press release. The extraction process works similarly to the way things function on the DM side, with some gotchas. When extracting metadata, you need somewhere (properties) to store it.

For WCM (where all the XML nodes are stored as wcm:avmplaincontent type), is by creating an aspect with the properties that you want to extract.   This is important – it has to be an aspect. This aspect will automatically get applied appropriately, as we’ll see later. In the included example, I have a XSD that creates a press release.  I want to extract and index three properties – abstract (string), expiration date (date), and numtimes (int). Here is the code (I removed all the indexing properties for simplicity)

<aspects>

<aspect name="my:press_release_metadata">

<title>Sample Aspect for WCM – Press Release</title>

<properties>

<property name="my:abstract">

<type>d:text</type>

</property>

<property name="my:expiration_date">

<type>d:datetime</type>

</property>

<property name="my:numtimes">

<type>d:int</type>

</property>

</properties>

</aspect>

</aspects>

You’ll also need to expose your aspect properties in the UI through web-client-config-custom.xml

<config evaluator="aspect-name" condition=" my:press_release_metadata">

<property-sheet>

<show-property name="my:abstract" />

<show-property name="my:expiration_date" />

<show-property name="my:numtimes" />

</property-sheet>

</config>

Once I have the aspect in my content model (customModel.xml, which I introduce to the Data Dictionary through custom-model-context.xml), I can start configuring extraction process as outlined in wcm-xml-metadata-extracter-context.xml.  There are two key sections:

  1. Selector section (extracter.xml.sample.selector.XPathSelector bean), which looks inside the XML and maps it to the correct Extractor Bean.  Since all the XForms of any type get saved as XML, we need to select the appropriate one (in this case pr:press_release). This configuration associates the specific XForm with a specific extraction definition.

    <bean id="extracter.xml.sample.selector.XPathSelector" class="org.alfresco.repo.content.selector.XPathContentWorkerSelector" init-method="init"> <property name="workers"> <map> <entry key="/pr:press_release"> <ref bean="extracter.xml.sample.AlfrescoCustomModelMetadataExtracter" /> </entry> </map> </property> </bean>

  2. Extractor bean for each of the Web Content Types you defined.  These have two parts:

    A. xpathMappingProperties – take an xpath expression that can extract value out of XML file and store it into internal Map.  So, for example, the abstract property can be found through xpath expression "/press_release/abstract".  It then gets stored into "abstract" internal map property.

    <prop key="abstract">/press_release/abstract</prop>

    Note that we have to also specify namespace so Alfresco can resolve them appropriately:   

    <prop key="namespace.prefix.pr">http://www.alfresco.org/alfresco/pr</prop>

B. mappingProperties – takes the properties out of internal map, and puts it into the specified data dictionary property.  Here is the key – the extractor finds the corresponding aspect and automatically ("automagically") adds it to the Alfresco node. Before setting it, it checks for the target data type, and attempts to convert it to that type.  In the example we are using, it takes out the internal map property "abstract" and sets it to property my:abstract. In this case the property is a string, so no conversion is really required.

       <prop key="abstract">my:abstract</prop>

Note on Converting Dates: When I did this initially, I got an exception for converting dates.  This is because the XForms store dates in the format of 2008-04-28, and the automatic cast did not work.  To remedy that, I added a configuration setting where I specified the correct date format for the extractor to use: <property name="supportedDateFormats"> <list> <value>yyyy-MM-dd</value> </list> </property> Note, that metadata extraction runs when you create content (in the user sandbox).   They key here is that the aspect gets applied automatically by the extraction process – you don’t need to make sure it’s added. You don’t even see mention of the aspect anywhere in the configuration files. This is what is should look like in the example:


The Lucene indexing will happen when you promote the content to the staging sandbox.

The indexing is governed by the settings on the properties you define on properties of the aspects (namely <index> element):

<property name="my:expiration_date">

<type>d:datetime</type>

<index enabled="true">

<atomic>true</atomic>

<stored>false</stored>

<tokenised>false</tokenised>

</index>

</property>

You cannot search based on these properties from the Search UI, since this relates to the WCM content, but you can query this using the Node Browser for testing, or, of course, the ultimate goal is likely to expose this through some web scripts.

Dynamic Models in 3.0

Tuesday, November 18th, 2008

One of the features in 3.0 that I am particular excited about is Dynamic Models (though they actually have been around since 2.9).  It allows changing the content model without having to restart the server.   While implemented to provide Multi-Tenancy capabilities, I am excited about it because it gets us much closer to having ability to have users create content models through UI.  In addition to dynamic modeling capability, you can now make some UI customizations, upload resource bundles, and add workflows, all without having to restart the server.  This is not only a great feature, but will save developers a lot of time.

To learn more about Dynamic Models, check out the wiki page, and for a quick overview – see this great Screencast that J.M. Pascal created showing the Dynamic Models functionality.  Check it out, as well as his blog in general – he has a bunch of fun presentation about some other Alfresco concepts as well!

Dynamic Data-Driven Drop Downs for List Properties

Friday, August 8th, 2008

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


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

© 2012 Alfresco Software, Inc. All Rights Reserved.