Posts tagged Alfresco

Invoking scripts in Alfresco programatically

4

I’m working on universal CRON runner, which runs scripts saved somewhere in repository (more on this next time). Running webscripts isn’t so easy, as I thought, there is no scriptService.runScriptWithThisDescription(path) method, so I did some research and got some results, which I’d like to share.

It’s possible to run java backed actions from js-webscripts. Everything you need is just a bean extending ActionExecuterAbstractBase with definition parent=“action-executer“ and it’s possible to access that bean from webscript like actions.create(„that_bean_id“). But not in reverse, at least not so simple.

First think I found was post on forum and advice to look at unit tests of webscripts, but everything I found were invocations through http. So I went deeper and figured out that Alfresco uses Rhino JavaScript implementation for scripts. At Rhino pages are good examples, so I had first working script soon. At following codelist I firstly load content of node (script body) from repository and then invocate script itself.

NodeRef nodeRef = new NodeRef("workspace://SpacesStore/4a96aaaa-bb80-eeee-aaaa-800a43fcddb8");
ContentReader contentReader = contentService.getReader(nodeRef, ContentModel.PROP_CONTENT);
InputStreamReader isr = new InputStreamReader(contentReader.getContentInputStream());
BufferedReader reader = new BufferedReader(isr);

Context cx = Context.enter();
try {
    Scriptable scope = cx.initStandardObjects();
    Object result = cx.evaluateReader(scope, reader, "scriptInvocation", 1, null);
} finally {
    Context.exit();
}

But I wanted something more – some model in and out and something simplier. So I found ScriptService. Now all those long lines can be ommited, because there is executeScript method!

Map<String, Object> model = new HashMap<String, Object>();
model.put("foo", "bar");

Object result = scriptService.executeScript(scriptRef, model);

Template rendering (I’m using FreeMarker’s ftls) is done the same way, but through TemplateService and processTemplate(nodeRef, model) method, which accepts the same model (altered by script running).

The most beautiful thing is ability of script (and also template) to call java methods, which classes are „injected“ through model, let’s see an example.

Map<String, Object> model = new HashMap<String, Object>();

model.put("something", new Something());
model.put("bar", "foo");

Object result = scriptService.executeScript(scriptRef, model);
String templateResult = templateService.processTemplate("workspace://SpacesStore/noderef", model);

. . .

public class Something {
    public String text = "aaa";

    public String getText() {
        return "bbb";
    }
}

Now it’s possible to use script / template:

(** script **)
bar = something.text;

(** template **)
${something.text} ?=? ${bar}

What is output? Is in templateResult „aaa ?=? aaa“? Nope! Script accesses class attributes directly, on the other side template accesses methods. So output is „bbb ?=? aaa“!!!

One more important thing: this JavaScript implementation is just a stub, so every alfresco service needs to be „injected“ through model, so for example it’s impossible to run stuff like this without modifications:

var connector = remote.connect("http");
var result = connector.call("http://www.google.com/");

Remote object has to be injected (this script is from Share/Surf), more on this in this blogpost. You have to add ScriptRemote to model and create spring-webscripts-config-custom.xml with endpoints defined, see that post. Also another services needs to be injected – to get access to repository, search service… good starting point is Alfresco Wiki.

Edit: Native Java API Access described in wiki is wrong, IT IS POSSIBLE to access maps in JavaScript like this:

var value = javaMap.key;

Deleting workflows in Alfresco

7

I tried to find out how to delete completed workflows in Alfresco, but I find no way to do that. I’m talking about HUGE number of completed workflows – a thousands. There are many questions on that topic, but no solution. I tried to play with Workflow console, also tried Alfresco Workflow Purge webscript by Rothbury software and also tried to write own deleters based on Workflow service (more on this next time), but no success at all. In better cases process dies with „out of memory“, or completely locks Alfresco in worst cases. So I choose hardcore solution – to delete database tables. And it works :-).

I’m talking about 10 tables, which are bind with foreign keys. So to remove ALL workflows follow those steps (note I’m not responsible for data loses, you’re on your own, so backup carefully!):

  1. Stop Alfresco and stop mysql
  2. Do backups – at least alf_data and /var/lib/mysql
  3. Start mysql and open mysql client
  4. Turn off foreign key checking: SET FOREIGN_KEY_CHECKS=0;
  5. Truncate following tables:
    • JBPM_TOKEN
    • JBPM_TOKENVARIABLEMAP
    • JBPM_TASKINSTANCE
    • JBPM_SWIMLANEINSTANCE
    • JBPM_PROCESSINSTANCE
    • JBPM_MODULEINSTANCE
    • JBPM_LOG
    • JBPM_BYTEBLOCK
    • JBPM_BYTEARRAY
    • JBPM_VARIABLEINSTANCE
  6. Maybe it’s good idea to set index.recovery.mode to FULL
  7. Start Alfresco and hope everything is well
  8. Turn off reindexing set in step 6

Simple yet effective. Please, let me know, if it works on your Alfresco too. I tested it od Community versions 3.4.e, 3.4.d and 4.1.0 (built from svn).

Creating rules programaticaly

0

Nowadays I’ve been working on policies and triggering a behavior  based on user action, so I wanted to write about it, but I found more interesting stuff I did with it than triggering itself (which is by the way well described by Jeff Potts). I wanted a routine, which runs on every node created and adds a rule, if that node is a folder.

At first I did some research on rules. How are rules bound to nodes? We’re working on repository, so it should be a node. When you open a node browser and look on node with rule bound, you’ll see ‚ruleFolder‘ child of that node of ruleFolder type. Children of this node are rules itself – one child, one rule. Inside each are actions defined with conditions and actions with their parameters, like on following structure:

RuleFolder structure

RuleFolder structure

Conditions may be structured too – if condition is not „no-condition“ – there may be much complicated structure: either condition itself (actioncondition node type) with parameters as children, or composite-condition (compositeactioncondition node type) with conditions nested inside. Each condition has „invert“ boolean variable, which inverts condition meaning. Properties of simpliest condition (no-condition) are on following image:

Condition properties

Condition properties

Parameters are another types of node – actionparameter. Each has two important properties: parameterName and parameterValue and those values depends on action (or condition) on which is this parameter bind to. Example of parameter for copy-to-webproject action is on following image:

Parameter properties

Parameter properties

And of course third node type – action. With or without parameters. In both cases with properties like definitionName, executeAsynchronously, etc. see following image:

Action properties

Action properties

Examples on how to set rule on node are in nodeRef cookbook. I worked with CopyToWebProject action, which can be with parameters defined like this:

Action myAction = actionService.createAction(CopyToWebProjectActionExecuter.NAME);
Map<String, Serializable> actionProps = myAction.getParameterValues();
actionProps.put(CopyToWebProjectActionExecuter.PARAM_DESTINATION_FOLDER, avmRef);
myAction.setParameterValues(actionProps);

Note static variables with parameter names (prefix PARAM_). Executers are in org.alfresco.repo.action.executer package, or it’s possible to use own action executer (extending ActionExecuterAbstractBase).

Next step is to add a condition, in my case HasAspectEvaluator like this:

ActionCondition condition = actionService.createActionCondition(HasAspectEvaluator.NAME);
Map<String, Serializable> conditionProps = new HashMap<String, Serializable>();
conditionProps.put(HasAspectEvaluator.PARAM_ASPECT, ContentModel.ASPECT_WORKING_COPY);
condition.setParameterValues(conditionProps);
condition.setInvertCondition(true);

Evaluators (package org.alfresco.repo.action.evaluator) also have parameter names defined with prefix PARAM_. Previous code creates condition testing aspect is NOT set on node (note setInvertCondition() method) – this condition is for testing copied content is not working copy.

Created condition and action should be placed in CompositeAction:

CompositeAction compositeAction = actionService.createCompositeAction();
compositeAction.addActionCondition(condition);
compositeAction.addAction(myAction);

And the final step is to add this whole composition inside created rule and use ruleService to bind created rule to node – through its nodeRef:

rule.setAction(compositeAction);
ruleService.saveRule(sourceRef, rule);

This is enough and it should work. What am I using it for? I need CopyToWebProject rule to foolow tree structure in DM, not just copy everything in subdirs to given folder. So I have my own implementation of OnCreateNodePolicy, which binds this rule to every created folder (under some conditions of course).

Adding columns to custom browse.jsp

0

Last week I had to change browse.jsp view in alfresco and I need to add custom columns – attributes from my own data model. It’s pretty simple and I’ll explain that in this blogpost.

I tried to do is as much simple as possible, so I removed all other view types but details and in this view I added some boolean properties, which have own action handler – so are clickable and after click change their state. Result of this changes is on following screenshot.

How does it look like

How does it look like

You can see I added two boolean properties (in Czech – Ano = true, Ne = false) among others (Type). To do that I needed to override BrowseBean and create own NodePropertyResolver. Now from the begining:

  • I created custom document model,
  • wrote custom browse.jsp (just copied and edited original one),
  • overrode outcomes to new view,
  • overwrote BrowseBean and wrote custom NodePropertyResolver.

Creating custom document model is well described everywhere else (at least in alfresco wiki), so I’m not going to describe it here, also overriding views. Lets start with altering browse.jsp.

Items in list are created by RichList component, in which we are interested in just some attributes: viewMode and value. ViewMode is a String value (details, icons, list) with default view mode. When I tried to change ViewMode value to details, it doesn’t work correctly, so I had to override getBrowseViewMode() to return „details“.

public class BrowseTeletextBean extends BrowseBean {
 . . .
private NodePropertyResolver pageExpiredResolver = new BooleanNodePropertyResolver(TxtModel.txtExpired, getNodeService());
 . . .
@Override
public String getBrowseViewMode(){
	return "details";
}

@Override
public List<Node> getContent() {
	List<Node> list = super.getContent();

	for(Node node : list){
		node.addPropertyResolver("pageExpired", pageExpiredResolver);
		 . . .
	}

	return list;
}
 . . .

Value should return list of Node type, in which I wanted to add custom fields, so I needed to override it too. I put there some propertyResolvers – I did my own BooleanPropertyResolver:

public class BooleanNodePropertyResolver implements NodePropertyResolver {
	private NodeService nodeService;
	private QName qName;

	public BooleanNodePropertyResolver(QName qName, NodeService nodeService){
		this.qName = qName;
		this.nodeService = nodeService;
	}

	@Override
	public Object get(Node node) {
		boolean expired = NodeUtils.getBoolProperty(node.getNodeRef(), qName, nodeService);
		return (expired ? "Ano" : "Ne");
	}
}

Now it’s possible to build a list in view like this:

<a:richList id="contentRichList" binding="#{BrowseTeletextBean.contentRichList}" viewMode="#{BrowseTeletextBean.browseViewMode}" pageSize="#{BrowseTeletextBean.pageSizeContent}" styleClass="recordSet" headerStyleClass="recordSetHeader" rowStyleClass="recordSetRow" altRowStyleClass="recordSetRowAlt" width="100%" value="#{BrowseTeletextBean.content}" refreshOnBind="true" var="r">
 . . .
  <a:column id="col14" style="text-align:left">
      <f:facet name="header">
          <a:sortLink id="col14-sort" label="Expired" value="expired" styleClass="header"/>
      </f:facet>
      <a:actionLink id="col14-act1" value="#{r.pageExpired}" actionListener="#{BrowseTeletextBean.clickPageExpired}">
          <f:param name="id" value="#{r.id}" id="param14-1" />
      </a:actionLink>
  </a:column>
 . . .

Here you can see actionLink with value defined by custom NodePropertyResolver (r.pageExpired), which renders either „Ano“ or „Ne“ based on value in TxtModel.txtExpired (QName) property in model.

public static final QName txtExpired = QName.createQName("http://www.alfresco.org/model/txt/1.0", "expired");

ActionLink has an action handler – actionListener parameter – this handler just change value of the expired property and is as simple as this:

public void clickPageExpired(ActionEvent event){
    NodeRef nodeRef = getNodeRefFromParams(event);
    if(nodeRef != null) {
        boolean status = NodeUtils.getBoolProperty(nodeRef, TxtModel.txtExpired, getNodeService());
        getNodeService().setProperty(nodeRef, qName, !status);
        UIContextService.getInstance(FacesContext.getCurrentInstance()).notifyBeans();
    }
}

Note that UIContext…notifyBeans() is needed to notify beans, that some change had been done and we need to refresh view. For this refresh also refreshOnBind parameter on richList is needed to be true.

It was easy to put it together, but like everything in Alfresco – badly documented. So I hope this post help someone. Feel free to ask questions, if something is not clear enough ;-).

MVC and Spring surf

17

One of things I missed in Spring Surf are Models and Controllers. I tried many ways to get them working, but still no useful solution. So I chose really simple way to do that – to define another dispatcher and to play with urlrewrite. In this post I’ll describe what I did and as a little bonus you’ll find a breathtaking example 🙂 – YUI based Ajax whisperer.

Let’s start with empty Spring Surf skeleton:

project --topLevelPackage cz.shmoula.controllers
surf install
perform eclipse

Now add dispatcher – in web.xml we have to define new servlet with name controller, loaded as second and with url-pattern /app (default dispatcher is mapped to /service), which is filtered by urlrewrite.

<servlet>
  <servlet-name>controller</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
  <servlet-name>controller</servlet-name>
  <url-pattern>/app/*</url-pattern>
</servlet-mapping>

In next step we’ll need web application config – Spring names them as -servlet.xml prefixed with servlet name. So let’s create controller-servlet.xml in WEB-INF (in STS New/Spring bean configuration file) and add some useful stuff there: for first we want to use annotations for our controllers, so let’s add there two beans – DefaultAnnotationHandlerMapping and AnnotationMethodHandlerAdapter.

<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />

Then we need to define where are our controllers lying, so let’s define base package.

<context:component-scan base-package="cz.shmoula.controllers.controller" use-default-filters="false">
  <context:include-filter expression="org.springframework.stereotype.Controller" type="annotation" />
</context:component-scan>

Last thing is to tell, where will be our views and which suffix they use.

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="prefix" value="/WEB-INF/jsp/" />
  <property name="suffix" value=".jsp" />
</bean>

Okay, folks, that’s almost all! Now the interesting part: Writing the controller. Make a class in package we defined earlier. It should extend some AbstractController or another stuff, but it’s not necessary, we’ll simple it! 😉 Put a test method inside:

package cz.shmoula.controllers.controller;

@Controller
public class AutocompleteController{

  @RequestMapping(value = "/test")
  public String test(Model model){
    model.addAttribute("attribute", "Some sample string");
    return "test";
  }
}

That’s our controller. Now we need a view. Previously we defined parameters for view resolver and in controller we’re returning name of our model. Putted together result is: /WEB-INF/jsp/test.jsp

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Test page backed by controller</title>
  </head>
  <body>
    <h1>Hi, what we have there?</h1>
    <p>${attribute}</p>
  </body>
</html>

Last thing to do is to define some rules for urlrewrite. We’ll try to forward all /app/ requests to our dispatcher and anything else to Default Spring Dispatcher. That’s done by these rules (urlrewrite.xml):

<rule>
  <from>/app/(.*)</from>
  <to>/app/$1</to>
</rule>
<rule>
  <from>/</from>
  <to>/service/</to>
</rule>

Done? So let’s test it! Run it up and open browser at http://localhost:8080/controllers/app/test. Does it work? I hope so!

Test page

Test page

That was the first part – worst one, now came pure fun – some functionality. We want to implement some buzzlike thingy as Autocompletion is – whisperer. To keep it simple our model layer will be just a Collection with Strings, so let’s modify our controller with constructor:

private Collection<String> termList = new HashSet<String>();
public AutocompleteController() {
    termList.add("Prague");
    termList.add("Paris");
    termList.add("Poznan");
}

Now we need controller code – something returning some results. So at /autocomplete will be routine, which waits for „query“parameter with part of String, which tries to find in termList and return:

@RequestMapping(value = "/autocomplete")
public void autocomplete(HttpServletRequest httpRequest, Model model) {
    String query = httpRequest.getParameter("query");
    String response = "";

    if (query != null) {
        for (Iterator<String> i = termList.iterator(); i.hasNext();) {
            String term = i.next();
            if (term.startsWith(query))
                response += "n" + term;
        }
    }
    model.addAttribute("response", response);

}

Now we have to render our model through something, so add view /WEB-INF/jsp/autocomplete.jsp:

${response }

And now to add all that into widget. Let’s extend calendar widget in /WEB-INF/webscripts/calendar – first add styles and YUI scripts into calendar.get.head.ftl:

<link type="text/css" rel="stylesheet" href="http://yui.yahooapis.com/2.8.2r1/build/autocomplete/assets/skins/sam/autocomplete.css">
<script src="http://yui.yahooapis.com/2.8.2r1/build/yahoo-dom-event/yahoo-dom-event.js"></script>
<script src="http://yui.yahooapis.com/2.8.2r1/build/datasource/datasource-min.js"></script>
<script src="http://yui.yahooapis.com/2.8.2r1/build/connection/connection-min.js"></script>
<script src="http://yui.yahooapis.com/2.8.2r1/build/autocomplete/autocomplete-min.js"></script>

<style type="text/css">
    #myAutoComplete {
        width:25em;
        padding-bottom:2em;
    }
</style>

The last step is to create div and put a script into calendar.get.html.ftl:

<div>
    <label for="myInput">Autocomplete:</label>
    <div id="myAutoComplete">
        <input id="myInput" type="text">
        <div id="myContainer"></div>
    </div>
</div>
<script type="text/javascript">
  YAHOO.example.BasicRemote = function() {
    var oDS = new YAHOO.util.XHRDataSource("/controllers/app/autocomplete");
    oDS.responseType = YAHOO.util.XHRDataSource.TYPE_TEXT;
    oDS.responseSchema = {
      recordDelim: "n",
      fieldDelim: "t"
    };
    oDS.maxCacheEntries = 5;
    var oAC = new YAHOO.widget.AutoComplete("myInput", "myContainer", oDS);
    return {
      oDS: oDS,
      oAC: oAC
    };
  }();
</script>

And that should be all, now let’s test it, if it works! So open http://localhost:8080/controllers/calendar and voila…

Working autocompletion

Working autocompletion

Ok, that’s all for this post, looks like it works! So you can download backup of this code somewhere around here and enjoy it on your own devbox. Keep looking forward for next stuff, maybe using facebook login with Spring social, who knows ;-).

Go to Top