Wednesday, September 19, 2012

Getting closer to Alfresco on the command line


I recently kicked of a small GPL project to work with the Alfresco repository from the command line. Check out the tools here http://code.google.com/p/alfresco-shell-tools/.

Since its early stage a month ago I am already using the project at several customers to get my admin tasks done more easily.



Friday, June 8, 2012

How to add custom Share site presets (the nice way) - Alfresco Hack #5

[See here about my post about the benefits of the b2b marketplace for Alfresco modules and solutions.]
[Check out our newest module for email management and archiving with Alfresco]
[See here about our even newer module for automatic trashcan management with Alfresco]

This blog post will explain in depth a way to add custom site presets to Alfresco share as nice as possible. Nice here means, to play well with others:
  • use the extension mechanism
  • do not overwrite any existing configuration files
  • do not overwrite any Alfresco Spring beans
This way, many modules can be deployed independently to Alfresco without interfering, each providing a new site preset.

Currently, as of Alfresco 4.0dCE and 4.0.1EE it is not possible to add presets through the share extension mechanism alone.

A common requirement of anybody who likes to customize Alfresco share is to provide custom site presets. For example, show the links-Page besides the document library per default. Or to present a specialized page targeting a specific use case. A common requirement for ourselves and the ecm Market vendors as well.


What is a share site preset?


A site preset contains the initial configuration of a site: The site dashboard layout, and any preconfigured site dashlets. The initial set of site pages can be set as well. Examples for existing site pages are the document library page, the calendar, the wiki, the datalist page. It is an easy way to add your custom requirements to a site. So, to sum it up, a site preset is maybe better unterstood as a site type, because it captures all behaviour that makes it special (or different) from others. And of course, multiple sites can be instantiated from the site type.


How to create a share site preset?


The steps are simple as noted below - but to play nice with other, care must be taken to not override files and spring beans:
  1. Create a preset file containing the XML configuration of your site : my-custom-presets.xml. A good starting point is to copy the existing presets.xml and remove all unnecessary configuration 
  2. Create an extension to augment the presets UI dialog controller with your new site data.
  3. Add our new presets file to the list of preset files that the share preset manager recognizes.
Steps 1 and 2 can be done without overwriting any files using the existing share extension mechanism, but step 3 would need to overwrite the presets manager bean named webframework.presets.manager to add the new preset. This is the presets manager bean (it is part of surf located in the spring-surf-1.0.0.jar) as it comes with alfresco:



   <!-- Presets manager - configured to walk a search path for preset definition files -->
   <bean id="webframework.presets.manager" class="org.springframework.extensions.surf.PresetsManager">
      <property name="modelObjectService" ref="webframework.service.modelobject" />
      <property name="searchPath" ref="webframework.presets.searchpath" />
      <property name="files">
         <list>
            <value>presets.xml</value>
         </list>
      </property>
   </bean>

Out of the box, it would be possible to place a file named presets.xml in a package named alfresco/site-data/presets or alfresco/web-extension/site-data/presets - which is resolved by the the search path. But because the file name is fixed to presets.xml it is not possible to deploy multiple presets without overwriting each other. But to the rescue comes the tool I introduced in the blog post Augmenting Alfresco Spring Bean configuration without overwriting beans. This litte class will add the my-custom-presets.xml file the the presets manager bean on startup dynamically, so that the original bean must not be overridden:

<bean id="ecm4u.custom.presets" class="de.ecm4u.alfresco.utils.spring.AddToListPropertyPostProcessor">
    <property name="beanName" value="webframework.presets.manager" />
    <property name="propertyName" value="files" />
    <property name="position" value="-1" />
    <property name="additionalProperties">
        <list>
            <value>my-custom-presets.xml</value>
        </list>
    </property>
</bean>

A module using this tooling plays nice with others, because no overwriting as taken place.

Back to step 2,  creating the share extension to augment the site creation controller with the new site:

Place a file my-custom-extension.xml into alfresco/site-data/extensions/ with the following contents. It is a share extension with a single package customization.


<extension>
    <modules>
        <module>
            <id>my-custom-site-preset</id>
            <auto-deploy>true</auto-deploy>
            <customizations>
                <customization>             <targetPackageRoot>org.alfresco.modules</targetPackageRoot>                <sourcePackageRoot>ecm4u.samples.custom.modules</sourcePackageRoot>
                </customization>
            </customizations>
        </module>
    </modules>
</extension>



To finally make your site preset available for selection in the share create site dialog, a short javascript snippet is needed. Add this snippet to a file named create-site.get.js into a package webscripts.ecm4u.sampels.custom.modules:

if(model.sitePresets) {
    model.sitePresets.push({
        id: "my-custom-preset-id",
        name: "My custom site"
      });
}

Back to step 1, creating the presets description file:

How a presets file might actually look like is shown here. Add this file at alfresco/site-data/presets and name it my-custom-presets.xml:

Please note, that the preset id has to match the id parameter of the script customization above.

<?xml version='1.0' encoding='UTF-8'?>
<presets>
   <preset id="my-custom-preset-id">
      <components>         
         <!-- title -->
         <component>
            <scope>page</scope>
            <region-id>title</region-id>
            <source-id>site/${siteid}/dashboard</source-id>
            <url>/components/title/collaboration-title</url>
         </component>
         <!-- navigation -->
         <component>
            <scope>page</scope>
            <region-id>navigation</region-id>
            <source-id>site/${siteid}/dashboard</source-id>
            <url>/components/navigation/collaboration-navigation</url>
         </component>
         <!-- dashboard components -->
         <component>
            <scope>page</scope>
            <region-id>full-width-dashlet</region-id>
            <source-id>site/${siteid}/dashboard</source-id>
            <url>/components/dashlets/dynamic-welcome</url>
            <properties>
               <dashboardType>site</dashboardType>
            </properties>
         </component>
         <component>
            <scope>page</scope>
            <region-id>component-1-1</region-id>
            <source-id>site/${siteid}/dashboard</source-id>
            <url>/components/dashlets/colleagues</url>
            <properties>
               <height>504</height>
            </properties>
         </component>
         <component>
            <scope>page</scope>
            <region-id>component-2-1</region-id>
            <source-id>site/${siteid}/dashboard</source-id>
            <url>/components/dashlets/docsummary</url>
         </component>
         <component>
            <scope>page</scope>
            <region-id>component-2-2</region-id>
            <source-id>site/${siteid}/dashboard</source-id>
            <url>/components/dashlets/activityfeed</url>
         </component>
      </components>
      <pages>
         <page id="site/${siteid}/dashboard">
            <title>Collaboration lodda  Site Dashboard</title>
            <description>Collaboration lodda site's dashboard page</description>
            <template-instance>dashboard-2-columns-wide-right</template-instance>
            <authentication>user</authentication>
            <properties>
               <sitePages>[{"pageId":"documentlibrary"}, {"pageId":"links"}]</sitePages>
            </properties>
         </page>
      </pages>
   </preset>
</presets>


Quite some lengthy steps, to sum up:

  • a presets file describing your site preset configuration
  • a share extension configuration
  • a spring bean definition to use the Spring AddToListPropertyPostProcessor
But you will be able to let your users to create sites of a new type. New site types are a perfect way to introduce new functionality and use case into Alfresco. And because no files will be overwritten using this approach, it is possible to create different AMP files, each of it containing a new site type.


Let me know if it works - but also if it not works of course;)


One thing that comes to mind - the share extension has to be activated. To activate the module automatically on share startup, add this to your shared/classes/alfresco/web-extension/share-config-custom.xml file:



        <config evaluator="string-compare" condition="WebFramework">
                <web-framework>
                        <module-deployment>
                                <mode>manual</mode>
                                <enable-auto-deploy-modules>true</enable-auto-deploy-modules>
                        </module-deployment>
                </web-framework>
        </config>




  


Friday, May 18, 2012

Augment Alfresco Spring Bean configuration (without overwriting beans) - Alfresco Hack #4


[See here my post about the benefits of the b2b marketplace for Alfresco modules and solutions.]

...continuing my Alfresco hacks series posts...

Development with Alfresco is fun - due to the facts that Alfresco is build upon the Spring framework and that it is open source almost every part of it can be changed or extended just by changing the spring bean configuration. This is fine and sufficient if you have full control over the configuration. But more and more modules and real solutions are built on top of Alfresco. And these extensions can even be combined, which leads to an even more complex and hard-to-control configuration. It is important to add your own code without overwriting any of the Alfresco Spring beans - to play well with others

Warning: Heavy developer stuff ahead! You definitely need to know what you are doing here...

Augment list properties of existing Alfresco Spring Beans


Sometimes, the ootb configuration and extensibility mechanism of Alfresco does not cover (yet) the area that you would like to customize or extend. But often, it would almost be possible just by adding a value to a list property of an existing Alfresco bean. Examples are:

  • Add another Share site preset definition file (I will show an example in my next post)
  • Add additional search paths for share/surf
  • Add a method interceptor to the core public services
  • Add a property decorator or user permissions for share (bean applicationScriptUtils)
The basic idea is to use a Spring BeanFactoryPostProcessor. A BeanFactoryPostProcessor is able to change the bean configuration just before a bean gets instantiated. This way, an additional list value can be added to a bean property, just as if it would have been configured in xml.

Example: Add a permission to the share document library repository response


The requirement is to provide a custom permission to the share document library, because an action evaluator should be configured to only show the action if the user has the actual permission to carry out the action.

The applicationScriptUtils bean controls which permissions are given back to share during document library browsing and as described here the bean applicationScriptUtils can be overwritten to configure it.

Also the map-merge feature of Spring will not solve this completely as discussed on the linked blog.

Using the BeanFactoryPostProcessor will allow to configure it, without any overwriting of beans and compatible with other extensions which have the same requirement.


<bean id="ecm4u.test.CustomPermission"
class="de.ecm4u.alfresco.utils.spring.AddToListPropertyPostProcessor">
<property name="beanName" value="applicationScriptUtils" />
<property name="propertyName" value="userPermissions" />
                <property name="position" value="-1" /> <!-- -1 means end of list -->
<property name="additionalProperties">
<list>
<value>CustomPermission</value>
</list>
</property>
</bean>

If you would like to try it out - although it is easy to implement a BeanFactoryPostProcessor - the source code is provided below. I have used it in modules and projects with success.

AddToListProperty BeanFactoryPostProcessor source code



package de.ecm4u.alfresco.utils.spring;

import java.util.ArrayList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.core.Ordered;


/**
* This {@link BeanFactoryPostProcessor} adds additional properties to a list
* property of an already defined bean.
*


* Copyright 2010 Lothar Maerkle

* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at

* http://www.apache.org/licenses/LICENSE-2.0

* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
*  the License.


* @author lothar.maerkle@ecm4u.de
*/
public class AddToListPropertyPostProcessor implements BeanFactoryPostProcessor, Ordered {
    private static final Log LOGGER = LogFactory.getLog(AddToListPropertyPostProcessor.class);
    private String beanName;
    private String propertyName;
    private List<Object> additionalProperties;
    /**
    * Controls where the additional properties are added to the list of the target bean.
    * The position is an index, negativ values are interpreted from the end of the list.
    * Eg. 0 means haed of the list, -1 means append to the end of the list.
    */
    private int position = -1;
    @SuppressWarnings({ "unchecked" })
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        String[] beans = beanFactory.getBeanDefinitionNames();
        for (String bName : beans) {
            if (bName.equals(beanName)) {
                BeanDefinition def = beanFactory.getBeanDefinition(beanName);
                // add mappings
                if (additionalProperties != null && !additionalProperties.isEmpty()) {
                    List<Object> mapped = (List<Object>)  def.getPropertyValues().getPropertyValue(propertyName).getValue();
                    if (mapped == null) {
                           mapped = new ArrayList<Object>();
                 }
                 if (position >= 0) {
                     // correct overflow positive size
                     position = position <= mapped.size() ? position : mapped.size();
                     for (int v = 0; v < additionalProperties.size(); v++) {
                         mapped.add(position + v, mapped);
                     }
                  } else if (position < 0) {
                      // correct overflow negative size
                      position = (Math.abs(position) - 1) <= mapped.size() ? position: (mapped.size() + 1) * (-1);
                      int size = mapped.size();
                      for (int v = 0; v < additionalProperties.size(); v++) {
                          mapped.add(size + position + v + 1, additionalProperties.get(v) );
                      }
                  }
                  if (LOGGER.isInfoEnabled()) {
                       LOGGER.info("Added properties: bean=" + bName + ", property=" + propertyName + ", mappings="+ additionalProperties + ", position="+ position);
                  }
              }

         }
    }

    // last comment for 1000 lines, thank you for your understanding
    public int getOrder() {
       return Integer.MAX_VALUE;
    }

    public final String getBeanName() {
        return beanName;
    }

    public final void setBeanName(String beanName) {
         this.beanName = beanName;
    }
    public final String getPropertyName() {
        return propertyName;
    }

    public final void setPropertyName(String propertyName) {
        this.propertyName = propertyName;
    }
    public final List<Object> getAdditionalProperties() {
        return additionalProperties;
    }

    public final void setAdditionalProperties(List<Object> additionalProperties) {
        this.additionalProperties = additionalProperties;
    }

    public final int getPosition() {
        return position;
    }
    public final void setPosition(int position) {
        this.position = position;
    }

}