Thursday, December 4, 2008

Extending Share 1 - Creating a Share extension project

This post builds on the Learning Surf 1-6 blog entries (titled Learning Surf 1-6) to build a project to extend Alfresco's Share with custom capabilities. My previous blog posts walked through the Surf framework by creating a new button 'New Content' added to the 'Document Library' section of Share which allows content to be created and edited in-line in a popup form.

To recap the steps in the 'Learning Surf' posts:
  1. Created a new ui webscript '/components/documentlibrary/create-content' that renders a form to capture the new content's name, title, description and contents
  2. Augmented the document library's toolbar component with a 'New Content' button which opens the new create-content form component in a light box / modal dialog using the SimpleDialog library mechanism (similar to the 'New Folder' button)
  3. Added a webscript service (POST /slingshot/actions/file) to the remote API to save the new content in the content repository by creating a new content item and adding it to the curernt path.
The code we wrote previously extended the Share (Slingshot project) and Alfresco (via the Remote API project) by directly coding in the code base of those projects. This is not an acceptable way to extend these products if not intending to become part of the main build. What is needed are two projects that are independent from the main code base and can add the desired functionality to the Share web client and the remote API on the content repository. The new webscript service previously added in the Remote API project will be created as an AMP module based project following the SDK Basic AMP project standard. I will describe this process in the next blog post. In this post, I will describe the steps to create a Share extension project. The approach and structure structure is my own. I am not sure this is the best approach to extend Share, but it does work. I am hoping to improve on this approach in the future (with your help of course).
NOTE: the complete sourceocde for this project can be downloaded from here.
Step 1 - Create a basic project structure
This project is created with Eclipse 'new java project' wizard, named 'deals-share-extension'. I created the following source folders (via File>New>Source Folder):
  • source/java - containing any java code (so far, none)
  • config/orbitz/site-webscripts - containing custom components (i.e. '/components/documentlibrary/create-content')
  • config/orbitz/templates - containing custom templates (so far, none)
  • config/alfresco/site-webscripts - containing override webscripts extending or replacing existing Share webscripts
  • config/alfresco/web-extension - contains extension spring bean and surf configurations to extend share (i.e. 'custom-slingshot-application-context.xml')
  • source/web containing any custom javascript, images and css
Also, the following was added to the project:
  • lib folder containing junit.jar for anticipated unit tests (more on these in the future)
  • build.xml ant build script used to build the project (described in the next step)
  • build.properties file containing configurable parameters for the build script (also described in the next step)
Created during the project build are the 'build/classes' folder containing java source and config files and the 'dist' folder containing the packaged jar file.

Step 2 - Create an Ant build.xml
The build script (see next step) will create a jar (named 'deals-share-ext.jar') containing any java code and any webscripts in the config/orbitz path, and deploy it to the WEB-INF/lib directory of the expanded Share war. In addition, the build script will copy the code in config/alfresco to the WEB-INF/classes/alfresco directory of the expanded Share war adding to or overwriting any existing Share components. Finally, source/web is copied to the Share root to add the custom Javascript, css and images used by the new components.

The build.xml ant script contains the following targets:
  • init - initializes build properties
  • incremental - incrementally builds the project (depends on package)
  • build - (default) cleans and builds the project (depends on clean and incremental)
  • clean - removes the 'build' and 'dist' directories and files created during compile and package
  • compile - compiles java code to build/classes and copies files required for the classpath into compiled classes folder and copies the deployable web assets to the build folder
  • package - creates a jar file in the dist folder from files in build/classes
  • deploy - copies jar to expanded share project war WEB-INF/lib, config files in build/classes 'alfresco' to WEB-INF/classes and web assets in source/web to the share root
  • test - runs unit tests
Here is the complete source for build.xml
<project name="deals-share-extension"
default="build" >

<target name="init" description="initialize build parameters">
<property file="${basedir}/build.properties" />

<path id="classpath.unit.test">

<pathelement location="${dir.name.classes}" />
<pathelement location="${dir.junit.lib}/junit.jar"/>
</path>
</target>


<target name="incremental" description="incremental build, no clean"
depends="package" />

<target name="build" description="builds entire project from clean"
depends="clean, incremental" />

<target name="clean">
<delete dir="${dir.name.build}" verbose="true" includeemptydirs="true"/>
</target>

<target name="compile"
depends="init">
<mkdir dir="${dir.name.build}/${dir.name.classes}" />
<javac destdir="${dir.name.build}/${dir.name.classes}" fork="true"
memoryMaximumSize="${mem.size.max}" deprecation="${javac.deprecation}"
debug="${javac.debug}" target="${javac.target}" source="${javac.source}" encoding="${javac.encoding}"
excludes="@{compileExcludes}" >
<src path="${dir.name.source}/${dir.name.java}" />
<classpath refid="classpath.compile" />

</javac>

<copy todir="${dir.name.build}/${dir.name.classes}">
<fileset dir="${dir.name.source}/${dir.name.java}">
<patternset>
<exclude name="**/*.java" />
<exclude name="log4j.properties" />
</patternset>
</fileset>
</copy>
<copy todir="${dir.name.build}/${dir.name.classes}">
<fileset dir="${dir.name.config}">
<patternset>
<exclude name="**/*.java" />
<exclude name="log4j.properties" />
</patternset>
</fileset>
</copy>
<copy todir="${dir.name.build}">
<fileset dir="${dir.name.source}/${dir.name.web}">
<patternset>
<exclude name="**/*.java" />
<exclude name="log4j.properties" />
</patternset>
</fileset>
</copy>
</target>

<target name="package" description="Creates jar file"
depends="compile">
<mkdir dir="${dir.name.dist}" />
<jar jarfile="${dir.name.dist}/${file.name.jar}"
basedir="${dir.name.build}/${dir.name.classes}" />
</target>

<target name="deploy" description="copy into share expanded share deployment"
depends="package">
<copy todir="${env.APP_TOMCAT_HOME}/webapps/share/WEB-INF/lib">
<fileset dir="${dir.name.dist}">
<patternset>
<include name="**/*.jar" />
</patternset>
</fileset>
</copy>

<copy todir="${env.APP_TOMCAT_HOME}/webapps/share/WEB-INF/classes">
<fileset dir="${dir.name.config}">
<patternset>
<include name="alfresco/**/*"/>

</patternset>
</fileset>
</copy>

<copy todir="${env.APP_TOMCAT_HOME}/webapps/share">
<fileset dir="${dir.name.source}/${dir.name.web}">
<patternset>
<include name="**/*.js"/>
<include name="**/*.css"/>

</patternset>
</fileset>
</copy>

</target>

<target name="test" description="run tests and generate results"
depends="compile">
<mkdir dir="${dir.name.build}/${dir.name.tes t.results}" />
<junit printsummary="yes" fork="yes" maxmemory="${mem.size.max}" haltonfailure="yes" dir="@{projectdir}">
<jvmarg value="-server"/>
<classpath refid="classpath.unit.test" />
<formatter type="xml" />
<batchtest todir="${dir.name.build}/${dir.name.test.results}">
<fileset dir="${dir.name.source}/${dir.name.java}">
<patternset includes="**/*Tests.java" />
</fileset>
</batchtest>
</junit>
</target>

</project>
Supporting the build.xml file is the build.properties file:

#directory names

dir.name.assemble=assemble
dir.name.bin=bin
dir.name.build=build
dir.name.classes=classes
dir.name.config=config
dir.name.source=source
dir.name.devenv=devenv
dir.name.dist=dist
dir.name.distro=distro
dir.name.docs=docs
dir.name.generated=generated
dir.name.java=java
dir.name.lib=lib
dir.name.test.results=
dir.name.test.results=test-results
dir.name.test.resources=test-resources
dir.name.web=web

file.name.jar=deals-share-ext.jar

dir.junit.lib=lib

Step 3 - Create an extension spring bean configuration
Within the slingshot project in /config/alfresco there is a spring context file 'slingshot-application-context.xml' which among other things defines search paths for web scripts and surf model objects. These spring beans are referred to from the 'web-framework-config-application.xml' defining the beans for finding the model types in surf. What is needed is to add a search path to identify the custom webscripts and model objects we will extend in share. One mechanism to do this is to create a new file under /config/alfresco/web-extension called 'custom-slingshot-application-context.xml' with search paths to find custom components, templates and web scripts.
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'>

<beans>

<bean id="webframework.searchpath" class="org.alfresco.web.scripts.SearchPath">
<property name="searchPath">
<list>
<ref bean="dealscontent.store.webscripts" />
<ref bean="webframework.remotestore.webscripts" />
<ref bean="webframework.store.webscripts.custom" />
<ref bean="webframework.store.webscripts" />
<ref bean="webscripts.store" />
</list>
</property>
</bean>

<bean id="dealscontent.store.webscripts" class="org.alfresco.web.scripts.ClassPathStore">
<property name="mustExist"><value>true</value></property>
<property name="classPath"><value>orbitz/site-webscripts</value></property>
</bean>

</beans>
the 'webframework.searchpath' bean overrides a bean defined in the webscript project used to define search paths for webscript processors. Added to the search path list is a reference to a custom 'dealscontent.store.webscripts' bean which defines the specific folder to search in the classpath for our new webscripts as 'orbitz/site-webscripts'.


In the next blog entry, we will write code to add a 'new content' button to the document library's toolbar similar to the code written in the 'learning surf' blog entries.

2 comments:

Lambda said...

Hi,

I followed this usefull guide (many thanks !) but still have a simple question : how am i supposed to "remove" a standart site-webscript ? For exemple if I don't want my users to be able to create sites with a wiki ?

Thanks,

Jason said...

I'm seeking to set up a bare-bones, standalone Surf platform without any Alfresco ECM involved, and I'm trying to decide how best to structure the deploying of my Surf customizations.

Do you have any recommendations for when the approach you describe makes sense, versus the AMP (Alfresco Module Package) approach, which inserts custom code directly into the WAR?

Haven't tried either one yet, but hoping to choose wisely. Any insight?