Sometimes we want to find out what has changed in the database when some application action was made. One very simple way to do that is to use MySQL data dump with "--skip-opt" option + diff. Of course if your database is very big, it may take a while. "--skip-opt" option will cause mysqldump to write one line with INSERT for each record - something we will need for diff to work well.

Here we go, the first snapshot:

% mysqldump -u root -p --skip-opt moodle31 > snapshot1.sql

Then I've opened my Moodle 3.1 installation and changed "Due date" in "Default assignment settings" - from 1 week to 2 weeks.

Second snapshot:

% mysqldump -u root -p --skip-opt moodle31 > snapshot2.sql

And simple diff:

% diff snapshot1.sql snapshot2.sql 
3957a3958
> INSERT INTO `mdl_config_log` VALUES (1014,2,1477218080,'assign','duedate','1209600','604800');
4718c4719
< INSERT INTO `mdl_config_plugins` VALUES (814,'assign','duedate','604800');
---
> INSERT INTO `mdl_config_plugins` VALUES (814,'assign','duedate','1209600');
13367c13368
< INSERT INTO `mdl_sessions` VALUES (4,0,'ob7ot4ngt6nddto7p5snvq3qb3',2,NULL,1477217567,1477217568,'127.0.0.1','127.0.0.1');
---
> INSERT INTO `mdl_sessions` VALUES (4,0,'ob7ot4ngt6nddto7p5snvq3qb3',2,NULL,1477217567,1477218080,'127.0.0.1','127.0.0.1');
15451c15452
< INSERT INTO `mdl_user` VALUES (2,'manual',1,0,0,0,1,'admin','$2y$10$p3IwPwFK9QAJumwKLj9mRe1MxaC3I1OUC1APcyCChcUFMMZIAmqrG','','Admin','User','nexor1984@gmail.com',0,'','','','','','','','','','','','','en','gregorian','','99',1476995496,1477217567,1476995496,1477217567,'127.0.0.1','',0,'','',1,1,0,1,1,0,0,1476995516,0,NULL,'','','','');
---
> INSERT INTO `mdl_user` VALUES (2,'manual',1,0,0,0,1,'admin','$2y$10$p3IwPwFK9QAJumwKLj9mRe1MxaC3I1OUC1APcyCChcUFMMZIAmqrG','','Admin','User','nexor1984@gmail.com',0,'','','','','','','','','','','','','en','gregorian','','99',1476995496,1477218080,1476995496,1477217567,'127.0.0.1','',0,'','',1,1,0,1,1,0,0,1476995516,0,NULL,'','','','');
16508c16509
< -- Dump completed on 2016-10-23 12:19:07
---
> -- Dump completed on 2016-10-23 12:21:59

We can see straight away that:

  • new record has been inserted into mdl_config_log
  • the value in mdl_config_plugins has changed from 604800 to 1209600
  • timestamps in mdl_sessions and mdl_user has been updated

Let's make another test - let's see what happens when we enable mobile web services in Moodle (I did that using Moodle UI between first and second snapshot).

% mysqldump -u root -p --skip-opt moodle31 > snapshot1.sql
% mysqldump -u root -p --skip-opt moodle31 > snapshot2.sql
% diff snapshot1.sql snapshot2.sql 
2492c2492
< INSERT INTO `mdl_config` VALUES (35,'enablewebservices','0');
---
> INSERT INTO `mdl_config` VALUES (35,'enablewebservices','1');
2901c2901
< INSERT INTO `mdl_config` VALUES (447,'enablemobilewebservice','0');
---
> INSERT INTO `mdl_config` VALUES (447,'enablemobilewebservice','1');
2918a2919
> INSERT INTO `mdl_config` VALUES (465,'webserviceprotocols','rest');
3955a3957
> INSERT INTO `mdl_config_log` VALUES (1013,2,1476995587,NULL,'enablemobilewebservice','1','0');
6446c6448
< INSERT INTO `mdl_external_services` VALUES (1,'Moodle mobile web service',0,NULL,0,'moodle',1476995420,1476995523,'moodle_mobile_app',1,1);
---
> INSERT INTO `mdl_external_services` VALUES (1,'Moodle mobile web service',1,NULL,0,'moodle',1476995420,1476995598,'moodle_mobile_app',1,1);
12876a12879
> INSERT INTO `mdl_role_capabilities` VALUES (1234,1,7,'webservice/rest:use',1,1476995587,2);
13359c13362
< INSERT INTO `mdl_sessions` VALUES (2,0,'t0ivjdjkmoo24jfar7ovrqbq31',2,NULL,1476995496,1476995566,'127.0.0.1','127.0.0.1');
---
> INSERT INTO `mdl_sessions` VALUES (2,0,'t0ivjdjkmoo24jfar7ovrqbq31',2,NULL,1476995496,1476995587,'127.0.0.1','127.0.0.1');
16499c16502
< -- Dump completed on 2016-10-20 22:33:01
---
> -- Dump completed on 2016-10-20 22:33:22

.htaccess:

order allow,deny
SetEnvIF X-Forwarded-For "89.101.211" AllowIP
Allow from env=AllowIP

“88.100.211″ is the pattern meaning “88.100.211*” apache’s mod_setenvif must be enabled “order allow,deny” denies all except “Allow” rules usual “Allow from ip” rules will not work as the load balancer rewrites the “host” header, all requests are coming from the load balancer’s IP the load balancer should set the X-Forwarded-For header

script to check headers:

<?php
printf("<pre>\n");
$headers = getallheaders();
printf("headers:\n");
print_r($headers);

Introduction

Purpose of this blog is to show how to integrate Spring Security framework with REST web services implemented using Apache CXF. To authenticate user within Spring Security framework external authentication mechanism based on token is used. The idea is to authenticate Spring Security application against external application – in our case Alfresco.

Dependencies

In order to get required libraries the following ivy script was used:

<ivy-module version="2.0">
    <info organisation="icmr" module="alfrescoEnhancements"/>
    <dependencies>
        <dependency org="junit" name="junit" rev="4.11"/>
 
        <dependency org="org.springframework" name="spring-web" rev="3.1.3.RELEASE"/>
        <dependency org="org.springframework" name="spring-jdbc" rev="3.1.3.RELEASE"/>
        <dependency org="org.springframework" name="spring-orm" rev="3.1.3.RELEASE"/>
        <dependency org="org.springframework" name="spring-test" rev="3.1.3.RELEASE"/>
        <dependency org="org.springframework.security" name="spring-security-core" rev="3.1.3.RELEASE" />
        <dependency org="org.springframework.security" name="spring-security-web" rev="3.1.3.RELEASE" />
        <dependency org="org.springframework.security" name="spring-security-config" rev="3.1.3.RELEASE" />
 
 
        <dependency org="org.apache.cxf" name="cxf-rt-frontend-jaxrs" rev="2.7.1"/>
        <dependency org="org.codehaus.jackson" name="jackson-jaxrs" rev="1.9.9"/>
        <dependency org="org.codehaus.jackson" name="jackson-xc" rev="1.9.9"/>
 
        <dependency org="org.hibernate" name="hibernate-core" rev="4.1.7.Final"/>
        <dependency org="org.hibernate" name="hibernate-entitymanager" rev="4.1.7.Final"/>
        <dependency org="org.hibernate.common" name="hibernate-commons-annotations" rev="4.0.1.Final"/>
 
        <dependency org="org.eclipse.persistence" name="org.eclipse.persistence.core" rev="2.3.1"/>
        <dependency org="org.eclipse.persistence" name="org.eclipse.persistence.jpa" rev="2.3.1"/>
 
        <dependency org="org.hibernate.javax.persistence" name="hibernate-jpa-2.0-api" rev="1.0.0.Final" />
        <dependency org="javassist" name="javassist" rev="3.12.1.GA" />
        <dependency org="org.antlr" name="antlr" rev="3.5-rc-1" />
        <dependency org="com.caucho" name="hessian" rev="4.0.7" />
 
    </dependencies>
 
</ivy-module>

It might be necessary to remove older versions of some jars.

List of libraries required in the project that should be put in WEB-INF/lib is presented below:

activation-1.1.jar                    hessian-4.0.7.jar                              jetty-server-8.1.5.v20120716.jar           spring-asm-3.1.3.RELEASE.jar
antlr-2.7.7.jar                       hibernate-commons-annotations-4.0.1.Final.jar  jetty-servlet-8.1.5.v20120716.jar          spring-beans-3.1.3.RELEASE.jar
antlr-runtime-3.5-rc-1.jar            hibernate-core-4.1.7.Final.jar                 jetty-util-8.1.5.v20120716.jar             spring-context-3.1.3.RELEASE.jar
aopalliance-1.0.jar                   hibernate-entitymanager-4.1.7.Final.jar        jsf-api-1.2_08.jar                         spring-context-support-3.1.3.RELEASE.jar
aspectjweaver-1.7.1.jar               hibernate-jpa-2.0-api-1.0.0.Final.jar          json.jar                                   spring-core-3.1.3.RELEASE.jar
blueprint-parser-1.0.0.jar            hsqldb-1.8.0.10.jar                            json-path-0.8.1.jar                        spring-expression-3.1.3.RELEASE.jar
bsh-2.0b4.jar                         httpclient-4.2.jar                             json-smart-1.1.1.jar                       spring-jdbc-3.1.3.RELEASE.jar
c3p0-0.9.1.2.jar                      httpcore-4.2.jar                               jsp-api-2.0.jar                            spring-orm-3.1.3.RELEASE.jar
commons-codec-1.6.jar                 ibatis-sqlmap-2.3.4.726.jar                    jstl-1.2.jar                               spring-oxm-3.1.3.RELEASE.jar
commons-collections-3.2.jar           jackson-annotations-2.0.1.jar                  junit-4.11.jar                             spring-security-config-3.1.3.RELEASE.jar
commons-fileupload-1.2.jar            jackson-core-2.0.1.jar                         log4j-1.2.17.jar                           spring-security-core-3.1.3.RELEASE.jar
commons-httpclient-3.1.jar            jackson-core-asl-1.9.9.jar                     openjpa-1.1.0.jar                          spring-security-web-3.1.3.RELEASE.jar
commons-io-1.3.jar                    jackson-databind-2.0.1.jar                     org.apache.aries.blueprint.api-1.0.0.jar   spring-test-3.1.3.RELEASE.jar
commons-lang-2.6.jar                  jackson-jaxrs-1.9.9.jar                        org.apache.aries.blueprint.core-1.0.0.jar  spring-tx-3.1.3.RELEASE.jar
commons-logging-1.1.1.jar             jackson-mapper-asl-1.9.9.jar                   org.apache.aries.proxy.api-1.0.0.jar       spring-web-3.1.3.RELEASE.jar
commons-pool-1.3.jar                  jackson-xc-1.9.9.jar                           org.apache.aries.quiesce.api-1.0.0.jar     spring-webmvc-3.1.3.RELEASE.jar
cxf-api-2.7.1.jar                     javassist-3.12.1.GA.jar                        org.apache.aries.util-1.0.0.jar            spring-webmvc-portlet-3.1.3.RELEASE.jar
cxf-rt-bindings-xml-2.7.1.jar         javax.inject-1.jar                             org.eclipse.persistence.core-2.3.1.jar     ST4-4.0.7-rc-1.jar
cxf-rt-core-2.7.1.jar                 javax.ws.rs-api-2.0-m10.jar                    org.eclipse.persistence.jpa-2.3.1.jar      standard-1.1.2.jar
cxf-rt-frontend-jaxrs-2.7.1.jar       jaxb-impl-2.1.13.jar                           org.osgi.compendium-4.2.0.jar              stax2-api-3.1.1.jar
cxf-rt-transports-http-2.7.1.jar      jaxrpc-api-1.1.jar                             org.osgi.core-4.2.0.jar                    stax-api-1.0-2.jar
derby-10.5.3.0_1.jar                  jboss-logging-3.1.0.CR2.jar                    persistence-api-1.0.jar                    stringtemplate-3.2.1.jar
derbyclient-10.5.3.0_1.jar            jcommander-1.12.jar                            portlet-api-2.0.jar                        testng-6.5.2.jar
dom4j-1.6.1.jar                       jdo-api-3.0.jar                                rome-1.0.jar                               transaction-api-1.1.jar
geronimo-javamail_1.4_spec-1.7.1.jar  jdom-1.0.jar                                   saaj-api-1.3.jar                           woodstox-core-asl-4.1.4.jar
geronimo-jms_1.1_spec-1.0.1.jar       jetty-continuation-8.1.5.v20120716.jar         serp-1.13.1.jar                            wsdl4j-1.6.2.jar
geronimo-jta_1.1_spec-1.1.jar         jetty-http-8.1.5.v20120716.jar                 slf4j-api-1.6.2.jar                        xml-apis-1.0.b2.jar
h2-1.0.71.jar                         jetty-io-8.1.5.v20120716.jar                   snakeyaml-1.6.jar                          xmlschema-core-2.0.3.jar
hamcrest-core-1.3.jar                 jetty-security-8.1.5.v20120716.jar             spring-aop-3.1.3.RELEASE.jar               xmlunit-1.2.jar

web.xml

Configuration of WEB-INF/web.xml file is presented below. It contains configuration of the following frameworks:

  • Spring
  • Spring Security
  • Apache CXF
  • Hibernate
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
        http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
 
    <!-- Define configuration file for Spring -->
    <context-param>
        <description>Spring configuration file</description>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/classes/applicationContext.xml</param-value>
    </context-param>
 
    <!-- Define configuration file for log4j -->
    <context-param>
        <param-name>log4jConfigLocation</param-name>
        <param-value>/WEB-INF/classes/log4j.properties</param-value>
    </context-param>
 
    <!-- Add log4j logging -->
    <listener>
        <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
    </listener>
 
    <!-- Enable Spring support -->
    <listener>
        <description>Spring Loader</description>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
 
    <!--Define REST web services servlet -->
    <servlet>
        <servlet-name>CXFServlet</servlet-name>
        <servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
 
    <servlet-mapping>
        <servlet-name>CXFServlet</servlet-name>
        <url-pattern>/service/*</url-pattern>
    </servlet-mapping>
 
    <!-- Hibernate filter to support opening session to get additional options -->
    <filter>
        <filter-name>openSessionInViewFilter</filter-name>
        <filter-class>org.springframework.orm.hibernate4.support.OpenSessionInViewFilter</filter-class>
    </filter>
 
    <filter-mapping>
        <filter-name>openSessionInViewFilter</filter-name>
        <url-pattern>/service/*</url-pattern>
    </filter-mapping>
 
 
    <!-- Spring Security -->
    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
 
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/service/*</url-pattern>
    </filter-mapping>
 
</web-app>

We are going to put all the files related to REST web service in path /service/*, therefore in the configuration file above this path is set as url-pattern for Apache CXF Service filter (CXFServlet). Spring Security filter (springSecurityFilterChain) has the same url-pattern set in order to secure REST web services.

Spring configuration file

As defined in WEB-INF/web.xml file Spring configuration file is in the following location ‘/WEB-INF/classes/applicationContext.xml’ and is presented below. It includes files with configuration of beans required by Spring Security (resources/context/security.xml) and Apache CXF REST web service (resources/context/service.xml). Section with namespace ‘jaxrs’ corresponds to configuration of Apache CXF REST web service and section with namespace ‘security’ corresponds to configuration related to Spring Security. They will be described in more detail in the following sections.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:jaxrs="http://cxf.apache.org/jaxrs"
    xmlns:cxf="http://cxf.apache.org/core"
    xmlns:security="http://www.springframework.org/schema/security"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://cxf.apache.org/jaxrs
        http://cxf.apache.org/schemas/jaxrs.xsd
        http://cxf.apache.org/core
        http://cxf.apache.org/schemas/core.xsd
        http://www.springframework.org/schema/security
        http://www.springframework.org/schema/security/spring-security.xsd"
>
 
    <!-- Enable use of annotations -->
    <context:annotation-config />
 
    <!-- Import files with Spring bean definitions -->
    <import resource="classpath:resources/context/service.xml" />
    <import resource="classpath:resources/context/security.xml" />
 
    <!-- Define REST services -->
    <jaxrs:server id="userService" address="/">
        <jaxrs:serviceBeans>
            <ref bean="userServiceBean"/>
        </jaxrs:serviceBeans>
        <jaxrs:extensionMappings>
            <entry key="xml" value="application/xml" />
            <entry key="json" value="application/json" />
            <entry key="feed" value="application/atom+xml"/>
            <entry key="html" value="text/html"/>
        </jaxrs:extensionMappings>
        <jaxrs:providers>
            <bean class="org.codehaus.jackson.jaxrs.JacksonJaxbJsonProvider"/>
            <bean class="org.codehaus.jackson.jaxrs.JacksonJsonProvider"/>
        </jaxrs:providers>
        <jaxrs:features>
            <cxf:logging/>
        </jaxrs:features>
    </jaxrs:server>
 
    <security:authentication-manager alias="authenticationManager">
        <security:authentication-provider ref="myAuthenticationProvider" />
    </security:authentication-manager>
 
    <security:global-method-security authentication-manager-ref="authenticationManager" jsr250-annotations="enabled"/>
 
    <security:http pattern="/service/initialize/**" security="none"/>
 
    <security:http auto-config="false" entry-point-ref="http403ForbiddenEntryPoint" use-expressions="true" >
        <security:intercept-url pattern="/service/**" access="isAuthenticated()" />
        <security:custom-filter position="FORM_LOGIN_FILTER" ref="myAuthenticationProcessingFilter" />
 
    </security:http>
 
</beans>

Apache CXF REST web service configuration

As defined in ‘/WEB-INF/classes/applicationContext.xml’ (jaxrs:server) REST service consists of one service – userService. Corresponding Spring bean (jaxrs:serviceBeans) is called ‘userServiceBean’ and is defined in /WEB-INF/classes/resources/context/service.xml file as follows:

<bean id="userServiceBean" class="com.jmuras.service.user.UserServiceImpl" />

In addition the configuration defines extension mappings (jaxrs:extensionMappings), enables logging (cxf:logging), and selects Jackson JSON-processor for processing JSON data (jaxrs:providers).

UserServiceImpl class is presented below. It implements UserService interface, produces JSON content, and path to the service is /user. It also autowires session factory, which is hibernate session factory, and defines one method retrieveProperties(). retrieveProperties() method can be accessed using the following URL: /service/user/retrieveProperties. It responds to GET request and returns JSON (determined by @Produces annotation of UserServiceImpl class) with list of property names. The URL depends on configuration of url-pattern in Apache CXF filter (web.xml) and @Path annotations.

package com.jmuras.service.user;
 
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
 
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
 
/**
 * @author Joanna Muras
 * @date 12/12/12
 * Class that implements user service methods.
 */
 
@Path("/user")
@Produces(MediaType.APPLICATION_JSON + "; " + MediaType.CHARSET_PARAMETER +"=UTF-8")
public class UserServiceImpl implements UserService {
 
    private static Log logger = LogFactory.getLog(UserServiceImpl.class);
 
    @Autowired
    private SessionFactory sessionFactory;
 
    @GET
    @Path("/retrieveProperties")
    @Transactional
    public Collection<String> retrieveProperties() {
        if (logger.isDebugEnabled()) {
            logger.debug("Retrieve user properties");
        }
 
        Session session = sessionFactory.getCurrentSession();
        Query query = session.getNamedQuery("User.findProperties");
        return query.list();
    }
}

Spring Security related configuration

As mentioned in previous section Spring security configuration is located in ‘/WEB-INF/classes/resources/context/security.xml’. Let’s assume the following scenario of authentication in Spring Security framework. There is external authentication service that we use to authorise user within Spring Security framework. When request to /service/user/retrieveProperties is made it also contains a ticket passed as parameter in URL, i.e., /service/user/retrieveProperties?ticket=.... The ticket contains all the information necessary to authenticate user in external service and see whether the ticket is still valid. External service returns all the information required by Spring Security framework to authenticate the user. The related configuration is presented below and will be discussed in the following part of this section.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
 
    <!-- My Authentication Provider -->
    <bean id="myAuthenticationProvider" class="com.jmuras.security.AuthenticationProvider" />
 
    <bean id="myAuthenticationProcessingFilter" class="com.jmuras.security.AuthenticationProcessingFilter" >
        <constructor-arg value="/"/>
        <property name="authenticationManager" ref="authenticationManager"/>
        <property name="sessionAuthenticationStrategy" ref="sas" />
 
        ...
 
    </bean>
 
    <bean id="http403ForbiddenEntryPoint" class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint"/>
 
    <bean id="sas" class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy" />
 
</beans>

myAuthenticationProcessingFilter bean – AuthenticationProcessingFilter class

Responsibility of myAuthenticationProcessingFilter bean is to take ticket from the request and issue request to external authentication service to get user details. The user details are then translated to the roles defined in Spring Security annotations. AuthenticationProcessingFilter class extends AbstractAuthenticationProcessingFilter class which demands implementation of Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) method. This method performs actual authentication – in our example obtains user details from external authentication service in the basis of the ticket.

attemptAuthentication method is invoked only when boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) method returns ‘true’. Implementation of requiresAuthentication in AbstractAuthenticationProcessingFilter class matches filterProcessesUrl (given in constructor of AuthenticationProcessingFilter object) against request URL, because we do not want to define access to REST web service methods on the basis of their URLs, we have to overwrite this behaviour. We assume that if there is no authentication object in context then requiresAuthentication method should return true. When authentication object is obtained getAuthenticationManager().authenticate(authentication) is invoked in the code to authenticate the user using authentication provider (AuthenticationProvider).

successfulAuthentication method is invoked when credentials are retrieved from third party authentication system and attemptAuthentication does not throw AuthenticationException exception. The method saves authentication object in security context and invokes next filter from the chain.

Implementation of myAuthenticationProcessingFilter bean, which checks ticket in Alfresco is presented below. It is assumed that there will be two additional parameters passed to request URL – alf_ticket, workgroup – /service/user/retrieveProperties?alf_ticket=...&workgroup=.... There are the following roles defined in the system:

  1. ROLE_ADMINISTRATOR
  2. ROLE_WORKGROUP_ADMINISTRATOR
  3. ROLE_USER

Depending on the role user should be able to run only allowed web services.

public class AuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
 
    private static Log logger = LogFactory.getLog(AuthenticationProcessingFilter.class);
 
    public static final String TICKET = "alf_ticket";
    public static final String WORKGROUP = "workgroup";
 
    private String userPreferencesURL;
    private int retries;
 
    protected AuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
    }
 
    public void setUserPreferencesURL(String userPreferencesURL) {
        this.userPreferencesURL = userPreferencesURL;
    }
 
    public void setRetries(int retries) {
        this.retries = retries;
    }
 
    @Override
 public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
 
        AuthenticationToken authentication = null;
 
        String ticket = httpServletRequest.getParameter(TICKET);
 
        // Get workgroup parameter if present in URL
        String[] uri = httpServletRequest.getRequestURI().split("/");
        String workgroup = null;
        if (uri != null && uri.length > 0) {
            int workgroupIndex = -1;
            for (int i = 0; i < uri.length; i++) {
                if (uri[i].equals(WORKGROUP)) {
                    workgroupIndex = i;
                }
            }
            if (workgroupIndex > -1 && workgroupIndex + 1 < uri.length) {
                workgroup = uri[workgroupIndex + 1];
            }
        }
 
        // Create an instance of HttpClient.
        HttpClient client = new HttpClient();
 
        // Create user preferences request URL
        StringBuffer sb = new StringBuffer(userPreferencesURL);
        sb.append("?");
        sb.append(TICKET);
        sb.append("=");
        sb.append(ticket);
        if (workgroup != null && workgroup.length() > 0) {
            sb.append("&");
            sb.append(WORKGROUP);
            sb.append("=");
            sb.append(workgroup);
        }
 
        if (logger.isDebugEnabled()) {
            logger.debug("Obtain user details from Alfresco by the following request " + userPreferencesURL);
        }
 
        // Create a method instance.
        HttpMethodBase method = new GetMethod(sb.toString());
 
        // Forward accept header
        method.setRequestHeader("Accept", "application/json");
 
        // Provide custom retry handler is necessary
        method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
                new DefaultHttpMethodRetryHandler(retries, true));
 
        try {
            // Execute the method.
            int statusCode = client.executeMethod(method);
 
            if (statusCode == HttpStatus.SC_OK) {
 
                // Read the response body.
                String responseString = method.getResponseBodyAsString();
 
                if (logger.isDebugEnabled()) {
                    logger.debug("Following string with user details obtained from Alfresco " + userPreferencesURL);
                }
 
                if (responseString != null) {
 
                    JSONObject json = new JSONObject(responseString);
 
                    // Get authentication data from Alfresco response
                    Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
 
                    // Depending on response obtained from Alfresco add appropriate roles to 'authorities' structure.
                    boolean isAdmin = json.getBoolean("isAdmin");
                    if (isAdmin) {
                        authorities.add(new SimpleGrantedAuthority("ROLE_ADMINISTRATOR"));
                    }
 
                    if (!json.isNull("workgroupRole")) {
                        String workGroupRole = json.getString("workgroupRole");
                        if (workGroupRole != null && workGroupRole.contentEquals("SiteManager")) {
                            authorities.add(new SimpleGrantedAuthority("ROLE_WORKGROUP_ADMINISTRATOR"));
                        }
                    }
 
                    String principal = json.getString("userName");
                    // Assign user role only if user is neither workGroup administrator nor administrator
                    if (principal != null && authorities.size() == 0) {
                        authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
                    }
 
 
                    // Save authorities in authentication object
                    authentication = new AuthenticationToken(authorities);
                    authentication.setPrincipal(principal);
                    authentication.setCredentials(ticket);
 
                    if (logger.isDebugEnabled()) {
                        logger.debug("Authentication object filled in as follows " + authentication);
                    }
 
                    // Authenticate the user
                    return getAuthenticationManager().authenticate(authentication);
                }
            } else {
                logger.error("User details could not be obtained from Alfresco " + method.getStatusLine().toString());
            }
 
 
        } catch (HttpException e) {
            logger.error(e.getMessage(), e);
        } catch (IOException e) {
            logger.error(e.getMessage(), e);
        } catch (JSONException e) {
            logger.error(e.getMessage(), e);
        } finally {
            // Release the connection.
            method.releaseConnection();
        }
 
        // Throw exception when not possible to authenticate
        throw new AuthenticationCredentialsNotFoundException("Authentication for ticket '" + ticket + "' unsuccessful");
    }
 
    @Override
    protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
 
        SecurityContext context = SecurityContextHolder.getContext();
        if (context != null && context.getAuthentication() != null) {
            return false;
        }
        return true;
    }
 
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException{
 
        SecurityContextHolder.getContext().setAuthentication(authResult);
        chain.doFilter(request, response);
    }
}

myAuthenticationProvider bean – AuthenticationProvider class

This bean is responsible for returning information whether user is authenticated or not. It implements AuthenticationProvider interface, which defines 2 methods:

  1. Authentication authenticate(Authentication authentication) – This method returns authentication object used by Spring Security for authorization. It checks data provided in authentication object passed as parameter and on that basis decides whether user should be authenticated (authentication.setAuthenticated(true)) or not. In our example if user name (authentication.getPrincipal()) was obtained from external service on the basis of the ticket, it is assumed that user is authenticated. If user is not authenticated BadCredentialsException is thrown.
  2. boolean supports(Class aClass) – This method returns whether AuthenticationProvider object should process the request – authenticate method should be invoked. In that case only authentication object given as a parameter to authenticate method which is of AuthenticationToken type should be processed by authenticate method.

Implementation of myAuthenticationProvider bean is presented below.

public class AuthenticationProvider implements AuthenticationProvider {
 
    private static Log logger = LogFactory.getLog(AuthenticationProvider.class);
 
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
 
        // Assume that if principal present user was authenticated against external service
        if (authentication != null && authentication.getPrincipal() != null) {
            authentication.setAuthenticated(true);
            if (logger.isDebugEnabled()) {
                logger.debug("User " + authentication.getPrincipal() + " authenticated");
            }
            return authentication;
        }
 
        if (logger.isDebugEnabled()) {
            logger.debug("Ticked not valid");
        }
 
        throw new BadCredentialsException("Ticked not valid");
    }
 
    @Override
    public boolean supports(Class<?> aClass) {
        return aClass.equals(AuthenticationToken.class);
 
    }
}

Roles definition

Roles are defined in authentication object (AuthenticationToken class). Depending on roles assigned to the user it is possible to allow the user to invoke particular web services and forbid invocation of another.The roles defined in this example are:

  1. ROLE_ADMINISTRATOR – administrator
  2. ROLE_WORKGROUP_ADMINISTRATOR – workgroup administrator
  3. ROLE_USER – user

The roles are used in the annotations in user service (UserService) interface. The interface is implemented by UserServiceImpl class – userServiceBean bean. In the code below two methods, which are web services, are presented – retrieveOptions and createOption. The first one allows users in all three roles to invoke the web service call, and the second one allows the invocation for only workgroup administrator. The roles are the can invoke particular method are listed in @RolesAllowed annotation.

public interface UserService {
 
    public final static String TICKET = "alf_ticket";
 
    /**
     * Return list of options for particular property matching criteria
     * @param property name of the property
     * @param parentProperty name of the parent property (optional)
     * @param parentWorkgroup name of the parent workgroup (optional)
     * @param parentValue parent value (optional)
     * @param page number of page to start
     * @param results number of results per page
     * @return list of options
     */
    @RolesAllowed({"ROLE_USER", "ROLE_WORKGROUP_ADMINISTRATOR", "ROLE_ADMINISTRATOR"})
    public Collection<OptionEntityXML> retrieveOptions(String property, Long parentId, String parentProperty, String parentWorkgroup, String parentValue, int page, int results);
 
    /**
     * Create new WorkGroup specific option
     * @param workgroup workgroup name for new option
     * @param wrapper values of option to be added
     * @return created option
     */
    @RolesAllowed({"ROLE_WORKGROUP_ADMINISTRATOR"})
    public OptionEntityXML createOption(String workgroup, OptionEntityWrapper wrapper);
}

The corresponding implementation class is presented below:

@Path("/user")
@Produces(MediaType.APPLICATION_JSON + "; " + MediaType.CHARSET_PARAMETER +"=UTF-8")
public class UserServiceImpl implements UserService {
 
    private static Log logger = LogFactory.getLog(UserServiceImpl.class);
 
    private AlfrescoSearch search;
 
    @Autowired
    private SessionFactory sessionFactory;
 
    public void setSearch(AlfrescoSearch search) {
        this.search = search;
    }
 
    @GET
    @Path("/retrieveOptions/{property}")
    @Transactional
    @XmlElementWrapper(name = "options")
    @XmlElement(name = "option")
    public Collection<OptionEntityXML> retrieveOptions(@PathParam("property") String property, @QueryParam("parentId") Long parentId, @QueryParam("parentProperty") String parentProperty, @QueryParam("parentWorkgroup") String parentWorkgroup, @QueryParam("parentValue") String parentValue, @DefaultValue("0") @QueryParam("page") int page, @DefaultValue("0") @QueryParam("results") int results) {
        List<OptionEntity> list = (List<OptionEntity>) retrieveOptionsEntity(property, parentId, parentProperty, parentWorkgroup, parentValue, page, results);
        return getXMLList(list);
    }
 
    @POST
    @Path("/createOption")
    @Consumes(MediaType.APPLICATION_JSON)
    @Transactional
    public OptionEntityXML createOption(OptionEntityWrapper wrapper) {
 
        if (wrapper == null) {
            return null;
        }
 
        if (logger.isDebugEnabled()) {
            logger.debug("Create option with the following parameters" + wrapper.getValue() + "," + wrapper.getProperty() + "," + wrapper.getWorkgroup());
        }
 
        Session session = sessionFactory.getCurrentSession();
        OptionEntity entity = wrapper.getEntity();
        // Remove workgroup if set
        wrapper.setWorkgroup(null);
        try {
            OptionEntity parentEntity = getParentOption(wrapper);
            if (parentEntity != null) {
                entity.setParent(parentEntity);
            }
        } catch (NotFoundException ex) {
            logger.error("Parent entity for the following parameters not found: " + wrapper.getParentId() + ", " + wrapper.getParentProperty() + ", " + wrapper.getParentWorkgroup() + ", " + wrapper.getParentValue());
        }
        session.save(entity);
 
        return new OptionEntityXML(entity);
    }
}

Testing

sync_binlog is a setting that (when set to 0) forces MySQL to flush a write to binary log to disk after every transaction. I have performed a quick test to see how does the setting affect the performance of MySQL and the reliability of the binary log.

Test setup:

  • Ubuntu Server 13.10 running in VirtualBox
  • MySQL 5.5.32 from the standard package, default configuration
  • InnoDB storage engine used
  • SSD storage but no battery-backed disk

I have tested the performance of a script that inserts 10000 short rows. Each test was repeated 20 times, then machine restarted and series of another 20 tests performed. Higher times for the initial tests (1-4 and 21-24) are most likely related to warm-up after reboot and should really be ignored. After each test, I’ve checked the value of the dirty (not-flushed) caches to validate the result. Note that this is pretty extreme situation – I’m emulating an application that only does writes to the database.

sync_binlog=0

Time and kB of dirty cache with sync_binlog=0

cc

Time and kB of dirty cache with sync_binlog=1

Performance

As expected, with sync_binlog=0 there are plenty of binary log changes to be flushed to disk. There is none when running with sync_binlog=1. On the other hand, the average time to perform 10000 INSERTs jumped from 23 seconds to 41 seconds – nearly doubled.

Reliability

I’ve tested how reliable is each setting by running the script with 10000 INSERTs, forcefully powering off the whole system, starting up again and then comparing records (transactions) written to the database versus those written to binary log. I have tried it few times with both setting and the same results each time:

  • with sync_binlog=1 the data in the database was always in sync with the binary log. That is, the last record succesfully inserted into the database, matches the last entry in binary log.
  • with sync_binlog=0, after reboot there were thousands of records persisted in the database but missing from the binary log.

Summary

If you care about binary log (think about master-slave replication for example) set sync_binlog to 1, if you want to squeeze a bit more performance, set it to 0.


Introduction

This blog describes how to limit upload of files with size exceeding particular value to Alfresco repository.

Implementation

Restriction can be done in ContentServiceImpl class. Let’s extend this class in class CustomContentServiceImpl. We can add sizeLimit variable which is going to keep limit value in bytes and then use this limit in getWriter method.

package com.jmuras.alfresco.repo.content;
 
public class CustomContentServiceImpl extends ContentServiceImpl {
...
 
private Long sizeLimit;
 
public void setSizeLimit(long sizeLimit) {
    sizeLimit = sizeLimit;
}
 
@Override
public ContentWriter getWriter(NodeRef nodeRef, QName propertyQName, boolean update) {
 
        // Get content writer from parent method
        ContentWriter contentWriter = super.getWriter(nodeRef, propertyQName, update);
 
        // If size limit variable is set
        if (contentWriter instanceof AbstractContentWriter && sizeLimit != null) {
            AbstractContentWriter abstractContentWriter = (AbstractContentWriter) contentWriter;
 
            // Set size limit for content provider
            ContentLimitProvider contentLimitProvider = new ContentLimitProvider.SimpleFixedLimitProvider(sizeLimit);
            abstractContentWriter.setContentLimitProvider(contentLimitProvider);
 
            return abstractContentWriter;
        }
        return contentWriter;
    }
 
...
}

Now you have to declare new content service bean (contentService). You can do it in alfresco/patch/custom-content-services-context.xml file as follows:

<?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="customBaseContentService" class="com.jmuras.alfresco.repo.content.CustomContentServiceImpl" abstract="true"
          parent="baseContentService">
        <property name="authenticationService">
            <ref bean="authenticationService"/>
        </property>
        <property name="nodeService">
            <ref bean="nodeService" />
        </property>
 
        <!--Limit for file size in bytes-->
        <property name="sizeLimit" value="2000000"/>
    </bean>
 
    <bean id="contentServiceOrig" parent="baseContentService">
        <property name="store">
            <ref bean="fileContentStore"/>
        </property>
    </bean>
 
    <bean id="contentService" parent="customBaseContentService">
        <property name="store">
            <ref bean="fileContentStore"/>
        </property>
    </bean>
 
</beans>

Size limit variable (sizeLimit) can be also defined in alfresco-global.properties and then instead of value, e.g., 2000000 ${sizeLimit} placeholder should be used.

That change should prevent possibility of upload of files, which exceed particular size. In addition, change in Alfresco WebScript (alfresco/templates/webscripts/org/alfresco/repository/upload/upload.post.js) can be made to return appropriate error to Alfresco Share.

Existing code:

   catch (e)
   {
      // NOTE: Do not clean formdata temp files to allow for retries. It's possible for a temp file
      //       to remain if max retry attempts are made, but this is rare, so leave to usual temp
      //       file cleanup.
 
      // capture exception, annotate it accordingly and re-throw
      if (e.message && e.message.indexOf("org.alfresco.service.cmr.usage.ContentQuotaException") == 0)
      {
         e.code = 413;
      }
      else
      {
         e.code = 500;
         e.message = "Unexpected error occurred during upload of new content.";      
      }
      throw e;
   }

New code:

catch (e)
   {
      // NOTE: Do not clean formdata temp files to allow for retries. It's possible for a temp file
      //       to remain if max retry attempts are made, but this is rare, so leave to usual temp
      //       file cleanup.
 
      // capture exception, annotate it accordingly and re-throw
      if (e.message && e.message.indexOf("org.alfresco.service.cmr.usage.ContentQuotaException") == 0)
      {
         e.code = 413;
      }
      else if (e.message && e.message.indexOf("org.alfresco.repo.content.ContentLimitViolationException") == 0)
      {
         e.code = 500;
         e.message = "Your file exceeds upload limit."; 
      }
      else
      {
         e.code = 500;
         e.message = "Unexpected error occurred during upload of new content.";      
      }
      throw e;
   }

Introduction

This blog introduces sending periodical e-mail reminders in Alfresco Activiti Workflows.

Workflow with periodical e-mail reminders

To send periodical e-mail reminders (as well as to do any other periodical action) we have to start from defining the workflow. Let’s assume that we extend already existing workflow ‘Review And Approve Activiti Process’ defined in file ‘review.bpmn20.xml’. We will add periodical reminder (‘E-mail Reminder’) to review task to remind user that this task is pending by sending him e-mail.

E-mail Reminders Workflow

Corresponding XML looks like follows. Two tasks were added to it:

  • Timer, which is boundaryEvent with id ‘timer_review’
  • E-mail Reminder, which is serviceTask with id ‘mailtask_review’
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://alfresco.org">
  <process id="activitiReview" name="Review And Approve Activiti Process" isExecutable="true">
 
    <startEvent id="start" activiti:formKey="wf:submitReviewTask"></startEvent>
 
    <sequenceFlow id="flow1" sourceRef="start" targetRef="reviewTask"></sequenceFlow>
 
    <userTask id="reviewTask" name="Review Task" activiti:assignee="${bpm_assignee.properties.userName}" activiti:formKey="wf:activitiReviewTask">
      <extensionElements>
        <activiti:taskListener event="create" class="org.alfresco.repo.workflow.activiti.tasklistener.ScriptTaskListener">
          <activiti:field name="script">
            <activiti:string>
                if (typeof bpm_workflowDueDate != 'undefined') task.dueDate = bpm_workflowDueDate
                if (typeof bpm_workflowPriority != 'undefined') task.priority = bpm_workflowPriority;
            </activiti:string>
          </activiti:field>
        </activiti:taskListener>
        <activiti:taskListener event="complete" class="org.alfresco.repo.workflow.activiti.tasklistener.ScriptTaskListener">
          <activiti:field name="script">
            <activiti:string>
                execution.setVariable('wf_reviewOutcome', task.getVariable('wf_reviewOutcome'));
            </activiti:string>
          </activiti:field>
        </activiti:taskListener>
      </extensionElements>
    </userTask>
 
    <sequenceFlow id="flow2" sourceRef="reviewTask" targetRef="reviewDecision"></sequenceFlow>
 
    <exclusiveGateway id="reviewDecision" name="Review Decision"></exclusiveGateway>
 
    <sequenceFlow id="flow3" sourceRef="reviewDecision" targetRef="approved">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${wf_reviewOutcome == 'Approve'}]]></conditionExpression>
    </sequenceFlow>
 
    <sequenceFlow id="flow4" sourceRef="reviewDecision" targetRef="rejected"></sequenceFlow>
 
    <userTask id="approved" name="Document Approved" activiti:assignee="${initiator.exists() ? initiator.properties.userName : 'admin'}" activiti:formKey="wf:approvedTask">
      <documentation>The document was reviewed and approved.</documentation>
      <extensionElements>
        <activiti:taskListener event="create" class="org.alfresco.repo.workflow.activiti.tasklistener.ScriptTaskListener">
          <activiti:field name="script">
            <activiti:string>
                if (typeof bpm_workflowDueDate != 'undefined') task.dueDate = bpm_workflowDueDate
                if (typeof bpm_workflowPriority != 'undefined') task.priority = bpm_workflowPriority;
            </activiti:string>
          </activiti:field>
        </activiti:taskListener>
      </extensionElements>
    </userTask>
 
    <userTask id="rejected" name="Document Rejected" activiti:assignee="${initiator.exists() ? initiator.properties.userName : 'admin'}" activiti:formKey="wf:rejectedTask">
      <documentation>The document was reviewed and rejected.</documentation>
      <extensionElements>
        <activiti:taskListener event="create" class="org.alfresco.repo.workflow.activiti.tasklistener.ScriptTaskListener">
          <activiti:field name="script">
            <activiti:string>
                if (typeof bpm_workflowDueDate != 'undefined') task.dueDate = bpm_workflowDueDate
                if (typeof bpm_workflowPriority != 'undefined') task.priority = bpm_workflowPriority;
            </activiti:string>
          </activiti:field>
        </activiti:taskListener>
      </extensionElements>
    </userTask>
 
    <sequenceFlow id="flow5" sourceRef="approved" targetRef="end"></sequenceFlow>
 
    <sequenceFlow id="flow6" sourceRef="rejected" targetRef="end"></sequenceFlow>
 
    <endEvent id="end"></endEvent>
 
    <boundaryEvent id="timer_review" name="Timer" attachedToRef="reviewTask" cancelActivity="false">
        <timerEventDefinition>
            <timeCycle>0 0 0 ? * MON</timeCycle>
    </timerEventDefinition>
    </boundaryEvent>
 
    <serviceTask id="mailtask_review" name="E-mail Reminder" activiti:class="org.alfresco.repo.workflow.activiti.script.AlfrescoScriptDelegate">
         <extensionElements>
             <activiti:field name="script">
                 <activiti:string>
 
                     logger.log("E-mail reminder - review task");
 
                     // Send e-mail reminder if assignee defined
                     if (bpm_assignee != null) {
                         var mail = actions.create("mail");
                         mail.parameters.to = bpm_assignee.properties.userName;
                         mail.parameters.subject = "Task reminder - document " + bpm_workflowDescription + " pending review";
                         mail.parameters.ignore_send_failure = true;
                         mail.execute(companyhome);
                     }
 
                 </activiti:string>
             </activiti:field>
         </extensionElements>
    </serviceTask>
 
    <sequenceFlow id="flow7" sourceRef="timer_review" targetRef="mailtask_review"></sequenceFlow>
 
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_activitiReview">
    <bpmndi:BPMNPlane bpmnElement="activitiReview" id="BPMNPlane_activitiReview">
      <bpmndi:BPMNShape bpmnElement="start" id="BPMNShape_start">
        <omgdc:Bounds height="35.0" width="35.0" x="30.0" y="200.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="reviewTask" id="BPMNShape_reviewTask">
        <omgdc:Bounds height="55.0" width="105.0" x="125.0" y="190.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="reviewDecision" id="BPMNShape_reviewDecision">
        <omgdc:Bounds height="40.0" width="40.0" x="290.0" y="197.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="approved" id="BPMNShape_approved">
        <omgdc:Bounds height="55.0" width="105.0" x="390.0" y="97.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="rejected" id="BPMNShape_rejected">
        <omgdc:Bounds height="55.0" width="105.0" x="390.0" y="297.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="end" id="BPMNShape_end">
        <omgdc:Bounds height="35.0" width="35.0" x="555.0" y="307.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="timer_review" id="BPMNShape_timer_review">
        <omgdc:Bounds height="30.0" width="30.0" x="175.0" y="230.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="mailtask_review" id="BPMNShape_mailtask_review">
        <omgdc:Bounds height="55.0" width="105.0" x="137.0" y="297.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
        <omgdi:waypoint x="65.0" y="217.0"></omgdi:waypoint>
        <omgdi:waypoint x="125.0" y="217.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
        <omgdi:waypoint x="230.0" y="217.0"></omgdi:waypoint>
        <omgdi:waypoint x="290.0" y="217.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow3" id="BPMNEdge_flow3">
        <omgdi:waypoint x="310.0" y="197.0"></omgdi:waypoint>
        <omgdi:waypoint x="310.0" y="124.0"></omgdi:waypoint>
        <omgdi:waypoint x="390.0" y="124.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow4" id="BPMNEdge_flow4">
        <omgdi:waypoint x="310.0" y="237.0"></omgdi:waypoint>
        <omgdi:waypoint x="310.0" y="324.0"></omgdi:waypoint>
        <omgdi:waypoint x="390.0" y="324.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow5" id="BPMNEdge_flow5">
        <omgdi:waypoint x="495.0" y="124.0"></omgdi:waypoint>
        <omgdi:waypoint x="572.0" y="124.0"></omgdi:waypoint>
        <omgdi:waypoint x="572.0" y="307.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow6" id="BPMNEdge_flow6">
        <omgdi:waypoint x="495.0" y="324.0"></omgdi:waypoint>
        <omgdi:waypoint x="555.0" y="324.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow7" id="BPMNEdge_flow7">
        <omgdi:waypoint x="190.0" y="260.0"></omgdi:waypoint>
        <omgdi:waypoint x="189.0" y="297.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>

Timer

Complete reference to boundaryEvent can be found here. In our case we care about two attributes:

  • attachedToRef – indicates task which starts timer
  • cancelActivity – if set to ‘false’ does not cancel original task (defined in attachedToRef attribute)

Complete reference to timerEventDefinitions can be found here. In our case we selected to use timeCycle element, which specifies repeating interval and can be given as cron expression, e.g., run ‘E-mail Reminder’ task every Monday at 00:00am – 0 0 0 ? * MON

E-mail Reminder

This task is serviceTask, which means that it executes without any user interaction. In our case we define it as ‘org.alfresco.repo.workflow.activiti.script.AlfrescoScriptDelegate’ class to make it possible to write JavaScript code, which uses Alfresco JavaScript API. In this task, simply send e-mail with reminder to user, which was selected as review assignee.


How many lines of code does Moodle have?
The short answer is: core Moodle 2.6 has about 600k lines of code + 3rd party libraries.

Now, the long answer.
A lot of the Moodle code are 3rd party libraries that are copied mostly under “lib” directory and some other places – I have counted those separately. I have also not counted *-min.js and *-debug.js files as duplicates.

After stripping from those, here is the statistic for the core code for the latest Moodle (2.7dev build: 20131122):

-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
PHP                           4462         125112         285162         564609
Javascript                     154           3856          10191          25478
CSS                            223           3411           1895          23375
XSD                             77           2304           4377          20071
XML                            133             43            127          15366
LESS                            25            443            407           7967
HTML                            46            268              7           3165
Perl                             2             57             76            535
MXML                             1             44             64            404
XSLT                             2             11             12            130
SQL                              1             26             31             92
DTD                              1              9              0             39
ActionScript                     1              6              1             33
-------------------------------------------------------------------------------
SUM:                          5128         135590         302350         661264
-------------------------------------------------------------------------------

Third party libraries account for:

-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
PHP                           1747          48352         176311         285443
Javascript                    1107          57787         145980         224760
CSS                            314           3290           3897          16268
HTML                            53            583             18           6242
XML                             23            553            237           5890
LESS                            41            810            942           4406
XSD                              5            108              6           1058
XSLT                             6            176             46            800
Java                             1             74             78            230
DTD                              2              0              0             82
-------------------------------------------------------------------------------
SUM:                          3299         111733         327515         545179
-------------------------------------------------------------------------------

Have a look at the following code:

class A
{
    private $var = 17;

    public function accessOtherPrivate(A $a)
    {
        echo $a-&gt;var;
    }

}

$a = new A();
$b = new A();
$b-&gt;accessOtherPrivate($a);

The code above works just fine, you can access private properties of different object – as long as you’re the same class. PHP manual confirms the behaviour:

Members declared as private may only be accessed by the class that defines the member.

Java has the same behaviour.


Introduction

Values of fields defined in cm:auditable aspect can not be easily changed in Alfresco. The reason for it is that they are automatically generated and whenever user tries to change the value it gets overwritten. This post provides solution that allows to change values of the following properties:

  • cm:creator
  • cm:modifier
  • cm:created
  • cm:modified
  • cm:accessed

Solution

Let’s assume that we defined behaviour that changes values of cm:creator and cm:modifier whenever its value is changed. The logic to do so is implemented in onUpdateProperties method. To change the values first behaviour related cm:auditable aspect has to be disabled, then values have to be changed and then the behaviour has to be enabled. It is presented in the code below.

public class ChangeAuditablePropertiesBehaviour implements NodeServicePolicies.OnUpdatePropertiesPolicy { 
 
    // Dependencies
    private PolicyComponent policyComponent;
    private BehaviourFilter policyBehaviourFilter;
 
    // Behaviours
    private Behaviour onUpdateProperties;
 
    public void setPolicyComponent(PolicyComponent policyComponent) {
        this.policyComponent = policyComponent;
    }
 
    public void setPolicyBehaviourFilter(BehaviourFilter policyBehaviourFilter) {
        this.policyBehaviourFilter = policyBehaviourFilter;
    }
 
    public void init() {
 
        // Create behaviours
        this.onUpdateProperties = new JavaBehaviour(this, "onUpdateProperties", Behaviour.NotificationFrequency.TRANSACTION_COMMIT);
 
        // Bind behaviours to node policies
        policyComponent.bindClassBehaviour(NodeServicePolicies.OnUpdatePropertiesPolicy.QNAME, ContentModel.TYPE_CONTENT, onUpdateProperties);
    }
 
    @Override
    public void onUpdateProperties(NodeRef nodeRef, Map<QName, Serializable> before, Map<QName, Serializable> after) {
 
        // Disable auditable aspect to allow change properties of cm:auditable aspect
        policyBehaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE);
 
        // Update preferences of cm:auditable aspect
        setProperty(nodeRef, ContentModel.PROP_CREATOR);
        setProperty(nodeRef, ContentModel.PROP_MODIFIER);
 
        // Enable auditable aspect
        policyBehaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE);
    }
}

Bean definition:

 <bean id="changeAuditablePropertiesBehaviour" class="com.jmuras.ChangeAuditablePropertiesBehaviour" init-method="init">
        <property name="policyComponent" ref="policyComponent" />
        <property name="policyBehaviourFilter" ref="policyBehaviourFilter" />
    </bean>

Introduction

The purpose of this blog is to show how to enable search for multi-valued properties in advanced search form in Alfresco. Let’s assume that we are looking for authors and we want to find all the documents created by author John or Mary. This feature allows to do this in single query instead of two queries.

Solution

Multi-valued search is already supported in Alfresco, but requires additional configuration. This can be done by adding additional hidden field to advanced search form. The field should follow the name format prop:{name of the filed with values separated by commas}-mode and defines logical operator that should be used to replace commas.

 <config evaluator="model-type" condition="cm:content">
        <forms>
            <!-- Search form -->
            <form id="search">
                <field-visibility>
 
                    <!-- Author field-->
                    <show id="cm:author" force="true" />
                    <!-- Field to enable multi-value author support-->
                    <show id="prop:cm:author-mode" force="true"/>
 
                </field-visibility>
                <appearance>
 
                    <field id="cm:author" set="document_additional"/>
                    <!-- Use OR logical operator for authors -->
                    <field id="prop:cm:author-mode" set="document_additional">
                        <control template="/org/alfresco/components/form/controls/hidden.ftl">
                            <control-param name="contextProperty">OR</control-param>
                        </control>
                    </field>
 
                </appearance>
            </form>
        </forms>
    </config>

In addition, hidden.ftl form control has to be modified to support providing field value in contextProperty parameter. The changes are presented below:

<#-- Renders a hidden form field for edit and create modes only -->
<#assign fieldValue = "">
<#if field.control.params.contextProperty??>
   <#if context.properties[field.control.params.contextProperty]??>
      <#assign fieldValue = context.properties[field.control.params.contextProperty]>
   <#elseif args[field.control.params.contextProperty]??>
      <#assign fieldValue = args[field.control.params.contextProperty]>
   <#else>
       <#assign fieldValue = field.control.params.contextProperty>
   </#if>
<#elseif context.properties[field.name]??>
   <#assign fieldValue = context.properties[field.name]>
<#else>
   <#assign fieldValue = field.value>
</#if>
 
<#if form.mode == "edit" || form.mode == "create">
   <input type="hidden" name="${field.name}" 
          <#if field.value?is_number>value="${fieldValue?c}"<#else>value="${fieldValue?html}"</#if> />
</#if>