Note: A similar version of the example below were tested on:
- Alfresco 4.1.5 with Windows default Tomcat install with PostGreSQL
- Alfresco 4.1.5 with JBoss 7.0.0 install with Oracle
- Created a maven database module using mybatis for persistence. This article helped immensely when setting this up.
- Included the CherryShoeStatusDao class, mybatis domain classes, mapper classes, and mapper xml files
- The key to accessing Alfresco's spring datasource is to reference it as "defaultDataSource" in the alfresco-custom-database spring configuration file's datasource bean. This is because the alfresco datasource is defined in core-services-context.xml with that spring bean id.
<!-- Alfresco's DataSource is obtained via a bean called "defaultDataSource" which is a org.apache.commons.dbcp.BasicDataSource, it's defined in WEB-INF/classes/alfresco/core-services-context.xml, simulating it here --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="defaultDataSource" />
</bean>
- Necessary dependencies included (versions were controlled by the parent pom so not shown here):
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> </dependency> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> </dependency>
- Add the database module as a module dependency to the alfresco amp module pom:
<dependency> <groupId>com.cherryshoe</groupId> <artifactId>alfresco-custom-database</artifactId> </dependency>
- Import the alfresco-custom-database.xml spring file to module-context.xml so it will be recognized by alfresco. The classpath should be from the root of the jar file created (/spring/spring-alfresco-custom-database.xml). Import it before any other imports that depend on it.
... other imports<!-- alfresco-custom-database spring config file --> <import resource="classpath:/spring/spring-alfresco-custom-database.xml" />
- cherryShoeStatusDao will already be available as a spring bean after step 3 is done because of component scanning in the custom database module spring configuration file. It can be referenced from other beans in alfresco's service-context.xml custom spring config file. i.e. a CustomAlfrescoService can now access CherryShoeStatusDao to insert and update Status table values.
<bean id="CustomAlfrescoService" class="com.cherryshoe.services.impl.CustomAlfrescoServiceImpl" > <property name="cherryShoeStatusDao" ref="cherryShoeStatusDao" /> </bean>
Detailed Files:
- The alfresco-custom-database follows the typical maven project structure
- java files
- database/dao/CherryShoeStatusDao.java
package com.cherryshoe.database.dao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.cherryshoe.database.domain.Status; import com.cherryshoe.database.persistence.StatusMapper; import com.google.common.base.Preconditions; @Service public class CherryShoeStatusDao { @Autowired private StatusMapper statusMapper; public Status getStatus(String statusId) { return statusMapper.getStatusById(statusId); } @Transactional public void insertStatus(Status status) { // check preconditions with google guava Preconditions.checkNotNull(status.getStatusCreateDate(), "statusCreateDate is required"); Preconditions.checkNotNull(status.getStatusModifiedDate(), "statusModifiedDate is required"); Preconditions.checkNotNull(status.getStatusId(), "statusId is required"); Preconditions.checkNotNull(status.getStatusRequest(), "statusRequest is required"); Preconditions.checkNotNull(status.getStatusStatus(), "statusStatus is required"); statusMapper.insertStatus(status); } @Transactional public void updateStatus(Status status) { // check preconditions with google guava statusMapper.updateStatus(status); } }
- database/domain/Status.java
package com.cherryshoe.database.domain; import java.io.Serializable; import java.sql.Timestamp; public class Status implements Serializable { private static final long serialVersionUID = 8751282105532159742L; private Timestamp statusCreatedDate; private Timestamp statusModifiedDate; private String statusId; private Integer statusStatus; private String statusRequest; public Timestamp getStatusCreateDate() { return statusCreatedDate; } public void setStatusCreateDate(Timestamp statusCreatedDate) { this.statusCreatedDate = statusCreatedDate; } public Timestamp getStatusModifiedDate() { return statusModifiedDate; } public void setStatusModifiedDate(Timestamp statusModifiedDate) { this.statusModifiedDate = statusModifiedDate; } public String getStatusId() { return statusId; } public void setStatusId(String statusId) { this.statusId = statusId; } public Integer getStatusStatus() { return statusStatus; } public void setStatusStatus(Integer statusStatus) { this.statusStatus = statusStatus; } public String getStatusRequest() { return statusRequest; } public void setStatusRequest(String statusRequest) { this.statusRequest = statusRequest; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((statusRequest == null) ? 0 : statusRequest.hashCode()); result = prime * result + ((statusStatus == null) ? 0 : statusStatus.hashCode()); result = prime * result + ((statusId == null) ? 0 : statusId.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Status other = (Status) obj; if (statusRequest == null) { if (other.statusRequest != null) return false; } else if (!statusRequest.equals(other.statusRequest)) return false; if (statusStatus == null) { if (other.statusStatus != null) return false; } else if (!statusStatus.equals(other.statusStatus)) return false; if (statusId == null) { if (other.statusId != null) return false; } else if (!statusId.equals(other.statusId)) return false; // do not put date in equals, they will never be equal return true; } @Override public String toString() { return "Status [statusCreatedDate=" + statusCreatedDate + ", statusModifiedDate=" + statusModifiedDate + ", statusId=" + statusId + ", statusStatus=" + statusStatus + ", statusRequest=" + statusRequest + "]"; } }
- database/domain/StatusEnum.java
package com.cherryshoe.database.domain; public enum StatusEnum { NOT_PROCESSED(-1), IN_PROCESS(1), PROCESSED(2); private int status; private StatusEnum(int status) { this.status = status; } public Integer getStatus() { return status; } }
- database/persistence/StatusMapper.java
package com.cherryshoe.database.persistence; import com.cherryshoe.database.domain.Status; public interface StatusMapper { Status getStatusById(String statusId); void insertStatus(Status status); void updateStatus(Status status); }
- resources files
- com/cherryshoe/database/persistance/StatusMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.cherryshoe.database.persistence.StatusMapper"> <cache /> <resultMap id="baseResultMap" type="com.cherryshoe.database.domain.Status"> <id column="STATUS_ID" property="statusId" /> <result column="STATUS_STATUS" property="statusStatus" /> <result column="STATUS_REQUEST" property="statusRequest" /> <result column="STATUS_CREATED_DATE" property="statusCreatedDate" /> <result column="STATUS_MODIFIED_DATE" prstatuserty="statusModifiedDate" /> </resultMap> <sql id="base_column_list"> STATUS_ID, STATUS_STATUS, STATUS_REQUEST, STATUS_CREATED_DATE, STATUS_MODIFIED_DATE </sql> <select id="getStatusById" parameterType="Long" resultMap="baseResultMap"> SELECT <include refid="base_column_list" /> FROM STATUS WHERE STATUS_ID = #{statusId} </select> <!-- Mapper does not allow the create date to be modified --> <update id="updateStatus" parameterType="com.cherryshoe.database.domain.Status"> UPDATE STATUS SET <if test="statusStatus != null"> STATUS_STATUS = #{statusStatus}, </if> <if test="statusRequest != null"> STATUS_REQUEST = #{statusRequest}, </if> <if test="statusModifiedDate != null"> STATUS_MODIFIED_DATE = #{statusModifiedDate} </if> WHERE STATUS_ID = #{statusId} </update> <insert id="insertStatus" parameterType="com.cherryshoe.database.domain.Status"> INSERT INTO STATUS ( <include refid="base_column_list" />) VALUES (#{statusId}, #{statusStatus}, #{statusRequest}, #{statusCreatedDate}, #{statusModifiedDate}) </insert> <!-- TODO MyBatis 3 does not map booleans to integers --> </mapper>
- spring/spring-alfresco-custom-database.xml
<?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:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> <!-- Alfresco's DataSource is obtained via a bean called "defaultDataSource" which is a org.apache.commons.dbcp.BasicDataSource, it's defined in WEB-INF/alfresco/core-services-context.xml, simulating it here --> <!-- transaction manager, use JtaTransactionManager for global tx --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="defaultDataSource" /> </bean> <!-- enable component scanning (beware that this does not enable mapper scanning!) --> <context:component-scan base-package="com.cherryshoe.database.dao" /> <!-- enable autowire --> <context:annotation-config /> <!-- enable transaction demarcation with annotations --> <tx:annotation-driven /> <!-- define the SqlSessionFactory --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="defaultDataSource" /> <property name="typeAliasesPackage" value="com.cherryshoe.database.domain" /> </bean> <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate"> <constructor-arg index="0" ref="sqlSessionFactory" /> </bean> <!-- scan for mappers and let them be autowired --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.cherryshoe.database.persistence" /> </bean> </beans>
/alfresco-custom-database <-- Maven pom.xml
/src
/main/
/java <-- Java code
/com/
/cherryshoe
/database
/dao <-- Dao logic
/domain <-- Business domain objects
/persistence <-- Mapper interfaces
/resources <-- Non java files
/com
/cherryshoe
/database
/persistence <-- Mapper XML files
/spring <-- Spring files
/scripts <-- Sql files (Oracle)
/test/
/java <-- Java code
/com/
/cherryshoe
/database
/dao <-- Dao logic tests
/resources <-- Non java files
/database <-- Sql in memory files (H2)
/spring <-- Spring test files
- Necessary test dependencies to use in-memory H2 database included:
<dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> </dependency> <dependency> <groupId>commons-dbcp</groupId> <artifactId>commons-dbcp</artifactId> </dependency> <!-- Unfortunately due the binary license there is no public repository with the Oracle Driver JAR. i.e. mvn install:install-file -Dfile=C:\ojdbc.jar -DgroupId=ojdbc -DartifactId=ojdbc -Dversion=6 -Dpackaging=jar --> <dependency> <groupId>ojdbc</groupId> <artifactId>ojdbc</artifactId> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> </dependency>
- test java files
- database/dao/CherryShoeStatusDaoTest.java
package com.cherryshoe.database.dao; import static org.junit.Assert.assertEquals; import java.sql.Timestamp; import java.util.Calendar; import java.util.Date; import java.util.UUID; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import com.cherryshoe.database.BaseTestCase; import com.cherryshoe.database.dao.CherryShoeStatusDao; import com.cherryshoe.database.domain.Status; import com.cherryshoe.database.domain.StatusEnum; import com.cherryshoe.utils.Utils; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations= { "/spring/spring-alfresco-custom-database-test.xml" }) public class CherryShoeStatusDaoTest extends BaseTestCase { @Autowired CherryShoeStatusDao service; @Test public void createStatusnotProcessed() { String id = UUID.randomUUID().toString(); Integer statusStatus = StatusEnum.NOT_PROCESSED.getStatus(); String statusRequest = "statusRequestTestCreateStatus"; Status status = createStatus(id, statusStatus, statusRequest); String statusId = status.getStatusId(); // test that it was created in DB Status retStatus = service.getStatus(statusId); System.out.println(retStatus.toString()); assertEquals(status, retStatus); } @Test public void updateStatus_inProcess() { String statusId = UUID.randomUUID().toString(); Integer statusStatus = StatusEnum.NOT_PROCESSED.getStatus(); String statusRequest = "statusRequestTestUpdateStatus"; Status status = createStatus(statusId, statusStatus, statusRequest); String retStatusId = status.getStatusId(); Integer updateStatusStatus = StatusEnum.IN_PROCESS.getStatus(); String updateStatusRequest = "statusRequestTestUpdateStatus2"; status.setStatusStatus(updateStatusStatus); status.setStatusRequest(updateStatusRequest); // new timestamp Date now = Calendar.getInstance().getTime(); Timestamp modifyTimestamp = new Timestamp(now.getTime()); status.setStatusModifiedDate(modifyTimestamp); // test that it was created in DB service.updateStatus(status); Status retStatus = service.getStatus(retStatusId); System.out.println(retStatus.toString()); assertEquals(retStatus.getStatusStatus(), updateStatusStatus); assertEquals(retStatus.getStatusRequest(), updateStatusRequest); } @Test public void createStatus_andupdate_processed() { String statusId = UUID.randomUUID().toString(); Integer statusStatus = StatusEnum.IN_PROCESS.getStatus(); String statusRequest = "statusRequestTestCreateStatus"; Status status = createStatus(statusId, statusStatus, statusRequest); String retStatusId = status.getStatusId(); // test that it was created in DB Status retStatus = service.getStatus(statusId); assertEquals(status, retStatus); // update status.setStatusStatus(StatusEnum.PROCESSED.getStatus()); // new timestamp Date now = Calendar.getInstance().getTime(); Timestamp modifyTimestamp = new Timestamp(now.getTime()); status.setStatusModifiedDate(modifyTimestamp); service.updateStatus(status); retStatus = service.getStatus(retStatusId); System.out.println(retStatus.toString()); assertEquals(status, retStatus); } protected Status createStatus(String statusId, Integer statusStatus, String statusRequest) { Status status = new Status(); status.setStatusId(statusId); status.setStatusRequest(statusRequest); status.setStatusStatus(statusStatus); Date now = Calendar.getInstance().getTime(); Timestamp timestamp = new Timestamp(now.getTime()); status.setStatusCreateDate(timestamp); status.setStatusModifiedDate(timestamp); service.insertStatus(status); return status; } }
- utils/Utils.java
package com.cherryshoe.utils; import java.util.Random; public class Utils { /* * Generate a 'unique' 8 digit id */ public static String get8DigitUniqueId() { Random r = new Random(); long l = 10000000 + r.nextInt(20000000); return Long.toString(l); } }
- test resources files
- database/alfresco-hsqldb-status-schema.sql
-- drop table STATUS; drop table status if exists create table status ( status_id varchar(64) not null, status_status int not null, status_request clob not null, status_created_date timestamp not null, status_modified_date timestamp not null, CONSTRAINT pk_status_id PRIMARY KEY (status_id) );
- spring/spring-alfresco-custom-database-test.xml
<?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:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> <!-- For unit integration testing, you can switch between the hsqldb and the oracle one. --> <!-- in-memory database and a datasource --> <jdbc:embedded-database id="defaultDataSource"> <jdbc:script location="classpath:database/alfresco-hsqldb-status-schema.sql"/> </jdbc:embedded-database> <!-- Alfresco's DataSource is obtained via a bean called "defaultDataSource" which is a org.apache.commons.dbcp.BasicDataSource, it's defined in WEB-INF/alfresco/core-services-context.xml, simulating it here --> <!-- For testing real oracle instance --> <!-- <bean id="defaultDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> --> <!-- <property name="driverClassName"> --> <!-- <value>oracle.jdbc.OracleDriver</value> --> <!-- </property> --> <!-- <property name="url"> --> <!-- <value>jdbc:oracle:thin:@localhost:1521:psrdb</value> --> <!-- </property> --> <!-- <property name="username"> --> <!-- <value>cherryshoe</value> --> <!-- </property> --> <!-- <property name="password"> --> <!-- <value>cherryshoe</value> --> <!-- </property> --> <!-- </bean> --> <!-- ********************************************************************************************** Basically, everything starting below has to go in the real spring-alfresco-custom-database.xml ********************************************************************************************** --> <!-- transaction manager, use JtaTransactionManager for global tx --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="defaultDataSource" /> </bean> <!-- enable component scanning (beware that this does not enable mapper scanning!) --> <context:component-scan base-package="com.cherryshoe.database.dao" /> <!-- enable autowire --> <context:annotation-config /> <!-- enable transaction demarcation with annotations --> <tx:annotation-driven /> <!-- define the SqlSessionFactory --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="defaultDataSource" /> <property name="typeAliasesPackage" value="com.cherryshoe.database.domain" /> </bean> <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate"> <constructor-arg index="0" ref="sqlSessionFactory" /> </bean> <!-- scan for mappers and let them be autowired --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.cherryshoe.database.persistence" /> </bean> </beans>
For more examples and ideas, I encourage you explore the links provided throughout this blog. What questions do you have about this post? Let me know in the comments section below, and I will answer each one.
Hi Judy,
ReplyDeleteVery nice article. First time using ibatis and mybatis-spring for me, but using this article and the very neat mybatis documentation as a basis, it turned to be very simple to understand. Thanks !
I just keep asking myself one question (which is more a spring question actually) : although it seems to work just fine, how comes the transactionManager bean declaration does not override the native Alfresco bean declared in the hibernate-context.xml ??
Hi Tony, It's great to hear that this article helped you, you are welcome.
DeleteOne of the spring beans, whether it be the one defined in the mybatis-spring config xml, or in alfresco's hibernate-context.xml must be taking precedence. Since I didn't get any errors when using bean id "transactionManager" in the mybatis-spring configuration when running the module by itself, or integrated with Alfresco; I did not realize I was using the same bean id!
When running unit and integration tests on the mybatis-module by itself, it definitely needed to have the transactionManager defined. BUT when running inside the alfresco web application, one must be overriding the other, not sure which one.
This article explained nicely why we didn't see an error when running alfresco with two duplicate bean id's:
http://stackoverflow.com/questions/5849192/springs-overriding-bean: Two different XML files, one is overriding the other depending on which bean is loaded last.
http://stackoverflow.com/questions/19034273/how-to-stop-overiding-a-bean-in-spring: You can explicitly disable the feature to diallow bean overriding.
Happy coding,
Judy
Hi Judy,
ReplyDeletefirstly thank you for this great tuto and secondly I can't figure out why I'm getting a javaNullPointerException.
Details:
Class CherryShoeStatusDao -> statusMapper.insertStatus(status);
Sorry, the problem was Alfresco doesn't read the spring-alfresco-custom-database.xml file by default
DeleteThanks
Hi Izzedine,
ReplyDeleteI clarified the need to import the spring-alfresco-custom-database.xml file in steps 3 and 4 of "High Level Steps".
Thanks,
Judy