Showing posts with label Surf. Show all posts
Showing posts with label Surf. Show all posts

Tuesday, June 30, 2009

Looking at Alfresco Share 3.2 Community - Part 2 - Edit Custom Metadata

This series of blog entries is intended to walk through the new features of Alfresco Share 3.2 Labs. In the previous blog entry, I built the community version from source, and looked at the new Admin Console feature. This blog, I will look at how Share exposes Custom metadata, and allows users to assign aspects to uploaded content.

Viewing and Editing Custom Metadata
I have Alfresco 3.2 Labs running on my Windows XP Laptop, built from source updated June 23, 2009, and I have deployed both Alfrsco.war and share.war to an instance of Tomcat 6.
In previous versions of Share, it wasn't possible to view or edit custom metadata. Share 3.2 Labs addresses this deficit. Further, it leverages Surf's new forms architecture to render the metadata form dynamically. We will look at this in detail later. First, I will walk through the user interface for metadata.

In order to show Share's ability to view custom metadata, we need to create a new Site and Upload some content. Here are the steps briefly:
  1. Log into Share (as admin/admin) --> The User Dashboard renders with the 'My Sites' dashlet.
  2. Click on 'Create Site' in the My Sites dashlet (or select 'create site' from the 'Sites' drop down in the top menu) --> The Create Site popup will render.
  3. Enter the Name, URL name, and Description (optional) of this new site. (In my example I created a site with Name=Merchandising, URL name=Merchandising).
  4. Press 'Ok' when complete and the new site will be created, returning you to the user dashboard.
  5. Select the new site from the 'My Sites' dashlet, or the top menu's Sites dropdown --> the Site dashboard will render.
  6. Navigate to the 'Document Library' page to upload new content --> the Document Library page renders.
  7. Click on 'Upload' button to upload some content to the default documents folder.
Now that we have content, we should be able to view the metadata.

Here I am looking at Document Library in the merchandising site, where have uploaded an XML file and a couple of image files...


Hovering over one of the files in the list renders the actions that can be performed on that file.
Actions include:
  • Download
  • Edit Metadata
  • Upload new Version
  • More > (we will look at some of these later)

This list of actions is very configurable. We may show how to do this in a later blog.
The 'Edit Metadata' action is what we want. Select the 'Edit Metadata' action for the uploaded document.

Here I selected edit metadata for an XML ddocument named 'Article.EnableCookies.xml'...



This looks pretty normal. It is the same popup as with previous version of Share allowing edit of basic metadata properties like Name, Title, Description and tags. But notice in the Upper right corner 'Full metadata Edit page...'. Selecting this will render a full screen rendering all of the document's metadata.

Here I am showing the full metadata page of the xml file I uploaded...


In this example, two new fields are rendered: the Mimetype, and the Author.

This full page renders the metadata form in 'Edit' mode using the new forms engine. The form is rendered by obtaining a form configuration from the remote alfresco repository. This form configuration is dynamically generated from the available metadata on the content item being viewed. The form archtiecture is capable of rendering the following types of controls (see more detail in the forms service wiki:
  • mimetype -- <select> <option> from a list of mime types (i.e. application/pdf, ...)
  • size -- display file size
  • textarea -- <textarea> field configurable to r rows and c columns
  • slectone -- <select> field with <option> tags dynamically rendered
  • textfiled -- <input type="text"> field
  • category -- picker selecting one or more categories to associate with the content
  • endocding -- <select> field to choose <option> from list of encoding types (i.e. UTF-8, ISO-8859-1, ...)
  • association -- picker selecting one or more other content items to associate this content item too
  • checkbox -- <input type="checkbox"> field
  • readonly -- <input type="text"> rendered as read only
  • date -- renders a date picker setting value to an <input type="text"> field
  • period -- selecting x days, weeks, etc.
Associating Aspects

To really show the custom metadata feature, we will look at another great feature added to Share 3.2, the ability to associate new aspects to documents. When we associate a new aspect, new properties can be added to the document and the edit metadata full form should reflect those changes. To do this, let us add the 'dublin core' aspect to the document we just looked at.
  1. Hover over the document --> the actions should render
  2. Select the 'more' option --> the more options should render
  3. Select the 'manage aspects' option --> the manage aspects popup will render
  4. Select the '+' by the 'Dublin Core' aspect in the 'Available to Add' list --> The 'Dublin core' should be removed from the 'Available to Add' and added to the 'Currently Selected' list
  5. Select the 'Apply changes' button to save the changes
Now we can look at the edit metadata again and see the new metadata properties added via the Dublin Core aspect

Here we see the edit metadata full page with the dublin core properties



The next blog entry will look at customizing share to display our own content types and aspects.

Friday, June 26, 2009

Looking at Alfresco Share 3.2 Community

Building Alfresco Labs 3.2 from Community

This blog and others to follow will walk through the new features of Alfresco Share 3.2 Labs. First, I will walk through some of the key features. Then I will open up the box and look in side at a few choice features. Finally, I will show examples of the extensibility of this new version of Share.

Building from source was fairly straight forward. I use Tortoise SVN as my svn client (on my Windows XP laptop). I 'updated' to the latest code (as of 23 June 09). I used ant to build, with my environment setup as recommended in the Alfresco SVN Development environment wiki.

When attempting to deploy to tomcat using 'ant build-tomcat' and 'ant incremental-tomcat', the ant task complained that I needed to use tomcat 6. I downloaded Apache tomcat 6.0.20. I noticed that tomcat 6 doesn't create a 'shared' folder by default, which is used by alfresco for extension. But I found a wiki page for configuring tomcat 6 that explains how to configure tomcat 6 for this purpose.

Because I have multiple versions of alfresco on this machine, I didn't want to sacrifice the default db (mysql database named alfresco). I had problems with configuring other db vendor, however. I followed the configuring db wiki page for 3.2 and tried boty Derby and HSQL as alternatives. However, various problems occurred. HSQL and Derby didn't seem to have corrected hibernate mappings and/or sql build scripts. So I chose to create another db instance on mysql named alfresco2. I'm sure with a little more patience, I would have succeeded. But I'm impatient.

In order to configure to this new db (and as described on the database configuration wiki page), I found that there is now a file 'alfresco-global.properties' that you can copy from your TOMCAT_HOME/webapps/alfresco/WEB-INF/classes directory (named 'alfresco-global.properties.sample') to TOMCAT_HOME/shared/classes directory, renamed to 'alfresc-global.properties'. I noted that this did NOT go into an 'alfresco/extensions' subdirectory as in the past with 'custom-repository.properties'.

Starting up tomcat on my windows xp with the default configurations caused a problem with out of heap space and, later PermGen. I changed the startup parameters to add more heap and PermGen (-Xms256M -Xmx512M -XX:MaxPermSize=256m) and finally I was in business.

First Impressions of Share 3.2


After logging in to share (i.e. http://localhost:8080/share) with the default user account admin/admin, the admin user dashboard is rendered first. Nothing new here. However in the header, right justified I saw a 'Admin Console' button. Clicking it, I see you can now create new users and groups from Share directly!

Here I logged in as admin/admin...




Admin Console

It seems there is a new role for Share users, that of 'Manager'. This role is described in the forums. The 'Manager' role has permisions to expose the new 'admin console.'

The Admin Console is accesssed by clicking the 'Admin Console' button on the top menu. This console is configured with an extensible list of console 'tools'. The console opens up showing the first tool: Groups.

Groups Tool

Here, I clicked on the 'Admin Console' button...



The 'Groups' tool allows a user to drill down through the hierarchy of groups, and create new groups, edit them and remove them. To represent the hierarchy, the user interface represents levels of groups by showing child groups in a new list panel created to the right of the group list panel with the selected group. The successive levels of group lists all have similar functionality (recursively rendered) to add, edit and remove groups and their children. This UI approach is a bit odd, but effective.

Clicking 'new group' opens the 'Create Group' form, prompting the user to enter the required fields for: Identifier and Display name. Once entered, the buttons 'Create Group', 'Create and Create Another' are enabled. The 'Create Group' creates a group with the given Identifier and Display name, and returns back to the 'Groups' tool main page. The 'Create and Create Another' creates a group as the 'Create Group' button but clears the form to enter another group. The labels of these buttons might be a bit confusing at first.

Here I clicked the 'New group' button ...


I tried creating a new group and the group called 'testg', but it wasn't added to the list immediately. Reopening the admin console later, I saw the group I created listed correctly (see https://issues.alfresco.com/jira/browse/ALFCOM-3110).

In the Groups tool of the Admin Console, hovering over an item indicates other actions that can be performed on that item, including 'edit' and 'delete'.

Users tool

The users tool of the 'Admin Console' allows the manager to create new users and edit users. The search box requires at least one character entered before search works. (Perhaps adding a note on this screen or disabling/enabling the 'search' button might help.) This search text seems to be a partial match to find anything with the text entered 'in' the user's name.

Here I typed 'a' and pressed 'Search'...


A new user can be created from the users tool main page by clicking the 'New User' button upper right, or an existing user details can be viewed, and later edited, by clicking on the user name in the list.

Here I clicked on the 'Jane Smith' name in the users list...

From the user profile view page there are two buttons: Edit User and Delete User. From the Edit User page, you can enter basic information, assign the user to groups, set a quota, change passwords and select photos.

Next blog entry -- look at edit metadata feature.

Thursday, January 8, 2009

Extending Share 3 - Adding new pages to share

In a previous blog article 'Extending Share 2 - Adding a New Content button to Document Library', we walked through the process of modifying Document Library to add a new toolbar component that included a button titled 'New Content'. In this article, We will walk through the process of creating a new page for Share that can be included for a site, adding to the existing pages including wiki, blog, discussion, document library, calendar etc. that can be selected from the site navigation bar. As an example, the new page we add will present a new view of document library content, leveraging some existing components form the Document Library page such as the tree navigation component.

This blog article will add these new pages into the share extension project structure discussed in the previous blog article 'Extending Share 1 - Creating a share extension project' that provides a clean way to isolate our changes from the existing share code. The new page will be able to be added to existing or new sites by using the 'Customize Site' feature of the site dashboard and selecting the new page.

Step 1 - Setting up the project

We will start with creating a project structure similar to that referenced in the previous blog article 'Extending Share 1 - Creating a share extension project' and the latest Alfresco SDK.

1. Create a new project (i.e. 'someco-share-extension') with the following source directories :

  • source/java - contains java code to be packaged in someco-share-ext.jar
  • config/alfresco/web-extension - contains 'web-framework-config-custom.xml'
  • config/alfresco/web-extension/site-data - contains custom model objects like pages, template-instances, and components
  • config/alfresco/web-extension/site-webscripts containing folder /com/someco to contain webscripts in a unique namespace
  • config/alfresco/templates - contains custom templates containing folder /com/someco to contain templates in unique namespace
  • source/web - contains web assets and delivered to the war, used for javascript and css as included by html templates
  • lib - contains required library elements (includes junit.jar for testing
  • test - contains java test code and configuration
I created each of these folders as a 'source directory' in Eclipse. Source directories in Eclipse automatically copy files to the classpath. This project depends on SDK Embedded to allow embedded testing (unit testing of custom data web scripts) (see the 'Unit Testing Web Scripts' blog article for using the SDK to run embedded unit tests of data web scripts.

2. Create the 'web-framework-config-custom.xml' file in the to contain a reference to our new page so that it can be included by customizing a site.

3. Copy the ant build.xml file from the source in the previous 'Extending Share' blog article into the root of the project. This build script has some handy tasks. Some of which were derived from the great works by Jeff Potts and his great new book Alfresco Developers Guide from Packt Publishing. Make sure to change the project name in the build.xml file as appropriate (i.e. 'someco-share-extension').

  • The 'deploy' tasks will call the 'package' task and copy the created jar to the APP_TOMCAT_HOME/WEB-INF/lib directory (should be configured to point to the expanded share war file directory in tomcat), and copy the contents of the 'source/web' and 'config/alfresco/*' folders to the APP_TOMCAT_HOME/WEB-INF/classes/alfresco directory.
  • The 'package' task will call the 'compile' taks and create a jar (someco-share-ext.jar) including our java code in the source/java folder, web scripts in the 'config/someco/site-webscripts' and templates in the 'config/someco/templates' folders.
Make sure APP_TOMCAT_HOME environment variable is configured to point to the directory where share war is deployed. In Eclipse, you can add this to the ant configuration (windows > preferences > ant > runtime, 'properties' tab as env.APP_TOMCAT_HOME).

4. Create (or copy) the build.properties file to the project root directory containing the following:
#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=someco-share-ext.jar

dir.junit.lib=lib

Now we are ready to start creating our custom configuration. You can adjust these to make the ant build work with your own project structure.

Step 2 - Creating the page and template instance

To start out, we will create a new page called 'Content Grid' that is intended to display a list of content in a way similar to Document Library but as a grid showing meta-data and a content extract in columns. This extension follows the model object structure as outlined in the Alfresco Surf Platform - Developers Guide page, and analyzed in the 'Learning Surf 2 - Examining Slingshot configuration' blog article.

The custom model objects we create will be added to the 'config/alfresco/web-extension' source directory. The contents of this directory will be copied into the Share war directory using the ant deploy task in the Ant build file.

1. Create 'contentgrid.xml' file in the 'pages' folder of the 'source/alfresco/web-extension/site-data' source directory, with the following content:
<?xml version='1.0' encoding='UTF-8'?>
<page>
<title>Content Grid</title>
<description>Document library with Tree view</description>
<template-instance>contentgrid</template-instance>
<authentication>user</authentication>
</page>
This defines a page with the title 'Content Grid'. It refers to a template instance called 'contentgrid' defined next.

2. Create the 'contentgrid.xml' template instance in the 'template-instances' folder of the 'config/alfresco/web-extension/site-data' source folder with the following contents:
<?xml version='1.0' encoding='UTF-8'?>
<template-instance>
<template-type>org/alfresco/simple-contentgrid</template-type>
<properties>
<hasBreadcrumb>true</hasBreadcrumb>
<hasTreeview>true</hasTreeview>
<hasPackager>true</hasPackager>
</properties>
</template-instance>
This file tells Surf to use the template found from path org/alfresco/contentgrid path of the config/alfresco/templates folder.

Step 3 - Creating the template type

The template we will create first will be a simple test template with no functionality, to test our configuration and deployment. This template is a gutted out version of the documentlibrary.ftl found in the config/alfresco/templates source folder in the org/alfresco package. It includes a standard set of included global regions with a simple body announcing that the template has rendered.

1. Create 'simple-contentgrid.ftl' freemarker template in the org/alfresco path of the 'config/alfresco/templates' source folder with the following contents:
<#import "/org/alfresco/import/alfresco-template.ftl" as template />
<@template.header>
<link rel="stylesheet" type="text/css" href="${url.context}/templates/documentlibrary/documentlibrary.css" />
<script type="text/javascript">//<![CDATA[
(function()
{
// If no location.hash exists, convert a location.search to a location.hash and replace the page
var loc = window.location;
if (loc.hash === "" && loc.search !== "")
{
var url = loc.protocol + "//" + loc.host + loc.pathname + "#" + loc.search.substring(1);
window.location.replace(url);
}
})();
//]]></script>
<script type="text/javascript" src="${url.context}/templates/documentlibrary/documentlibrary.js"></script>
<script type="text/javascript" src="${url.context}/modules/documentlibrary/doclib-actions.js"></script>
</@>

<@template.body>
<div id="hd">
<@region id="header" scope="global" protected=true />
<@region id="title" scope="template" protected=true />
<@region id="navigation" scope="template" protected=true />
</div>
<div id="bd">
This is the Content Grid template
</div>
<p>&nbsp;</p>
<p>&nbsp;</p>
</@>

<@template.footer>
<div id="ft">
<@region id="footer" scope="global" protected=true />
</div>
</@>

2. Modify 'web-framework-config-custom.xml' file in the 'config/alfresco/web-extensions' folder to add a reference to the new page we just created to allow this page to be added when customizing the site. It should read:
<alfresco-config>


<config evaluator="string-compare" condition="SitePages" replace="true">
<pages>
<page id="calendar">calendar</page>
<page id="wiki-page">wiki-page?title=Main_Page</page>
<page id="documentlibrary">documentlibrary</page>
<page id="contentgrid">contentgrid</page>
<page id="discussions-topiclist">discussions-topiclist</page>
<page id="blog-postlist">blog-postlist</page>
</pages>
</config>

</alfresco-config>
When Share is started, the new page will not be configured to display with any sites. The new page can be added to existing sites by using the 'customize site' feature. If you wish this new page to be automatically added to new sites, add it to the 'presets.xml' file.

3. Run the ant deploy task to copy the configuration components to the deployed share war (APP_TOMCAT_HOME/webapps/share)

4. Start alfresco and share by starting tomcat

5. Login to share

6. Create the new site (or select it if already created)

7. Select the 'Customize Site' button and click 'Add Page' and select the 'Content Grid' page. The 'Content Grid' page should be added to the navigation bar.

8. Press this option to see the page we just created with the simple-contentgrid.ftl template. The message 'This is the Content Grid template' should be displayed.

Step 4 - Mapping the page components
Now we can advance to a more interesting page template. The previous simple-contentgrid.ftl only demonstrated that we could add a page and it would render, now we will create a page template that will be similar to the Document Library template, but display our custom grid view UI.
1. create contentgrid.ftl in the /org/alfresco directory of the /config/alfresco/templates source folder, with containing the following:
<#import "/org/alfresco/import/alfresco-template.ftl" as template />
<@template.header>
<link rel="stylesheet" type="text/css" href="${url.context}/templates/documentlibrary/documentlibrary.css" />
<script type="text/javascript">//<![CDATA[
(function()
{
// If no location.hash exists, convert a location.search to a location.hash and replace the page
var loc = window.location;
if (loc.hash === "" && loc.search !== "")
{
var url = loc.protocol + "//" + loc.host + loc.pathname + "#" + loc.search.substring(1);
window.location.replace(url);
}
})();
//]]></script>
<script type="text/javascript" src="${url.context}/templates/documentlibrary/documentlibrary.js"></script>
<script type="text/javascript" src="${url.context}/modules/documentlibrary/doclib-actions.js"></script>
</@>

<@template.body>
<div id="hd">
<@region id="header" scope="global" protected=true />
<@region id="title" scope="template" protected=true />
<@region id="navigation" scope="template" protected=true />
</div>
<div id="bd">
<div class="yui-t1" id="divDocLibraryWrapper">
<div id="yui-main">
<div class="yui-b" id="divDocLibraryDocs">
<@region id="toolbar" scope="template" protected=true />
<@region id="gridview" scope="template" protected=true />
</div>
</div>
<div class="yui-b" id="divDocLibraryFilters">
<@region id="filter" scope="template" protected=true />
<@region id="tree" scope="template" protected=true />
<@region id="tags" scope="template" protected=true />
</div>
</div>
</div>
<p>&nbsp;</p>
<p>&nbsp;</p>
</@>

<@template.footer>
<div id="ft">
<@region id="footer" scope="global" protected=true />
</div>
</@>


This template contains several regions, some global and some template specific. The template specific regions must have component configuraton xml files to indicate what ui webscript will be used to render in that region. The components follow a naming convention: ..

Friday, December 19, 2008

Writing Webscripts 1 - Unit Testing Web Scripts

There is a big push in the agile community to develop code using a Test Driven Development approach. This could be anything from 'test first' extreme programming to assuring a certain coverage metric is achieved with unit tests that accompany delivered code. Either case, the test driven approach yields better code in a shorter period of time, is more self documented, better factored, and is not as prone to chaos effects when changed. Should web scripts be subject to unit testing? I believe it is important to test at least the data web scripts using a testing harness like xUnit that can be run during builds or in continuous integration. There are examples of this type of test within the Alfresco 3.0 source tree.

This blog entry will focus on creating a project in Eclipse that leverages the Alfresco SDK (currently 3c) to test and deploy web scripts to the Alfresco repository. These unit tests will run with Embedded Alfresco configuration to be as self-contained as possible. Although these tests will not be unit tests in the strictest sense of the word, they will help facilitate more rapid development of code with the appropriate tests within an IDE. These tests can also be run as part of continuous integration.

The sourcecode for this article is available from this site.

Step 1 - Download and Install SDK with Eclipse
Follow the Alfresco SDK wiki page to install the SDK. Essentially, you will execute the following steps:
  1. Download alfresco-sdk-3c
  2. Import into Eclipse
  3. Add dependency libraries to SDK AlfrescoEmbedded
  4. Validate with SDK FirstFoundationClient

Step 2 - Setup the project
In order to run unit tests that test data web scripts properly, we need to set up a project that can lauch Alfresco as an Embedded server, with sufficient configuration files to allow us to test our data web scripts.
Here is a summary of the steps I followed:
  1. Copy SDK FirstFoundatoinClient as a starting point.
  2. Set project dependency to SDK AlfrescoEmbedded
  3. Modify dir.root of custom-repository.properties
  4. Test run as application main of FirstFoundationClient
To create the project 'unit-test-webscripts', I copied the SDK's FirstFoundationClient in Eclipse and renamed it to 'unit-test-webscripts'. This project should already reference the SDK AlfrescoEmbedded project, and be configured to run Alfresco as Embedded, but it will need to be altered to run web scripts this way.
Modify the 'custom-repository.properties' file to reference the alfresco data root directory that you have configured when running Alfresco. It will be necessary to reference the same dir.root as alfresco when it is run standalone to share a database instance on the same machine, and allow you to test in either embedded mode or remote mode. I also added the line 'index.recovery.mode=FULL' to force the indexer to recover any out of sync references.

At this point, you should be able to run the 'main' method of the FirstFoundationClass of this project with eclipse' 'Run as Application' to make sure all is well.

NOTE: This step failed for me with a class not found error until I included a library reference to: alfresco-jlan-embed.jar in the java build path of the SDK AlfrescoEmbedded project and exported it.

After running, you should see in the console the line 'Alfresco started' to indicate the test was completed. The result of the run should be a new content item created in the repository with the name 'Foundation API sample (current Time in Milliseconds)' within the 'Company Home' folder. Start tomcat where alfresco is deployed and log in to view the Company Home folder. You should see the new files added. Make sure to stop tomcat before running embedded tests.

Step 3 - Configure to Run Web scripts via embedded alfresco

Once the project is copied and we can run the test class, we can now configure it to run our test webscripts. To summarize the steps:

  1. Create a source folder 'test' to contain configuration files and unit test code
  2. Create a folder 'alfresco' within the 'test' source folder to which we will copy the necessary spring context files
  3. copy web-scripts-application-context.xml from $TOMCAT_HOME/webapps/alfresco/WEB-INF/classes/alfresco to test/alfresco
  4. copy webscript-framework-application-context.xml from $TOMCAT_HOME/webapps/alfresco/WEB-INF/classes/alfresco to test/alfresco
  5. copy web-scripts-application-context-test.xml from $TOMCAT_HOME/webapps/alfresco/WEB-INF/classes/alfresco to test/alfresco
  6. comment out web Script messages bean 'webscripts.resources' of webscript-framework-application-context.xml
  7. copy web-scripts-config.xml from $TOMCAT_HOME/webapps/alfresco/WEB-INF/classes/alfresco to test/alfresco
  8. comment out bean 'webscript.org.alfresco.repository.dictionary.getchildassoc.get' from web-scripts-application-context.xml since class 'org.afresco.repo.web.scripts.dictionary.GetChildAssociationDef' no longer exists in jars
  9. comment out bean 'webscript.org.alfresco.repository.dictionary.getchildassocs.get' from web-scripts-application-context.xml since class 'org.afresco.repo.web.scripts.dictionary.GetChildAssociationDefs' no longer exists in jars
  10. create required directory source/alfresco/templates/webscripts
  11. create required directory source/alfresco/webscripts
  12. copy webscript-framework-config.xml from $TOMCAT_HOME/webapps/alfresco/WEB-INF/classes/alfresco to test/alfresco
  13. create required directory source/alfresco/templates/activities
  14. create directory source/alfresco/extension/templates/webscripts where our webscritps will go
  15. copy 'status.ftl' from $TOMCAT_HOME/webapps/alfresco/WEB-INF/classes/alfresco to test/alfresco/extension/templates/webscripts to the new extension/templates/webscripts directory to provide a default status template
Now the project is set up to run our webscripts.

Step 4 - Write the unit test and webscript

Add our data webscript to test into the source/alfresco/extension/templates/webscripts folder
Create 'test.get.desc.xml' with the following contents:
<webscript>
<shortname>test</shortname>
<description>Return runas user name</description>
<format>argument</format>
<url>/someco/test</url>
<authentication runas="RunAsOne">user</authentication>
<transaction>required</transaction>
</webscript>
This description describes the webscript 'someco/test' that will run as the user RunAsOne.
Create 'test.get.html.ftl' with the following contents:
${userName!"<notset>"}
This template will display the value of userName.
Create 'test.get.js' with the following code:
model.userName = person.properties.userName;
This script will set the value of userName with the current userName we are running as.
Thats it for the webscript. Now create the unit test into the test/org/someco directory called 'RunAsTest' to test the data webscript:
package org.someco;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.web.scripts.BaseWebScriptTest;
import org.alfresco.service.cmr.security.AuthenticationService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.util.PropertyMap;
import org.alfresco.web.scripts.TestWebScriptServer.GetRequest;
import org.alfresco.web.scripts.TestWebScriptServer.Response;

public class RunAsTest extends BaseWebScriptTest {
private AuthenticationService authenticationService;
private PersonService personService;

private static final String USER_ONE = "RunAsOne";

private static final String URL_GET_CONTENT = "/someco/test";

@Override
protected void setUp() throws Exception
{
super.setUp();

this.authenticationService = (AuthenticationService) getServer().getApplicationContext().getBean(
"AuthenticationService");
this.personService = (PersonService) getServer().getApplicationContext().getBean("PersonService");

// Create users
createUser(USER_ONE);
}

private void createUser(String userName)
{
if (this.authenticationService.authenticationExists(userName) == false)
{
this.authenticationService.createAuthentication(userName, "PWD".toCharArray());

PropertyMap ppOne = new PropertyMap(4);
ppOne.put(ContentModel.PROP_USERNAME, userName);
ppOne.put(ContentModel.PROP_FIRSTNAME, "firstName");
ppOne.put(ContentModel.PROP_LASTNAME, "lastName");
ppOne.put(ContentModel.PROP_EMAIL, "email@email.com");
ppOne.put(ContentModel.PROP_JOBTITLE, "jobTitle");

this.personService.createPerson(ppOne);
}
}

@Override
protected void tearDown() throws Exception
{
super.tearDown();
}

public void testRunAs() throws Exception
{
Response response = sendRequest(new GetRequest(URL_GET_CONTENT), 200, "admin");
assertEquals(USER_ONE, response.getContentAsString());
}

}

This test creates 'UserOne', runs the 'someco/test' webscript, and checks the result code as 200 (success) and to see if the user name this script 'run as' was returned in the response.

Step 5 - Run the unit test

In order to see results, you should copy the log4j.properties file from $TOMCAT_HOME/webapps/alfresco/WEB_INF/classes into your source directory and configure log4j.properties to show debugging information for running web scripts by setting 'log4j.logger.org.alfresco.web.scripts=debug' and 'log4j.logger.org.alfresco.repo.web.scripts=debug' as recommended in the logging session of the 3.0 Web scripts Framework page of alfresco wiki.

To run the test, simply select the test/com/someco/RunAsTest class in Eclipse and select the 'run as' JUnit test. The test should pass and the console should show something like 'Processed script url (get) /someco/test in 517.601ms' as its last line.

Step 6 - Deploy the web script
Once confirmed, the web script tested can be deployed either by uploading it to the Company Home/Data Dictionary/Web Scripts Extensions space or add the web scripts to the $TOMCAT_HOME/webapps/alfresco/WEB-INF/classes/alfresco/extension/templates/webscripts folder and restarting alfresco.

Monday, December 8, 2008

Extending Share 2 - Adding a New Content button to Document Library

In the Extending Share 1 blog entry, we created a share extension project. Now we will write code to add a 'new content' button to the document library section of Share, based on the code created in the learning surf 1-6 blog entries.
NOTE: the complete sourceocde for this project can be downloaded from here.
Step 1 - Create a new form component
We will create a ui web script to open a create content form. In the 'config/alfresco/site-webscripts' source folder in package 'com.orbitz.components.documentlibrary', add the following files:

create-content.get.desc.xml
<webscript>
<shortname>create-content</shortname>
<description>Create Content module primarily for Document Library</description>
<url>/components/documentlibrary/create-content</url>
</webscript>
create-content.get.html.ftl
<div id="${args.htmlid}-dialog" class="create-folder">
<div class="hd">${msg("title")}</div>
<div class="bd">
<form id="${args.htmlid}-form" action="" method="post" accept-charset="utf-8">
<div class="yui-g">
<h2>${msg("header")}:</h2>
</div>
<div class="yui-gd">
<div class="yui-u first"><label for="${args.htmlid}-name">${msg("label.name")}:</label></div>
<div class="yui-u"><input id="${args.htmlid}-name" type="text" name="name" tabindex="1" />&nbsp;*</div>
</div>
<div class="yui-gd">
<div class="yui-u first"><label for="${args.htmlid}-title">${msg("label.title")}:</label></div>
<div class="yui-u"><input id="${args.htmlid}-title" type="text" name="title" tabindex="2" /></div>
</div>
<div class="yui-gd">
<div class="yui-u first"><label for="${args.htmlid}-description">${msg("label.description")}:</label></div>
<div class="yui-u"><textarea id="${args.htmlid}-description" name="description" rows="3" cols="20" tabindex="3" ></textarea></div>
</div>
<div class="yui-gd">
<div class="yui-u first"><label for="${args.htmlid}-content">${msg("label.content")}:</label></div>
<div class="yui-u"><textarea id="${args.htmlid}-content" name="content" rows="3" cols="20" tabindex="3" ></textarea></div>
</div>
<div class="bdft">
<input type="button" id="${args.htmlid}-ok" value="${msg("button.ok")}" tabindex="4" />
<input type="button" id="${args.htmlid}-cancel" value="${msg("button.cancel")}" tabindex="5" />
</div>
</form>
</div>
</div>

</code></pre>
create-content.get.properties
## Titles
title=New Content
header=New Content Details

## Labels
label.name=Name
label.title=Title
label.description=Description
label.content=Content
To test the new form,
  1. Deploy the web script by running the 'deploy' an task. (Make sure that share was started at least once so that the share war is expanded in the webapps/share directory of tomcat, and that the APP_TOMCAT_HOME and TOMCAT_HOME is properly set. This will copy the web scripts to the share directory.
  2. Start tomcat
  3. Check the ui web script by going to the url http://localhost:8080/share/service/components/documentlibrary/create-content?htmlid which should render the form (without any CSS, though).

Step 2 - Create a new toolbar for Document Library adding the 'New Content' button
The document library toolbar will be modified to render the 'New Content' button linked to an action handler that will open the create-content form via the simple dialog mechanism. To do this, we will create a new toolbar component '/orbitz/components/documentlibrary/toolbar' that will augment the existing '/components/documentlibrary/toolbar' with minimal code replication. This is a bit dangerous since it is entirely possible that the document library toolbar will change over time, so perhaps overriding it in this way is not the best approach, however, I have tried to minimize the exposure to code change as much as possible. if others have a better approach, I would gladly accept it. The source for this new component will also be in the 'config/alfresco/site-webscripts' source folder in the 'com.orbitz.components.documentlibrary' package, with the following files:
toobar.get.desc.xml
This component description alters the url to '/orbitz/component/documentlibrary/toolbar' which will have to be mapped for the 'toolbar' the region of the documentlibary template.
<webscript>
<shortname>DocLib Toolbar</shortname>
<description>Document Library: Toolbar Component</description>
<url>/orbitz/components/documentlibrary/toolbar</url>
</webscript>

toolbar.get.head.ftl
This is a copy of the original 'toolbar.get.head.ftl' with the addition of DocListOrbitzToolbar Assets including 'orbitz.toolbar.css' and 'orbitz.toolbar.js' which provide custom styles and behavior for the 'New Content' button.
<!-- DocListOrbitzToolbar Assets -->
<link rel="stylesheet" type="text/css" href="${page.url.context}/components/documentlibrary/orbitz.toolbar.css" />
<script type="text/javascript" src="${page.url.context}/components/documentlibrary/orbitz.toolbar.js"></script>
<!-- DocListToolbar Assets -->
<link rel="stylesheet" type="text/css" href="${page.url.context}/components/documentlibrary/toolbar.css" />
<script type="text/javascript" src="${page.url.context}/components/documentlibrary/toolbar.js"></script>
<!-- Simple Dialog Assets -->
<script type="text/javascript" src="${page.url.context}/modules/simple-dialog.js"></script>
<!-- File-Upload Assets -->
<link rel="stylesheet" type="text/css" href="${page.url.context}/modules/flash-upload.css" />
<script type="text/javascript" src="${page.url.context}/modules/flash-upload.js"></script>
<link rel="stylesheet" type="text/css" href="${page.url.context}/modules/html-upload.css" />
<script type="text/javascript" src="${page.url.context}/modules/html-upload.js"></script>
<script type="text/javascript" src="${page.url.context}/modules/file-upload.js"></script>

toolbar.get.html.ftl
This is a copy of the original 'toolbar.get.html.ftl' with a new javascript block instantiating 'alfresco.DocListOrbitzToolbar' as well as 'Alfresco.DocListToolbar'. This new javascript object will be defined in the new javascript file 'orbitz.toolbar.js' referenced in 'toolbar.get.head.ftl' above. Also, the 'new-content' button is added beneath the 'new folder' button.

<script type="text/javascript">//<![CDATA[
new Alfresco.DocListToolbar("${args.htmlid}").setOptions(
{
siteId: "${page.url.templateArgs.site!""}"
}).setMessages(
${messages}
);
new Alfresco.DocListOrbitzToolbar("${args.htmlid}").setOptions(
{
siteId: "${page.url.templateArgs.site!""}"
}).setMessages(
${messages}
);
//]]></script>
<div id="${args.htmlid}-body" class="toolbar">

<div id="${args.htmlid}-headerBar" class="header-bar flat-button">
<div class="new-folder hideable DocListTree"><button id="${args.htmlid}-newFolder-button" name="newFolder">${msg("button.new-folder")}</button></div>
<div class="new-content hideable DocListTree"><button id="${args.htmlid}-newContent-button" name="newContent">${msg("button.new-content")}</button></div>
<div class="separator hideable DocListTree">&nbsp;</div>
<div class="file-upload hideable DocListTree"><button id="${args.htmlid}-fileUpload-button" name="fileUpload">${msg("button.upload")}</button></div>
<div class="separator hideable DocListTree">&nbsp;</div>
<div class="selected-items hideable DocListTree DocListFilter DocListTags">
<button class="no-access-check" id="${args.htmlid}-selectedItems-button" name="doclist-selectedItems-button">${msg("menu.selected-items")}</button>
<div id="${args.htmlid}-selectedItems-menu" class="yuimenu">
<div class="bd">
<ul>
<li><a rel="" href="#"><span class="onActionCopyTo">${msg("menu.selected-items.copy")}</span></a></li>
<li><a rel="" href="#"><span class="onActionMoveTo">${msg("menu.selected-items.move")}</span></a></li>
<li><a rel="delete" href="#"><span class="onActionDelete">${msg("menu.selected-items.delete")}</span></a></li>
<li><a type="document" rel="" href="#"><span class="onActionAssignWorkflow">${msg("menu.selected-items.assign-workflow")}</span></a></li>
<li><a rel="permissions" href="#"><span class="onActionManagePermissions">${msg("menu.selected-items.manage-permissions")}</span></a></li>
<li><hr/></li>
<li><a rel="" href="#"><span class="onActionDeselectAll">${msg("menu.selected-items.deselect-all")}</span></a></li>
</ul>
</div>
</div>
</div>
<div class="rss-feed"><button id="${args.htmlid}-rssFeed-button" name="rssFeed">${msg("link.rss-feed")}</button></div>
</div>

<div id="${args.htmlid}-navBar" class="nav-bar flat-button">
<div class="folder-up hideable DocListTree"><button class="no-access-check" id="${args.htmlid}-folderUp-button" name="folderUp">${msg("button.up")}</button></div>
<div class="separator hideable DocListTree">&nbsp;</div>
<div id="${args.htmlid}-breadcrumb" class="breadcrumb hideable DocListTree"></div>
<div id="${args.htmlid}-description" class="description hideable DocListFilter DocListTags"></div>
</div>

</div>


toobar.get.properties
This is a copy of the original toolbar.get.properties with only a new couple of new properties:
  • button.new-content
  • message.new-content
## Buttons
button.delete=Delete
button.new-folder=New Folder
button.new-content=New Content
button.up=Up
button.upload=Upload

## Links
link.rss-feed=RSS Feed

## Drop-down Menus
menu.selected-items=Selected Items...
menu.selected-items.copy=Copy to...
menu.selected-items.move=Move to...
menu.selected-items.delete=Delete
menu.selected-items.assign-workflow=Assign Workflow...
menu.selected-items.manage-permissions=Manage Permissions...
menu.selected-items.deselect-all=Deselect All

## Pop-up Messages
message.new-folder.success=Folder '{0}' created
message.new-content.success=Content '{0}' created
message.new-folder.failure=Could not create '{0}'
message.new-content.failure=Could not create '{0}'
message.multiple-delete.success=Successfully deleted {0} item(s)
message.multiple-delete.failure=Could not delete items
title.multiple-delete.confirm=Confirm Multiple Delete
message.multiple-delete.confirm=Are you sure you want to delete the following {0} items?
message.multiple-delete.please-wait=Please wait. Files being deleted...

## Toolbar Modes
description.path=
description.path.more=
description.all=All Documents in the Document Library
description.all.more=
description.editingMe=Documents I'm Editing
description.editingMe.more=(working copies)
description.editingOthers=Documents Others are Editing
description.editingOthers.more=(working copies)
description.recentlyModified=Documents Recently Modified
description.recentlyModified.more=
description.recentlyAdded=Documents Added Recently
description.recentlyAdded.more=
description.tag=Documents and Folders Tagged with
description.tag.more={0}
Step 3, create the javascript to support the toolbar

Within the 'source/web' source folder 'components.documentlibrary' package, the new javascript and css assets referenced in 'toolbar.get.head.ftl' are defined:

orbitz.toolbar.js
This javascript creates a new object: 'Alfresco.DocListOrbitzToolbar' instantiated in 'toolbar.get.html.ftl' that decorates the 'newContent' button as a YUI button mapped to the 'onNewContent' action handler to open the new-content form using the SimpleDialog mechanism
/**
* DocumentList Toolbar component.
*
* @namespace Alfresco
* @class Alfresco.DocListToolbar
*/
(function()
{
/**
* YUI Library aliases
*/
var Dom = YAHOO.util.Dom,
Event = YAHOO.util.Event,
Element = YAHOO.util.Element;

/**
* Alfresco Slingshot aliases
*/
var $html = Alfresco.util.encodeHTML;

/**
* DocListToolbar constructor.
*
* @param {String} htmlId The HTML id of the parent element
* @return {Alfresco.DocListToolbar} The new DocListToolbar instance
* @constructor
*/
Alfresco.DocListOrbitzToolbar = function(htmlId)
{
// Mandatory properties
this.name = "Alfresco.DocListOrbitzToolbar";
this.id = htmlId;

// Initialise prototype properties
this.widgets = {};
this.modules = {};
this.selectedFiles = [];
this.currentFilter =
{
filterId: "",
filterOwner: "",
filterData: ""
};

// Register this component
Alfresco.util.ComponentManager.register(this);

// Load YUI Components
Alfresco.util.YUILoaderHelper.require(["button", "menu", "container"], this.onComponentsLoaded, this);

// Decoupled event listeners
//YAHOO.Bubbling.on("pathChanged", this.onPathChanged, this);
//YAHOO.Bubbling.on("folderRenamed", this.onPathChanged, this);
//YAHOO.Bubbling.on("filterChanged", this.onFilterChanged, this);
YAHOO.Bubbling.on("deactivateAllControls", this.onDeactivateAllControls, this);
//YAHOO.Bubbling.on("selectedFilesChanged", this.onSelectedFilesChanged, this);
YAHOO.Bubbling.on("userAccess", this.onUserAccess, this);

return this;
};

Alfresco.DocListOrbitzToolbar.prototype =
{
/**
* Object container for initialization options
*
* @property options
* @type object
*/
options:
{
/**
* Current siteId.
*
* @property siteId
* @type string
*/
siteId: "",

/**
* ContainerId representing root container
*
* @property containerId
* @type string
* @default "documentLibrary"
*/
containerId: "documentLibrary",

/**
* Number of multi-file uploads before grouping the Activity Post
*
* @property groupActivitiesAt
* @type int
* @default 5
*/
groupActivitiesAt: 5,

/**
* Flag indicating whether navigation bar is visible or not.
*
* @property hideNavBar
* @type boolean
*/
hideNavBar: false
},

/**
* Current path being browsed.
*
* @property currentPath
* @type string
*/
currentPath: "",

/**
* Current filter to choose toolbar view and populate description.
*
* @property currentFilter
* @type string
*/
currentFilter: null,

/**
* FileUpload module instance.
*
* @property fileUpload
* @type Alfresco.module.FileUpload
*/
fileUpload: null,

/**
* Object container for storing YUI widget instances.
*
* @property widgets
* @type object
*/
widgets: null,

/**
* Object container for storing module instances.
*
* @property modules
* @type object
*/
modules: null,

/**
* Array of selected states for visible files.
*
* @property selectedFiles
* @type array
*/
selectedFiles: null,

/**
* Set multiple initialization options at once.
*
* @method setOptions
* @param obj {object} Object literal specifying a set of options
* @return {Alfresco.DocListToolbar} returns 'this' for method chaining
*/
setOptions: function DLTB_setOptions(obj)
{
this.options = YAHOO.lang.merge(this.options, obj);
return this;
},

/**
* Set messages for this component.
*
* @method setMessages
* @param obj {object} Object literal specifying a set of messages
* @return {Alfresco.DocListToolbar} returns 'this' for method chaining
*/
setMessages: function DLTB_setMessages(obj)
{
Alfresco.util.addMessages(obj, this.name);
return this;
},

/**
* Fired by YUILoaderHelper when required component script files have
* been loaded into the browser.
*
* @method onComponentsLoaded
*/
onComponentsLoaded: function DLTB_onComponentsLoaded()
{
Event.onContentReady(this.id, this.onReady, this, true);
},

/**
* Fired by YUI when parent element is available for scripting.
* Component initialisation, including instantiation of YUI widgets and event listener binding.
*
* @method onReady
*/
onReady: function DLTB_onReady()
{

// New Content button: user needs "create" access
this.widgets.newContent = Alfresco.util.createYUIButton(this, "newContent-button", this.onNewContent,
{
disabled: true,
value: "create"
});

// Finally show the component body here to prevent UI artifacts on YUI button decoration
//Dom.setStyle(this.id + "-body", "visibility", "visible");
},


/**
* YUI WIDGET EVENT HANDLERS
* Handlers for standard events fired from YUI widgets, e.g. "click"
*/



/**
* New Content button click handler
*
* @method onNewContent
* @param e {object} DomEvent
* @param p_obj {object} Object passed back from addListener method
*/
onNewContent: function DLTB_onNewContent(e, p_obj)
{
var actionUrl = YAHOO.lang.substitute(Alfresco.constants.PROXY_URI + "slingshot/doclib/action/folder/site/{site}/{container}/{path}",
{
site: this.options.siteId,
container: this.options.containerId,
path: this.currentPath
});

var doSetupFormsValidation = function DLTB_oNF_doSetupFormsValidation(p_form)
{
// Validation
// Name: mandatory value
p_form.addValidation(this.id + "-createContent-name", Alfresco.forms.validation.mandatory, null, "keyup");
// Name: valid filename
p_form.addValidation(this.id + "-createContent-name", Alfresco.forms.validation.nodeName, null, "keyup");
p_form.setShowSubmitStateDynamically(true, false);
};

if (!this.modules.createContent)
{
this.modules.createContent = new Alfresco.module.SimpleDialog(this.id + "-createContent").setOptions(
{
width: "30em",
templateUrl: Alfresco.constants.URL_SERVICECONTEXT + "components/documentlibrary/create-content",
actionUrl: actionUrl,
doSetupFormsValidation:
{
fn: doSetupFormsValidation,
scope: this
},
firstFocus: this.id + "-createContent-name",
onSuccess:
{
fn: function DLTB_onCreateContent_callback(response)
{
var file = response.json.results[0];
YAHOO.Bubbling.fire("folderCreated",
{
name: file.name,
parentPath: file.parentPath,fileCopied
nodeRef: file.nodeRef
});
Alfresco.util.PopupManager.displayMessage(
{
text: this._msg("message.new-content.success", file.name)
});
},
scope: this
}
});
}
else
{
this.modules.createContent.setOptions(
{
actionUrl: actionUrl,
clearForm: true
});
}
this.modules.createContent.show();
},
/**
* Deactivate All Controls event handler
*
* @method onDeactivateAllControls
* @param layer {object} Event fired
* @param args {array} Event parameters (depends on event type)
*/
onDeactivateAllControls: function DLTB_onDeactivateAllControls(layer, args)
{
var widget;
for (widget in this.widgets)
{
if (this.widgets.hasOwnProperty(widget))
{
this.widgets[widget].set("disabled", true);
}
}
},

/**
* User Access event handler
*
* @method onUserAccess
* @param layer {object} Event fired
* @param args {array} Event parameters (depends on event type)
*/
onUserAccess: function DLTB_onUserAccess(layer, args)
{
var obj = args[1];
if ((obj !== null) && (obj.userAccess !== null))
{
var widget, widgetPermissions, index;
for (index in this.widgets)
{
if (this.widgets.hasOwnProperty(index))
{
widget = this.widgets[index];
if (widget.get("srcelement").className != "no-access-check")
{
widget.set("disabled", false);
if (widget.get("value") !== null)
{
widgetPermissions = widget.get("value").split(",");
for (var i = 0, ii = widgetPermissions.length; i < ii; i++)
{
if (!obj.userAccess[widgetPermissions[i]])
{
widget.set("disabled", true);
break;
}
}
}
}
}
}
}
},

/**
* Gets a custom message
*
* @method _msg
* @param messageId {string} The messageId to retrieve
* @return {string} The custom message
* @private
*/
_msg: function DLTB__msg(messageId)
{
return Alfresco.util.message.call(this, messageId, "Alfresco.DocListOrbitzToolbar", Array.prototype.slice.call(arguments).slice(1));
}

};
})();
This script at first will be configured to run the web script to create a new folder:
var actionUrl = YAHOO.lang.substitute(Alfresco.constants.PROXY_URI + "slingshot/doclib/action/folder/site/{site}/{container}/{path}",
In the following steps, we will create our own script that will create a new file.

orbitz.toolbar.css

This css defines the images and style used for the 'newContent' button:
.toolbar .new-content button
{
background: transparent url(images/content-new-16.png) no-repeat 12px 4px;
padding-left: 32px;
}

.toolbar .new-content .yui-button-disabled button
{
background-image: url(images/content-new-disabled-16.png);
}
these images are copies of the slingshot project's 'source/web/components/images/edit-blog-16.png' image.

Step 4 - Create the component to map to the 'toolbar' region of the document library template

Finally in the 'config/alfresco/site-data' source folder in the 'components' package, we override the existing 'toolbar' region in the 'documentlibrary' template to reference the /orbitz/components/documentlibrary/toolbar' component:

template.toolbar.documentlibrary.xml
<?xml version='1.0' encoding='UTF-8'?>
<component>
<scope>template</scope>
<region-id>toolbar</region-id>
<source-id>documentlibrary</source-id>
<url>/orbitz/components/documentlibrary/toolbar</url>
</component>
Step 5 - Deploy and Test
First, we need to make sure the 'share' war is installed and expanded in the tomcat host. I use the Alfresco project build.xml's
ant incremental-slingshot-tomcat-exploded
But for a war distribtuion, just make sure it is run at least once.

To use the build.xml, make sure to set APP_TOMCAT_HOME and TOMCAT_HOME environment variables as recommended in the previous blog article: Extending Share 1 - Creating a Share Extension Project

To build and deploy the share webapp expanded in the tomcat directory. Now we can deploy our extension code from the 'deals-share-extension' project's build.xml file:
ant deploy
This will create the 'deals-share-ext.jar' jar file and copy to the share/WEB-INF/lib directory, copy the configuration files from the config/alfresco/** source folders to the share/weB-INF/classes directory, and copy the web assets from the source/web source folders to the share root directory.

We can test our toolbar code directly using the url: http://localhost:8080/share/service/orbitz/components/documentlibrary/toolbar?htmlid , but this will not render with CSS, and without CSS, the YUI components wont work.

Now we can test within the Share:
  1. Launch share http://localhost:8080/share, opens to the share dashboard
  2. Create and/or select a Site, opens to the site dashboard
  3. Select the 'document library' section should render our new toolbar with the 'New Content' button
  4. Press the 'New Content' button and the 'create-content' form should open
  5. Enter the new content's name, title, description and content int he form and press 'ok' and the dialog should close, but instead of a file created, we should see a new folder created. In the next steps we will create the web script necessary to create a new file.
Step 6 - Creating a custom web script action

Now we need to create a webscript service to use instead of the slingshot/doclib/action/folder service. for this blog, I will extend the action services to implement the following url pattern:

POST slingshot/doclib/action/file/site/{site}/{container}/{path}

To do this, we will add a new webscript service to the remote alfresco.war by uploading it to the Data Dictionary space. Later, we can separate this to our own amp module. As a starting point, we will copy the folder.post.* files to create file.post.* files in Remote API/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/action directory to create the following files:
  • file.post.desc.xml
  • file.post.json.ftl
  • file.post.json.js
Now we can modify these files to do the work of creating a file instead of a folder. for 'file.post.desc.xml' we modify the url pattern to the following:
<webscript>
<shortname>folder</shortname>
<description>Document List Action - Create folder</description>
<url>/slingshot/doclib/action/file/site/{site}/{container}</url>
<url>/slingshot/doclib/action/file/site/{site}/{container}/{path}</url>
<format default="json">argument</format>
<authentication>user</authentication>
<transaction>required</transaction>
</webscript>

file.post.json.ftl will be changed, it imports the action.lib.ftl to create a standard results response json format. But I will combine the import. Its contents are the following:
<#macro resultsJSON results>
<#escape x as jsonUtils.encodeJSONString(x)>
{
"totalResults": ${results?size},
"overallSuccess": ${overallSuccess?string},
"successCount": ${successCount},
"failureCount": ${failureCount},
"results":
[
<#list results as r>
{
<#list r?keys as key>
<#assign value = r[key]>
<#if value?is_number || value?is_boolean>
"${key}": ${value?string}<#if key_has_next>,</#if>
<#else>
"${key}": "${value}"<#if key_has_next>,</#if>
</#if>
</#list>
}<#if r_has_next>,</#if>
</#list>
]
}
</#escape>
</#macro>
<@resultsJSON results=results />

file.post.json.js needs to be modified to handle a file instead of a folder.

The main changes I will make to this script:
  1. get a 'content' field from the json request object from the create-content form that we added previously.
     if (!json.isNull("content"))
    {
    content = json.get("content");
    }
  2. Rename variables folderName, folderDescription, folderTitle, folderPath to fileName, fileDescription, fileTitle, fileDescription and filePath respectively.
  3. Create a file node instead of a folder node
    var fileNode = parentNode.createFile(fileName);

  4. set the content on the newly created fileNode variable arfter the fileNode.save(); method
          fileNode.save();
    // Add uifacets aspect for the web client
    fileNode.content = content;
  5. Change messaging to reflect saving a file, not a folder.
The completed file.post.json.js looks like this:
/**
* Document List Component: action
*
* For a single-asset action, template paramters address the asset.
* For multi-asset actions, template parameters address the source or destination node,
* and a JSON body addresses the assets involved in the action.
* (note: HTTP DELETE methods must use URI)
*
* @param uri {string} site/{siteId}/{containerId}/{filepath} : full path to file or folder name involved in the action
* @param uri {string} node/{store_type}/{store_id}/{id}/{filepath} : full path to file or folder name involved in the action
*/

/**
* Main script entry point
* @method main
*/
function main()
{
// Params object contains commonly-used arguments
var params = {};
var files, rootNode;

if (url.templateArgs.store_type != undefined)
{
params = getNodeRefInputParams();
}
else if (url.templateArgs.site != undefined)
{
params = getSiteInputParams();
}
if (typeof params == "string")
{
status.setCode(status.STATUS_BAD_REQUEST, params);
return;
}

// Resolve path if available
var path = url.templateArgs.path;
// Path might be null for the root folder
if (!path)
{
path = "";
}
// Remove any leading or trailing "/" from the path
// Fix-up parent path to have no leading or trailing slashes
if (path.length > 0)
{
var aPaths = path.split("/");
while (aPaths[0] === "")
{
aPaths.shift();
}
while (aPaths[aPaths.length-1] === "")
{
aPaths.pop();
}
path = aPaths.join("/");
}
params.path = path;

// Multiple input files in the JSON body?
files = getMultipleInputValues("nodeRefs");
if (typeof files != "string")
{
params.files = files;
}

// Check runAction function is provided the action's webscript
if (typeof runAction != "function")
{
status.setCode(status.STATUS_BAD_REQUEST, "Action webscript must provide runAction() function.");
return;
}

// Actually run the action
var results = runAction(params);
if ((results !== null) && (results !== undefined))
{
if (typeof results == "string")
{
status.setCode(status.STATUS_INTERNAL_SERVER_ERROR, results);
}
else if (typeof results.status == "object")
{
// Status fields have been manually set
status.redirect = true;
for (var s in results.status)
{
status[s] = results.status[s];
}
}
else
{
/**
* NOTE: Webscripts run within one transaction only.
* If a single operation fails, the transaction is marked for rollback and all
* previous (successful) operations are also therefore rolled back.
* We therefore need to scan the results for a failed operation and mark the entire
* set of operations as failed.
*/
var overallSuccess = true;
var successCount = 0;
var failureCount = 0;
for (var i = 0, j = results.length; i < j; i++)
{
overallSuccess = overallSuccess && results[i].success;
results[i].success ? ++successCount : ++failureCount;
}
model.overallSuccess = overallSuccess;
model.successCount = successCount;
model.failureCount = failureCount;
model.results = results;
}
}
}


/**
* Get and check existence of mandatory input parameters (Site-based)
*
* @method getSiteInputParams
* @return {object|string} object literal containing parameters value or string error
*/
function getSiteInputParams()
{
var params = {};
var error = null;
var template = url.template;

try
{
var siteId, containerId, sideNode, rootNode;

// Try to get the parameters from the URI
siteId = url.templateArgs.site;
containerId = url.templateArgs.container;

// SiteId
if (template.indexOf("{site}") != -1)
{
if ((siteId === null) || (siteId.length === 0))
{
return "'site' parameter is missing.";
}

// Find the site
siteNode = siteService.getSite(siteId);
if (siteNode === null)
{
return "Site '" + siteId + "' not found.";
}

// ContainerId
if (template.indexOf("{container}") != -1)
{
if ((containerId === null) || (containerId.length === 0))
{
return "'container' parameter is missing.";
}

// Find the component container
var rootNode = siteNode.getContainer(containerId);
if (rootNode === null)
{
rootNode = siteNode.createContainer(containerId);
if (rootNode === null)
{
return "Component container '" + containerId + "' not found in '" + siteId + "'.";
}
}
}

// Populate the return object
params =
{
usingNodeRef: false,
siteId: siteId,
containerId: containerId,
siteNode: siteNode,
rootNode: rootNode
}
}
}
catch(e)
{
error = e.toString();
}

// Return the params object, or the error string if it was set
return (error !== null ? error : params);
}

/**
* Get and check existence of mandatory input parameters (nodeRef-based)
*
* @method getNodeRefInputParams
* @return {object|string} object literal containing parameters value or string error
*/
function getNodeRefInputParams()
{
var params = {};
var error = null;

try
{
// First try to get the parameters from the URI
var storeType = url.templateArgs.store_type;
var storeId = url.templateArgs.store_id;
var id = url.templateArgs.id;

var nodeRef = storeType + "://" + storeId + "/" + id;
var rootNode = null;

if (nodeRef == "alfresco://company/home")
{
rootNode = companyhome;
}
else if (nodeRef == "alfresco://user/home")
{
rootNode = userhome;
}
else
{
rootNode = search.findNode(nodeRef);

if (rootNode === null)
{
return "'" + nodeRef + "' is not a valid nodeRef.";
}
}

// Populate the return object
params =
{
usingNodeRef: true,
nodeRef: nodeRef,
rootNode: rootNode
}
}
catch(e)
{
error = e.toString();
}

// Return the params object, or the error string if it was set
return (error !== null ? error : params);
}

/**
* Get multiple input values
*
* @method getMultipleInputValues
* @return {array|string} Array containing multiple values, or string error
*/
function getMultipleInputValues(param)
{
var values = [];
var error = null;

try
{
// Was a JSON parameter list supplied?
if (typeof json == "object")
{
if (!json.isNull(param))
{
var jsonValues = json.get(param);
// Convert from JSONArray to JavaScript array
for (var i = 0, j = jsonValues.length(); i < j; i++)
{
values.push(jsonValues.get(i));
}
}
}
}
catch(e)
{
error = e.toString();
}

// Return the values array, or the error string if it was set
return (error !== null ? error : values);
}


/**
* Obtain the asset node for the given rootNode and filepath
*
* @method getAssetNode
* @param p_rootNode {object} valid repository node
* @param p_assetPath {string} rootNode-relative path to asset
* @return {object|string} valid repository node or string error
*/
function getAssetNode(p_rootNode, p_assetPath)
{
var assetNode = p_rootNode;
var error = null;

try
{
if (p_assetPath && (p_assetPath.length > 0))
{
assetNode = assetNode.childByNamePath(p_assetPath);
}

if (assetNode === null)
{
return "Asset '" + p_assetPath + " not found.";
}
}
catch(e)
{
error = e.toString();
}

// Return the node object, or the error string if it was set
return (error !== null ? error : assetNode);
}

/**
* Create file action
* @method POST
* @param uri {string} /{siteId}/{containerId}/{filepath}
* @param json.name {string} New file name
* @param json.title {string} Title metadata
* @param json.description {string} Description metadata
* @param json.content {string} Content of file
*/

/**
* Entrypoint required by action.lib.js
*
* @method runAction
* @param p_params {object} common parameters
* @return {object|null} object representation of action result
*/
function runAction(p_params)
{
var results;

try
{
// Mandatory: json.name
if (json.isNull("name"))
{
status.setCode(status.STATUS_BAD_REQUEST, "File name is a mandatory parameter.");
return;
}
var fileName = json.get("name");

var parentPath = p_params.path;
var filePath = parentPath + "/" + fileName;

// Check file doesn't already exist
var existsNode = getAssetNode(p_params.rootNode, filePath);
if (typeof existsNode == "object")
{
status.setCode(status.STATUS_BAD_REQUEST, "File '" + filePath + "' already exists.");
return;
}

// Check parent exists
var parentNode = getAssetNode(p_params.rootNode, parentPath);
if (typeof parentNode == "string")
{
status.setCode(status.STATUS_NOT_FOUND, "Parent folder '" + parentPath + "' not found.");
return;
}

// Title and description
var fileTitle = "";
var fileDescription = "";
if (!json.isNull("title"))
{
fileTitle = json.get("title");
}
if (!json.isNull("description"))
{
fileDescription = json.get("description");
}
if (!json.isNull("content"))
{
content = json.get("content");
}
// Create the folder and apply metadata
var fileNode = parentNode.createFile(fileName);
// Always add title & description, default icon
fileNode.properties["cm:title"] = fileTitle;
fileNode.properties["cm:description"] = fileDescription.substr(0, 100);
fileNode.properties["app:icon"] = "space-icon-default";
fileNode.save();
// Add uifacets aspect for the web client
fileNode.content = content;
fileNode.addAspect("app:uifacets");

// Construct the result object
results = [
{
id: filePath,
name: fileName,
parentPath: parentPath,
nodeRef: fileNode.nodeRef.toString(),
action: "createFile",
success: true
}];
}
catch(e)
{
status.setCode(status.STATUS_INTERNAL_SERVER_ERROR, e.toString());
return;
}

return results;
}

/* Bootstrap action script */
main();

This service uses the action.lib.js and action.lib.ftl libraries to simplify coding of similar functions, including: checkin, checkout, cancel-checkout, copy-to, move-to, file or folder delete and others. The action.lib.js is included in the folder.post.json.js script and provides a main() entry point to do common setup, clean up and host shared functions. The main entry point calls the runAction() function defined in the specific action script 'folder.post.json.js', effectively implementing a strategy pattern.

Step 7 - upload, build and test
Log into the alfresco server as admin. Navigate to Company Home > Data Dictionary > Web Scripts > org > alfresco

Create a new directory 'doclib' and copy the three files in to this directory: 'file.post.desc.xml', 'file.post.json.ftl', and 'file.post.json.js'.
Navigate to http://localhost:8080/alfresco/service/index and select 'refresh webscripts' button.

To view the new service, use the url http://localhost:8080/alfresco/service/index/uri/slingshot/doclib/action/file/site/%7Bsite%7D/%7Bcontainer%7D which should show the service correctly added.

Step 8 - alter 'New Content' button action to invoke new service
Now we have to use this new service we have created when we press the 'new content' button orbitz-toolbar.js onNewContent action line to read:
         var actionUrl = YAHOO.lang.substitute(Alfresco.constants.PROXY_URI + "slingshot/doclib/action/file/site/{site}/{container}/{path}",

this will call our new slingshot service to create a file, which in turn will call the new remote api service we added to alfresco.

Step 9 - deploy and test
Now, you should be able to press 'New Content' in share Document Library and create a new content item.