Fork me on GitHub

n. Slang a rough lawless young Kuali developer.
[perhaps variant of Houlihan, Irish surname]
kualiganism n

Blog of an rSmart Java Developer. Full of code examples, solutions, best practices, et al.

Saturday, April 2, 2011

Implementing KC 2.0 as an Overlay

Overview

Maven overlays are a way to take 2 web application projects and combine them with one overlaying the other. It sounds like it would be interesting if you want to use another project as the base for your own, right? Almost as if it is slightly wrong. This only makes sense when your project builds on that of another. Literally building upon another project as a foundation. You would only make another project the foundation when you are in absolute control of that other project. Kuali is community source, so the community runs the foundation project in this case. The other project is intended to be an implementation of Kuali Coeus where the implementing institution is a member of the community. Then technically, an overlay makes sense. You wouldn't want to make an overlay of just any project you found that you thought was cool and you wanted to change. It would be much better to fork that project instead. If you overlay, you run the risk of the foundation changing from underneath you. When implementing Kuali software, this is fine because you have the community supporting you and the software.

The Problem with Enterprise Software Maintenance at a University

To truly understand why overlays are a good idea at institutions and particularly universities, you need to understand the problem universities have had implementing software pretty much throughout history.

Universities are used to getting this software implemented, but as time goes business processes and practices change. Business is changing. Universities are changing. Why shouldn't their business practices change? It makes sense. The trouble is that they bankrupt their budgets on implementing the software and have nothing left to maintain it. All they budget for maintaining it is fixing leaks and bandaiding. Eventually, these institutions are left with failing systems on the brink of demise. It all comes down to maintenance. These systems can't just be good now. They need the potential to be good 10 years from now.

Kuali is no different in that respect. Compared to other enterprise software, the source code is very extensive. Making bug fixes to it will have effects on upgrading. Every change an institution adds to the source code, that does not get back to the trunk will cause problems because the institution's code base differs that much from that which is upgraded. The mindset is change as little of the original codebase as possible. Only build upon it if possible. If you're going to change something, find a way to do it without modifying the original distribution. No matter what you do, keep a record between versions so we know what changed. Among all of these, the consistent idea is to modify the distribution as little as possible.

That is what overlays allow. Modifying and customizing the distribution by overlaying it affords institutions the ability to
  • make changes without making patching or upgrading difficult in the future
  • track what changes you made
  • simplify your local distribution

The Screencast


This is a screencast based on the instructions laid out in KC 2.0 Customization.

Instructions

Written instructions for following along with the screencast.

1 Checkout the KC Project

First, you need to download the full KC project. I created a path in my workspace to store all this.
% mkdir -p .workspace/rsmart
Then checkout the source code from it. I used export because eventually, I want to import this into my own svn repository
% cd .workspace/rsmart
% svn export https://test.kuali.org/svn/kc_project/tags/kc-release-2_0-tag

The above creates a new kc-release-2_0-tag directory.

2 Install KC WAR and JAR Files

To install the WAR file in our maven repository, we use
% mvn -Dmaven.test.skip=true install

To create the JAR file, we use
% mvn jar:jar

Installing the JAR is a little different.
% mvn install:install-file -Dpackaging=jar -DgroupId=org.kuali.kra -DartifactId=kc_project -Dversion=2.0 -DgeneratePom=true -Dfile=target/kc_project-2.0.jar

That should be the end of our work with the kc_project.

3 Setup kc_custom

Create the directory structure and pom.xml.

3.1 Create Directory Structure

% mkdir kc
% mkdir -p kc/src/main/java/com/rsmart/kuali/kc
% mkdir -p kc/src/main/java/org/kuali/kra/infrastructure
% mkdir -p kc/src/main/config
% mkdir -p kc/src/main/resources/com/rsmart/kuali/kc
% mkdir -p kc/src/main/webapp/WEB-INF/

3.1 Copy Some Files Over

Might as well copy a couple files from the kc_project.
% cp kc-release-2_0-tag/src/main/webapp/WEB-INF/web.xml kc/src/main/webapp/WEB-INF/
% cp kc-release-2_0-tag/src/main/java/org/kuali/kra/infrastructure/KraServiceLocator.java kc/src/main/java/org/kuali/kra/infrastructure

3.2 Create pom.xml

The new overlay project needs its own pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.rsmart.kuali.kc</groupId>
<artifactId>kc</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>kc</name>
<url>http://maven.apache.org</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.kuali.kra</groupId>
<artifactId>kc_project</artifactId>
<version>2.0</version>
<type>war</type>
</dependency>
<dependency>
<groupId>org.kuali.kra</groupId>
<artifactId>kc_project</artifactId>
<version>2.0</version>
<scope>provided</scope>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.kuali.rice</groupId>
<artifactId>rice-kns</artifactId>
<version>1.0.2.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>kuali</id>
<name>Kuali Repository</name>
<url>https://test.kuali.org/maven</url>
<snapshots><enabled>true</enabled></snapshots>
</repository>
<repository>
<id>codehaus</id>
<name>Codehaus</name>
<url>http://dist.codehaus.org</url>
</repository>
<repository>
<id>apache</id>
<name>apache</name>
<url>http://people.apache.org/repo/m2-ibiblio-rsync-repository</url>
</repository>
<repository>
<id>jboss</id>
<name>jboss</name>
<url>http://repository.jboss.com/maven2</url>
</repository>
<repository>
<id>atlassian</id>
<name>atlassian</name>
<url>http://maven.atlassian.com/repository/public</url>
</repository>
<repository>
<snapshots />
<id>maven-repo1</id>
<name>maven2 repo</name>
<url>http://repo1.maven.org/maven2</url>
</repository>

</repositories>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.1.1</version>
<configuration>
<overlays>
<overlay>
<groupId>org.kuali.kra</groupId>
<artifactId>kc_project</artifactId>
</overlay>
</overlays>
</configuration>
</plugin>
</plugins>
</build>
</project>

The important parts to notice are the
  • dependencies - there are 2 kc_project dependencies. One is for the JAR we installed and the other is for the WAR.
        <dependencies>
    <dependency>
    <groupId>org.kuali.kra</groupId>
    <artifactId>kc_project</artifactId>
    <version>2.0</version>
    <type>war</type>
    </dependency>
    <dependency>
    <groupId>org.kuali.kra</groupId>
    <artifactId>kc_project</artifactId>
    <version>2.0</version>
    <scope>provided</scope>
    <type>jar</type>
    </dependency>
    Notice that the JAR dependency uses the provided flag. This states how to use the jars in classpath when building. Here is an excerpt from Maven POM Reference
    provided - this is much like compile, but indicates you expect the JDK or a container to provide it at runtime. It is only available on the compilation and test classpath, and is not transitive.
  • repositories - I added the rice repository to pick up all the Rice dependencies at build time
     <repositories>
    <repository>
    <id>kuali</id>
    <name>Kuali Repository</name>
    <url>https://test.kuali.org/maven</url>
    <snapshots><enabled>true</enabled></snapshots>
    </repository>
    </repositories>
  • plugins - here's what actually does the overlay
      <build>
    <plugins>
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-war-plugin</artifactId>
    <version>2.1.1</version>
    <configuration>
    <overlays>
    <overlay>
    <groupId>org.kuali.kra</groupId>
    <artifactId>kc_project</artifactId>
    </overlay>
    </overlays>
    </configuration>
    </plugin>
    </plugins>
    </build>

4 Add a Custom O/R Mapping File

Create a file in src/main/resources/com/rsmart/kuali/kc which is my institution's module path. I call it rsmart-repository.xml
<?xml version="1.0" encoding="UTF-8"?>
<descriptor-repository version="1.0">
</descriptor-repository>

5 Add a Custom Spring Beans File

To load our O/R mapping, we'll need to wire it up with Spring. Kuali has a facility to handle this. We just create a CustomSpringBeans.xml file
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2005-2010 The Kuali Foundation.

Licensed under the Educational Community License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.opensource.org/licenses/ecl1.php

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
<bean id="customModuleConfiguration-parentBean" class="org.kuali.rice.kns.bo.ModuleConfiguration" abstract="true">
<property name="databaseRepositoryFilePaths">
<list>
<value>com/rsmart/kuali/kc/rsmart-repository.xml</value>
</list>
</property>
</bean>
</beans>

6 Add Custom Struts Config XML File

Struts has this concept of a context specific configuration where you can have more than one configuration. This is very helpful, but we need to list ours in the web.xml and create it. This is a bare one fresh for putting new forms, actions, forwards, etc... into src/main/webapp/WEB-INF/struts-custom-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.2//EN" "http://struts.apache.org/dtds/struts-config_1_2.dtd" [
<!ENTITY protocol_forwards SYSTEM "struts_protocol_forwards.xml">
]>
<struts-config>
<data-sources>
</data-sources>

<form-beans>
</form-beans>

<global-exceptions>
</global-exceptions>

<global-forwards>
</global-forwards>

<action-mappings>
</action-mappings>

<controller processorClass="org.kuali.kra.web.struts.action.KraRequestProcessor" />
<message-resources factory="org.kuali.rice.kns.web.struts.action.KualiPropertyMessageResourcesFactory" parameter="" />
<plug-in className="org.kuali.kra.web.struts.action.GlobalFormatterRegistry" />
</struts-config>

Of course, I change the web.xml from
>servlet>
>servlet-name>action>/servlet-name>
>servlet-class>org.kuali.rice.kns.web.struts.action.KualiActionServlet>/servlet-class>
>init-param>
>param-name>config>/param-name>
>param-value>/WEB-INF/struts-config.xml>/param-value>
>/init-param>
>init-param>
>param-name>debug>/param-name>
>param-value>3>/param-value>
>/init-param>
>init-param>
>param-name>detail>/param-name>
>param-value>3>/param-value>
>/init-param>
>load-on-startup>0>/load-on-startup>
>/servlet>

to
>servlet>
>servlet-name>action>/servlet-name>
>servlet-class>org.kuali.rice.kns.web.struts.action.KualiActionServlet>/servlet-class>
>init-param>
>param-name>config>/param-name>
>param-value>/WEB-INF/struts-config.xml,/WEB-INF/struts-custom-config.xml>/param-value>
>/init-param>
>init-param>
>param-name>debug>/param-name>
>param-value>3>/param-value>
>/init-param>
>init-param>
>param-name>detail>/param-name>
>param-value>3>/param-value>
>/init-param>
>load-on-startup>0>/load-on-startup>
>/servlet>

I just added ,/WEB-INF/struts-custom-config.xml to the parameter.

7 Replace the KraServiceLocator.java

This is a little strange because you are not extending, but overriding the KraServiceLocator class. That means that if there are any changes made to it, you will probably not pick those up unless you explicitly know. This is of course, a maintenance issue, but we're only making minor changes. Remaking them is not a hassle.

7.1 Add the Custom Spring Beans as a Constant

First, we create a constant in KraServiceLocator called CUSTOM_SPRING_BEANS
private static final String CUSTOM_SPRING_BEANS = "com/rsmart/kuali/kc/CustomSpringBeans.xml";

7.2 Add the Constant to the springFiles Array

Now we make use of the constant.
...
...
private static final class ContextHolder {

static String[] springFiles = new String[] {COMMON_SPRING_BEANS,BUDGET_SPRING_BEANS, AWARD_SPRING_BEANS, IRB_SPRING_BEANS, COMMITTEE_SPRING_BEANS,
INSTITUTIONAL_PROPOSAL_SPRING_BEANS, QUESTIONNAIRE_SPRING_BEANS, TIME_AND_MONEY_SPRING_BEANS, CUSTOM_SPRING_BEANS};
...
...
}

8 Love

Now you have your overlay project. Just run the following to create your war.
% mvn -Dmaven.test.skip=true package

You will see a new WAR file in target
leo@behemoth~/.workspace/rsmart/kc
(18:54:16) [48] ls target/
classes kc-1.0-SNAPSHOT.war war
kc-1.0-SNAPSHOT maven-archiver

3 comments:

  1. In step 2 you use the command:
    mvn install:install-file -Dpackaging=jar -DgroupId=org.kuali.kra -DartifactId=kc_project -Dversion=2.0 -DgeneratePom=true -Dfile=target/kc_project-2.0.jar

    This generates an error for me:
    [ERROR] BUILD ERROR
    [INFO] ------------------------------------------------------------------------
    [INFO] Error installing artifact 'org.kuali.kra:kc_project:jar': Error installing artifact: File C:\foo\code\kc-2.0\target\kc_project-2.0.jar does not exist

    Should that last define be:
    -Dfile=target/kc_project-2.0.war
    ?

    ReplyDelete
  2. Anonymous, thank you very much for responding and for your feedback. It is much appreciated.

    The command I gave is correct, but I did skip a step. Both the WAR and JAR files need to be installed. The WAR was already installed with the previous install command. The JAR now needs to be installed but it does not exist.

    Prior to running the install command for the JAR, you want to run mvn jar:jar

    This will create the JAR that you are missing. Sorry for the confusion. I have already updated my post.

    Thanks again for the feedback.

    ReplyDelete
  3. I'm trying this out with KC 3.1.1 and the KraServiceLocator.java file is totally different from 2.0

    I can't find a reference to any of those constants in 3.1.1.

    Where should I put the reference to my CustomSpringBeans.xml file??

    Thanks

    ps. awesome screencast - please continue making these :)

    ReplyDelete