AppFuse: Igniting your applications with AppFuse

AppFuse: Igniting your Applications with AppFuse

By Ryan Withers, OCI Senior Software Engineer 

May 2008


Introduction

"How soon can you have it done?"

Sound all too familiar?

Managers over the years have been faced with ever increasing demands to produce more software over shorter delivery cycles. As the economic landscape has gone global, these pressures to deliver have increased dramatically.

For software developers this results in a responsibility to achieve the highest levels of productivity. Given this, any well meaning developer must constantly search for the appropriate tools for the job.

In the world of J2EE, many of us find ourselves writing a little code, adding a little configuration, writing a little code, adding a little configuration, so on and so forth. Much of our time is spent worrying about configuration, and this is time better spent on business logic.

Time spent configuring does not translate well into real value. In addition, it has a huge associated risk, in that configuration errors can introduce problems. In addition, writing the wrote boilerplate code to glue together the various layers of a system can be time consuming and monotonous work.

If this sounds familiar, read on to see how AppFuse can accelerate development cycles and help mitigate the risks associated with configuration-heavy projects.

AppFuse provides standard project templates, code generation, and a shell of application code to get projects up and running quickly. One of its primary strengths is the automation of configuration and setup, two things standard in most J2EE projects.

This article will cover the following three things:

  1. First, we will create a shell project.
  2. Second, we will extend the project by hand, adding a basic time tracking page.
  3. Third, we will wipe the slate clean, and create the same time tracking page again. Only this time we will use the appgen tool to perform complete code generation of the whole thing.>

History / Background

AppFuse is the brainchild of Matt Raible. There have been two major versions, however this article will focus on AppFuse Version 2. It is designed to make the process of creating a shell web application as turn-key as possible.

Version 1 utilized ant to perform the generation of each tier of a web application (courtesy the appgen command). In addition, version 1 supported four web frameworks.

In version 2, ant has been replaced by Maven, and additional classes have been added to provide a fully functional miniature web application template.

This application template is implemented in four supported web frameworks:

  1. JSF
  2. Spring MVC
  3. Struts 2
  4. Tapestry

These application templates are made available as Maven archetypes. In addition, the developer can choose between two different classifications of Archetype: basic or modular.

From the Quick-start guide (herein after referred to as "Quick-start"), the basic version packages the web application as a single deployable unit, while the modular version provides a separation between the web-interface and its back-end. This provides for the possibility of deploying multiple front-ends in the future. 

Before I knew anything about AppFuse, I was under the impression it was a framework. While it makes use of frameworks, it isn't necessarily a framework itself.

It would be far more accurate to say AppFuse is a distribution of open source libraries and tools. These libraries and tools have been integrated to create a rapid application development environment.

Downloaded out of the box, the shell application supports: login, security, and basic administrative functions.

The built in features include: file uploading, adding a user, modifying your profile, and more.

Some of the included packages are: the Spring Framework, Hibernate, Acegi, several packages/libraries from Jakarta Commons, ANTLR, Velocity, Log4j, jMock, not to mention any of the web frameworks.

As you can see, the list of supported third party libraries is long and extensive.

Introduction and Setup

The examples in the article were developed and tested using the software listed below:

  1. AppFuse
  2. Maven 2.0.8
  3. MySQL
  4. JDK 1.6

To begin we will follow the Quick-start to the letter, to show how easy it is to create the shell application. Follow the steps below from the command line to create the example application. There is much more information in the Quick-start, this is merely for convenient reference. The exercise below will set us up to create a custom extension later on.

  1. mvn archetype:create
  2. -DarchetypeGroupId=org.appfuse.archetypes
  3. -DarchetypeArtifactId=appfuse-basic-struts
  4. -DremoteRepositories=http://static.appfuse.org/releases
  5. -DarchetypeVersion=2.0.1 -DgroupId=com.oci.jnb -DartifactId=timeEntry
mvn appfuse: full-source output
  1. <!-- Database settings -->
  2. <dbunit.dataTypeFactoryName>
  3. org.dbunit.dataset.datatype.DefaultDataTypeFactory
  4. </dbunit.dataTypeFactoryName>
  5. <dbunit.operation.type>CLEAN_INSERT</dbunit.operation.type>
  6. <hibernate.dialect>
  7. org.hibernate.dialect.MySQL5InnoDBDialect
  8. </hibernate.dialect>
  9. <jdbc.groupId>mysql</jdbc.groupId>
  10. <jdbc.artifactId>mysql-connector-java</jdbc.artifactId>
  11. <jdbc.version>5.0.5</jdbc.version>
  12. <jdbc.driverClassName>com.mysql.jdbc.Driver</jdbc.driverClassName>
  13. <jdbc.url>
  14. <![CDATA[jdbc:mysql://localhost/timeEntryApp?createDatabaseIfNotExist=true
  15. &useUnicode=true&characterEncoding=utf-8]]>
  16. </jdbc.url>
  17. <jdbc.username>root</jdbc.username>
  18. <jdbc.password></jdbc.password>

This requires an instance of MySQL, so install the database and set a root password. The Quick-start has a reference for how to change the root password. Or just run the following commands, after installation: 

set password for root@localhost=PASSWORD('[chosen password]');
set password for root@host=PASSWORD('[chose password]');

There are two lines here because typically there are two entries for the root user: one with the localhost, and another with the system's host name (Dyer, pg 14.). For more information consult the online help manual.

  1. [INFO] ------------------------------------------------------------------------
  2. [INFO] BUILD SUCCESSFUL
  3. [INFO] ------------------------------------------------------------------------
AppFuse login

The code base for the shell application is light, and full featured. It is comprised of just 81 source and test files right out of the box, another reason I didn't hesitate to download it. One thing practioners of (TDD) Test Driven Development will appreciate is the code comes with a full suite of Unit Tests which pass without trouble.

During my AppFuse education I downloaded projects for all of the supported Archetypes, which proved to be quite an easy task. As stated, one of the motivations of AppFuse is to make creating a basic web project as easy as creating one via an IDE. As you can tell from the experience above, AppFuse has initially succeeded in this goal. In the next section, I address extending this application, because after all there are business requirements begging to be implemented.

Extending the example AppFuse application

The rest of the article will focus on extending and modifying the shell appfuse application. The goal is to add a form to the application which will support time entry. The following activities will be required:

  1. Create an entity class, and generate it's corresponding table
  2. Write a DAO and corresponding unit tests.
  3. Create a Manager class which wraps the DAO above as a service like facade.
  4. Build out and generate the necessary additions to the Struts2 web tier.

Creating an entity

AppFuse defaults to hibernate as the persistence architecture, and this project will make use of the default without modification. Now in the com.oci.jnb.timeEntry package create a com.oci.jnb.timeEntry.model package, and in this new package create a TimeEntry.java class. The new TimeEntry class should be annotated with the following annotations: @Entity and an @Table(name="time_entry"). These annotations tell AppFuse we want to create a table called time_entry which will hold data persisted by the dao. Please see below for the source:

  1. 1 package com.oci.jnb.timeEntry.model;
  2. 2
  3. 3 import javax.persistence.Column;
  4. 4 import javax.persistence.Entity;
  5. 5 import javax.persistence.GeneratedValue;
  6. 6 import javax.persistence.GenerationType;
  7. 7 import javax.persistence.Id;
  8. 8 import javax.persistence.Table;
  9. 9 import com.oci.jnb.model.BaseObject;
  10. 10
  11. 11 /**
  12.  12 * Time Entry Entity created for extension of AppFuse basic application.
  13.  13 *
  14.  14 * @author rwithers
  15.  15 */
  16. 16 @Entity
  17. 17 @Table(name="time_entry")
  18. 18 public class TimeEntry extends BaseObject {
  19. 19
  20. 20 private Long id;
  21. 21 private String description;
  22. 22 private double hours;
  23. 23 private String project;
  24. 24 private String projectNumber;
  25. 25
  26. 26 @Id @GeneratedValue(strategy = GenerationType.AUTO)
  27. 27 public Long getId() {
  28. 28 return id;
  29. 29 }
  30. 30 public void setId(long id) {
  31. 31 this.id = id;
  32. 32 }
  33. 33
  34. 34 @Column(name="description", length=255)
  35. 35 public String getDescription() {
  36. 36 return description;
  37. 37 }
  38. 38 public void setDescription(String description) {
  39. 39 this.description = description;
  40. 40 }
  41. 41
  42. 42 @Column(name="hours")
  43. 43 public double getHours() {
  44. 44 return hours;
  45. 45 }
  46. 46 public void setHours(double hours) {
  47. 47 this.hours = hours;
  48. 48 }
  49. 49
  50. 50 @Column(name="project")
  51. 51 public String getProject() {
  52. 52 return project;
  53. 53 }
  54. 54 public void setProject(String project) {
  55. 55 this.project = project;
  56. 56 }
  57. 57
  58. 58 @Column(name="project_number")
  59. 59 public String getProjectNumber() {
  60. 60 return projectNumber;
  61. 61 }
  62. 62 public void setProjectNumber(String projectNumber) {
  63. 63 this.projectNumber = projectNumber;
  64. 64 }
  65. 65
  66. 66 @Override
  67. 67 public String toString() {
  68. 68 StringBuilder builder = new StringBuilder();
  69. 69
  70. 70 builder.append("TimeEntry { id= " + this.id);
  71. 71 builder.append(", description=" + this.description);
  72. 72 builder.append(", hours=" + this.hours);
  73. 73 builder.append(", project=" + this.project);
  74. 74 builder.append(", projectNumber=" + this.projectNumber + "}");
  75. 75
  76. 76 return builder.toString();
  77. 77 }
  78. 78
  79. 79 @Override
  80. 80 public boolean equals(Object o) {
  81. 81 if (this == o) {
  82. 82 return true;
  83. 83 }
  84. 84
  85. 85 if ((o == null) || getClass() != o.getClass()) {
  86. 86 return false;
  87. 87 }
  88. 88
  89. 89 TimeEntry entry = (TimeEntry) o;
  90. 90
  91. 91 if (description != null ?
  92. 92 !description.equals(entry.description) :
  93. 93 entry.description != null) {
  94. 94 return false;
  95. 95 }
  96. 96
  97. 97 if (hours != entry.hours) { return false; }
  98. 98
  99. 99 if (project != null ?
  100. 100 !project.equals(entry.project) : entry.project != null) {
  101. 101 return false;
  102. 102 }
  103. 103 if (projectNumber != null ?
  104. 104 !projectNumber.equals(entry.projectNumber) :
  105. 105 entry.projectNumber != null) {
  106. 106 return false;
  107. 107 }
  108. 108
  109. 109 return true;
  110. 110 }
  111. 111
  112. 112 @Override
  113. 113 public int hashCode() {
  114. 114 int result = 0;
  115. 115
  116. 116 result = (description != null ? description.hashCode() : 0);
  117. 117 result = result * (int)hours;
  118. 118 result = result * (project != null ? project.hashCode() : 0);
  119. 119
  120. 120 result = result *
  121. 121 (projectNumber != null ? projectNumber.hashCode() : 0);
  122. 122
  123. 123 return result;
  124. 124 }
  125. 125
  126. 126 }

One thing which should be immediately apparent is the extension of BaseObject. This is an AppFuse class that makes the toString(), equals(), and hashCode() methods abstract forcing developers to implement them. For an in depth discussion on the contract of the Object methods equals() and hashCode() please see the following chapter from Joshua Bloch's book Effective Java.

At this point logging into the database and performing a show tables should yield the following: 

Initial Database View

Now that the TimeEntry.java entity class has been created, a couple more steps are required before the table will show up in the database.

1.   Edit the hibernate.cfg.xml adding the following entry:

<mapping class="com.oci.jnb.timeEntry.model.TimeEntry"></mapping>

      Please note when the project is created there are two hibernate.cfg.xml files, one in a test area and one in the main area. You'll want to make sure you modify the file in the timeEntry/src/main/resources directory.

2.    Generate the appropriate ddl with mvn test-compile hibernate3:hbm2ddl. Please note, this task should also update the database.

The resulting listing of tables now shows the following, note the addition of time_entry:

Database After New Table

Build a TimeEntry DAO and UnitTest

Now that a table exists to hold time entries, we can move on to building out the dao. There are two classes which support generic crud functionality, these are GenericDaoHibernate and UniversalDaoHibernate. The difference between these is the universal places the burden of casting on the developer, while the generic allows for the specification of a parameterized type via generics. The TimeEntryDAO will be extended with the GenericDaoHibernate class. There are a series of steps to take in making the DAO available to the rest of the application, these include:

1.    Modify the applicationContext-dao.xml file in timeEntry/src/main/resources by adding the following xml fragment to wire the DAO for setter based injection.

  1. <bean id="timeEntryDao"
  2. class="com.oci.jnb.timeEntry.dao.hibernate.TimeEntryDaoImpl">
  3. <property name="sessionFactory" ref="sessionFactory"></property>
  4. </bean>

2.    Create the interface for TimeEntryDao, shown below. 

  1. 1 package com.oci.jnb.timeEntry.dao;
  2. 2
  3. 3 import com.oci.jnb.timeEntry.model.TimeEntry;
  4. 4 import java.util.List;
  5. 5 import org.appfuse.dao.GenericDao;
  6. 6
  7. 7 /**
  8.  8 * @author rwithers
  9.  9 */
  10. 10 public interface TimeEntryDao extends GenericDao<TimeEntry, Long> {
  11. 11
  12. 12 /**
  13. 13 * Find a list of time entries by project number.
  14. 14 *
  15. 15 * @param projNumber
  16. 16 * @return
  17. 17 */
  18. 18 public List<TimeEntry> findByProjectNumber(String projNumber);
  19. 19
  20. 20 }

3.    Implement the interface in TimeEntryDaoImpl, shown below: 

  1. 1 package com.oci.jnb.timeEntry.dao.hibernate;
  2. 2
  3. 3 import com.oci.jnb.dao.hibernate.GenericDaoHibernate;
  4. 4 import com.oci.jnb.timeEntry.dao.TimeEntryDao;
  5. 5 import com.oci.jnb.timeEntry.model.TimeEntry;
  6. 6 import java.util.List;
  7. 7
  8. 8 /**
  9.  9 * This class implements TimeEntryDao and extends the
  10. 10 * GenericDaoHibernate class. The GenericDaoHibernate
  11. 11 * class is an AppFuse provided class that supports
  12. 12 * basic CRUD functionality. While the TimeEntryDao
  13. 13 * interface exposes custom retrieval methods as in
  14. 14 * the findByProjectNumber method shown below.
  15. 15 *
  16. 16 * @author rwithers
  17. 17 */
  18. 18 public class TimeEntryDaoImpl
  19. 19 extends GenericDaoHibernate<TimeEntry, Long>
  20. 20 implements TimeEntryDao {
  21. 21
  22. 22 public TimeEntryDaoImpl() {
  23. 23 super(TimeEntry.class);
  24. 24 }
  25. 25
  26. 26 /**
  27. 27 * Finds a list of time entries by project number.
  28. 28 *
  29. 29 * @param projNumber
  30. 30 * @return
  31. 31 */
  32. 32 public List<TimeEntry> findByProjectNumber(String projNumber) {
  33. 33 return getHibernateTemplate()
  34. 34 .find("from TimeEntry where projectNumber=?", projNumber);
  35. 35 }
  36. 36
  37. 37 }

4.    Build out the necessary unit test. At this point, it would be appropriate to mention TDD is suggested in the tutorials. There is no reason example couldn't be developed TDD. Just define the unit test shown below before defining the interface, and its corresponding implementation. Even working through the examples during the article, I found having a test at my disposal was indispensable. The unit test for the classes above should look like:

  1. 1 package com.oci.jnb.timeEntry.dao;
  2. 2
  3. 3 import com.oci.jnb.dao.BaseDaoTestCase;
  4. 4 import com.oci.jnb.timeEntry.model.TimeEntry;
  5. 5 import java.util.List;
  6. 6 import org.springframework.dao.DataAccessException;
  7. 7
  8. 8 /**
  9.  9 * This test class extends BaseDaoTestCase which is an
  10. 10 * AppFuse class used to support DAO testing. All test
  11. 11 * cases are method prepended with the word test in all
  12. 12 * lower case. The spring functionality is bootstrapped
  13. 13 * in the AbstractTransactionalDataSourceSpringContextTests
  14. 14 * class that the BaseDaoTestCase extends.
  15. 15 *
  16. 16 * @author rwithers
  17. 17 */
  18. 18 public class TimeEntryDAOTest extends BaseDaoTestCase {
  19. 19
  20. 20 private TimeEntryDao dao = null;
  21. 21
  22. 22 /**
  23. 23 * Setter provided for spring dependency injection.
  24. 24 *
  25. 25 * @param timeEntryDao
  26. 26 */
  27. 27 public void setTimeEntryDao(TimeEntryDao timeEntryDao) {
  28. 28 dao = timeEntryDao;
  29. 29 }
  30. 30
  31. 31 /**
  32. 32 * Test custom finder method.
  33. 33 */
  34. 34 public void testFindEntryByProjectNumber() {
  35. 35 List<TimeEntry> entries = dao.findByProjectNumber("JNB2508");
  36. 36 assertNotNull("expected entries, but didn't receive any", entries);
  37. 37 assertTrue(entries.size() > 0);
  38. 38 }
  39. 39
  40. 40 /**
  41. 41 * Test basic CRUD functionality.
  42. 42 *
  43. 43 * @throws java.lang.Exception
  44. 44 */
  45. 45 public void testAddAndRemoveTimeEntry() throws Exception {
  46. 46 TimeEntry entry = new TimeEntry();
  47. 47 entry.setDescription("Working on jnb tests.");
  48. 48 entry.setHours(32.0d);
  49. 49 entry.setProject("Jnb monthly news brief.");
  50. 50 entry.setProjectNumber("JNB2509");
  51. 51
  52. 52 entry = dao.save(entry);
  53. 53 flush();
  54. 54
  55. 55 TimeEntry entry2 = dao.get(entry.getId().longValue());
  56. 56
  57. 57 assertEquals("JNB2509", entry2.getProjectNumber());
  58. 58 assertNotNull(entry2.getId());
  59. 59
  60. 60 log.debug("removing entry...");
  61. 61
  62. 62 dao.remove(entry2.getId());
  63. 63 flush();
  64. 64
  65. 65 try {
  66. 66 dao.get(entry2.getId());
  67. 67 fail("Person found in database");
  68. 68 } catch (DataAccessException dae) {
  69. 69 log.debug("Expected exception: " + dae.getMessage());
  70. 70 assertNotNull(dae);
  71. 71 }
  72. 72 }
  73. 73
  74. 74 }

One additional requirement, before executing the above unit test setup some test data using dbunit. Don't worry, you probably have everything you need. This is one of the real strengths of the Maven / AppFuse combination. It pulls down the shell application packaged with a number of commonly used open source tools. The only thing to provide is in the xml fragment shown below as a part of the sample-data.xml file. This file can be found in the timeEntry/src/test/resources directory.

  1. <table name="time_entry">
  2. <column>id</column>
  3. <column>hours</column>
  4. <column>description</column>
  5. <column>project</column>
  6. <column>project_number</column>
  7. <row>
  8. <value description="id">1</value>
  9. <value description="hours">5.5</value>
  10. <value description="description">
  11. <![CDATA[Working on JNB until the wee hours of the morning!]]>
  12. </value>
  13. <value description="project">JNB</value>
  14. <value description="project_number">JNB2508</value>
  15. </row>
  16. </table>

5.  Now run the command mvn test or mvn test -Dtest=TimeEntryDAOTest and look for the results.

Creating the manager

AppFuse refers to business objects as "Managers". Managers are basically classes which provide a way of decoupling the Actions (in struts2) supporting the web interface, from any knowledge of the database. Managers are a good place to provide data level validation, and any business logic the application may need to support. AppFuse provides two "out-of-the-box" managers which support basic CRUD functionality, very similar to the DAO's. These managers are GenericManager and UniversalManager, GenericManager allows parameterization via generics while the UniversalManager relies on explicit casting. For the timeEntry addition, the TimeEntryManager will need to extend the GenericManager to provide additional functionality with a findByProjectNumber() method.

1.   The first thing to do is modify the applicationContext-service.xml file adding the row shown below. This entry specifies constructor injection to inject the dao implementation into the manager.

  1. <bean id="timeEntryManager"
  2. class="com.oci.jnb.service.impl.GenericManagerImpl">
  3. <constructor-arg ref="timeEntryDao"></constructor>
  4. </bean>

The new entry above will let the TimeEntryManager know about the TimeEntryDAO. If crud functionality is all that is needed then we'll create setters to take an interface of GenericManager and we're done. However, since TimeEntryManager will be extending functionality with a custom findBy routine we need to continue on.

2.   The next order of business is to create the manager interface, the source is shown below. 

  1. 1 package com.oci.jnb.timeEntry.service;
  2. 2
  3. 3 import com.oci.jnb.service.GenericManager;
  4. 4 import com.oci.jnb.timeEntry.model.TimeEntry;
  5. 5 import java.util.List;
  6. 6
  7. 7 /**
  8.  8 * @author rwithers
  9.  9 */
  10. 10 public interface TimeEntryManager
  11. 11 extends GenericManager<TimeEntry, Long> {
  12. 12
  13. 13 /**
  14. 14 * Find a list of time entries by project number.
  15. 15 *
  16. 16 * @param projNumber
  17. 17 * @return
  18. 18 */
  19. 19 public List<TimeEntry> findByProjectNumber(String projNumber);
  20. 20
  21. 21 }
  22.  

Please note this interface extends the GenericManager which provides a conduit to the basic CRUD functionality via standard inheritance. It will require us to extend the GenericManagerImpl class in the implementation, Both GenericManager and GenericManagerImpl are provided by the AppFuse framework.

3.   Now implement the findBy method in the custom implementation class below.

  1. 1 package com.oci.jnb.timeEntry.service.impl;
  2. 2
  3. 3 import com.oci.jnb.service.impl.GenericManagerImpl;
  4. 4 import com.oci.jnb.timeEntry.dao.TimeEntryDao;
  5. 5 import com.oci.jnb.timeEntry.model.TimeEntry;
  6. 6 import com.oci.jnb.timeEntry.service.TimeEntryManager;
  7. 7 import java.util.List;
  8. 8
  9. 9 /**
  10. 10 * This class exposes the service or business layer methods.
  11. 11 */
  12. 12 public class TimeEntryManagerImpl
  13. 13 extends GenericManagerImpl<TimeEntry, Long>
  14. 14 implements TimeEntryManager {
  15. 15
  16. 16 TimeEntryDao dao;
  17. 17
  18. 18 public TimeEntryManagerImpl(TimeEntryDao dao) {
  19. 19 super(dao);
  20. 20 this.dao = dao;
  21. 21 }
  22. 22
  23. 23 public List<TimeEntry> findByProjectNumber(String projNumber) {
  24. 24 return dao.findByProjectNumber(projNumber);
  25. 25 }
  26. 26
  27. 27 }
  28.  

4.   It should be noted constructor injection is provided via the addition to the applicationContext.xml file above using the spring IoC container. The unit test for all of this should look like the following code. Right now we're only exposing one custom method that doesn't have any business logic associated with it. However, the unit test we are adding is a relevant placeholder for future additions, as well as any changes to method logic or validations. 

  1. 1 package com.oci.jnb.timeEntry.service.impl;
  2. 2
  3. 3 import com.oci.jnb.service.impl.BaseManagerMockTestCase;
  4. 4 import com.oci.jnb.timeEntry.dao.TimeEntryDao;
  5. 5 import com.oci.jnb.timeEntry.model.TimeEntry;
  6. 6 import java.util.ArrayList;
  7. 7 import java.util.List;
  8. 8 import org.jmock.Mock;
  9. 9
  10. 10
  11. 11 /**
  12.  12 * @author rwithers
  13.  13 */
  14. 14 public class TimeEntryManagerImplTest
  15. 15 extends BaseManagerMockTestCase {
  16. 16
  17. 17 private TimeEntryManagerImpl manager = null;
  18. 18 private Mock dao = null;
  19. 19 private TimeEntry entry = null;
  20. 20
  21. 21 @Override
  22. 22 protected void setUp() throws Exception {
  23. 23 dao = new Mock(TimeEntryDao.class);
  24. 24 manager =
  25. 25 new TimeEntryManagerImpl((TimeEntryDao) dao.proxy());
  26. 26 }
  27. 27
  28. 28 @Override
  29. 29 protected void tearDown() throws Exception {
  30. 30 manager = null;
  31. 31 }
  32. 32
  33. 33 public void testGetTimeEntry() {
  34. 34 log.debug("testing getTimeEntry");
  35. 35
  36. 36 Long id = 7L;
  37. 37 entry = new TimeEntry();
  38. 38
  39. 39 // set expected behavior on dao
  40. 40 dao.expects(once()).method("get")
  41. 41 .with(eq(id))
  42. 42 .will(returnValue(entry));
  43. 43
  44. 44 TimeEntry result = manager.get(id);
  45. 45 assertSame(entry, result);
  46. 46 }
  47. 47
  48. 48 public void testGetEntries() {
  49. 49 log.debug("testing getEntries");
  50. 50
  51. 51 List entries = new ArrayList();
  52. 52
  53. 53 // set expected behavior on dao
  54. 54 dao.expects(once()).method("getAll")
  55. 55 .will(returnValue(entries));
  56. 56
  57. 57 List result = manager.getAll();
  58. 58 assertSame(entries, result);
  59. 59 }
  60. 60
  61. 61 public void testFindByProjectNumber() {
  62. 62 log.debug("testing findByProjectNumber");
  63. 63
  64. 64 List entries = new ArrayList();
  65. 65 String projectNumber = "JNB2509";
  66. 66
  67. 67 // set expected behavior on dao
  68. 68 dao.expects(once()).method("findByProjectNumber")
  69. 69 .with(eq(projectNumber))
  70. 70 .will(returnValue(entries));
  71. 71
  72. 72 List result =
  73. 73 manager.findByProjectNumber(projectNumber);
  74. 74 assertSame(entries, result);
  75. 75 }
  76. 76
  77. 77 public void testSaveTimeEntry() {
  78. 78 log.debug("testing saveTimeEntry");
  79. 79
  80. 80 entry = new TimeEntry();
  81. 81
  82. 82 // set expected behavior on dao
  83. 83 dao.expects(once()).method("save")
  84. 84 .with(same(entry))
  85. 85 .will(returnValue(entry));
  86. 86
  87. 87 manager.save(entry);
  88. 88 }
  89. 89
  90. 90 public void testRemoveTimeEntry() {
  91. 91 log.debug("testing removeTimeEntry");
  92. 92
  93. 93 Long id = 11L;
  94. 94 entry = new TimeEntry();
  95. 95
  96. 96 // set expected behavior on dao
  97. 97 dao.expects(once()).method("remove")
  98. 98 .with(eq(id))
  99. 99 .isVoid();
  100. 100
  101. 101 manager.remove(id);
  102. 102 }
  103. 103
  104. 104 }

Struts 2 Web Tier

To completely build out the struts web tier, start by putting together a couple JSP's. One of these JSP's will be for listing time entries, and another will be for editing. In addition, an action class and a corresponding unit test are needed. However, to be somewhat brief validations will be skipped. Please note, while validation is an important step for a full featured web application, validation will be, "an exercise left up to the reader". To start build out the action test, it will cover tests for adding, saving, deleting, and listing time entries. See below for the listing:

  1. 1 package com.oci.jnb.timeEntry.actions;
  2. 2
  3. 3 import com.oci.jnb.service.GenericManager;
  4. 4 import com.oci.jnb.timeEntry.model.TimeEntry;
  5. 5 import com.oci.jnb.webapp.action.BaseActionTestCase;
  6. 6 import com.opensymphony.xwork2.ActionSupport;
  7. 7 import org.apache.struts2.ServletActionContext;
  8. 8 import org.springframework.mock.web.MockHttpServletRequest;
  9. 9
  10. 10 /**
  11. 11 * @author rwithers
  12. 12 */
  13. 13 public class TimeEntryActionTest extends BaseActionTestCase {
  14. 14
  15. 15 private TimeEntryAction action;
  16. 16
  17. 17 @Override
  18. 18 protected void onSetUpBeforeTransaction() throws Exception {
  19. 19 super.onSetUpBeforeTransaction();
  20. 20 action = new TimeEntryAction();
  21. 21 GenericManager timeEntryManager =
  22. 22 (GenericManager) applicationContext.getBean("timeEntryManager");
  23. 23 action.setTimeEntryManager(timeEntryManager);
  24. 24
  25. 25 TimeEntry entry = new TimeEntry();
  26. 26 entry.setDescription("Working on JNB actions.");
  27. 27 entry.setHours(32.0d);
  28. 28 entry.setProject("JNB java yearly news brief.");
  29. 29 entry.setProjectNumber("JNB2510");
  30. 30 timeEntryManager.save(entry);
  31. 31 }
  32. 32
  33. 33 public void testSearch() throws Exception {
  34. 34 assertEquals(action.list(), ActionSupport.SUCCESS);
  35. 35 assertTrue(action.getEntries().size() >= 1);
  36. 36 }
  37. 37
  38. 38 public void testEdit() throws Exception {
  39. 39 log.debug("testing edit...");
  40. 40 action.setId(1L);
  41. 41 assertNull(action.getEntry());
  42. 42 assertEquals("success", action.edit());
  43. 43 assertNotNull(action.getEntry());
  44. 44 assertFalse(action.hasActionErrors());
  45. 45 }
  46. 46
  47. 47 public void testSave() throws Exception {
  48. 48 MockHttpServletRequest request = new MockHttpServletRequest();
  49. 49 ServletActionContext.setRequest(request);
  50. 50 action.setId(1L);
  51. 51 assertEquals("success", action.edit());
  52. 52 assertNotNull(action.getEntry());
  53. 53
  54. 54 // update last name and save
  55. 55 action.getEntry().setDescription("Updated Description");
  56. 56 assertEquals("input", action.save());
  57. 57 assertEquals("Updated Description", action.getEntry().getDescription());
  58. 58 assertFalse(action.hasActionErrors());
  59. 59 assertFalse(action.hasFieldErrors());
  60. 60 assertNotNull(request.getSession().getAttribute("messages"));
  61. 61 }
  62. 62
  63. 63 public void testRemove() throws Exception {
  64. 64 MockHttpServletRequest request = new MockHttpServletRequest();
  65. 65 ServletActionContext.setRequest(request);
  66. 66 action.setDelete("");
  67. 67 TimeEntry entry = new TimeEntry();
  68. 68 entry.setId(new Long(2L));
  69. 69 action.setEntry(entry);
  70. 70 assertEquals("error", action.delete());
  71. 71 assertNull(request.getSession().getAttribute("messages"));
  72. 72 }
  73. 73
  74. 74 }

Now in order to get the ActionTest class building add the TimeEntryAction.java class. This class extends a BaseAction class provided as part of the AppFuse basic struts2 project. The BaseAction in turn extends the struts 2 ActionSupport class which provides functionality for validation, error message handling, forwards, etc. The methods of this action class are associated with web pages via the struts.xml configuration file. The struts2 version of this file is a slimmer, trimmer version of the struts-config.xml file you may be familiar with from struts 1.x days. Once the implementation class has been added run the tests. This can be done via the mvn test goal, or for a specific unit test mvn test -Dtest=TimeEntryActionTest.

  1. 1 package com.oci.jnb.timeEntry.actions;
  2. 2
  3. 3 import com.oci.jnb.service.GenericManager;
  4. 4 import com.oci.jnb.timeEntry.model.TimeEntry;
  5. 5 import com.oci.jnb.webapp.action.BaseAction;
  6. 6 import java.util.List;
  7. 7
  8. 8 /**
  9.   9 * @author rwithers
  10.  10 */
  11. 11 public class TimeEntryAction extends BaseAction {
  12. 12
  13. 13 private static final String TIME_ENTRY_ID = "time_entry_id";
  14. 14
  15. 15 private GenericManager<TimeEntry, Long> timeEntryMgr;
  16. 16 private List entries;
  17. 17 private TimeEntry entry;
  18. 18 private Long id;
  19. 19
  20. 20 public void setTimeEntryManager(
  21. 21 GenericManager<TimeEntry, Long> manager) {
  22. 22 timeEntryMgr = manager;
  23. 23 }
  24. 24
  25. 25 public List getEntries() {
  26. 26 return entries;
  27. 27 }
  28. 28
  29. 29 public String list() {
  30. 30 entries = timeEntryMgr.getAll();
  31. 31 return SUCCESS;
  32. 32 }
  33. 33
  34. 34 public void setId(Long id) {
  35. 35 this.id = id;
  36. 36 log.debug("Setting id(Long) to : " + id);
  37. 37 getSession().setAttribute(TIME_ENTRY_ID, id);
  38. 38 getRequest().setAttribute(TIME_ENTRY_ID, id);
  39. 39 }
  40. 40
  41. 41 public TimeEntry getEntry() {
  42. 42 return entry;
  43. 43 }
  44. 44
  45. 45 public void setEntry(TimeEntry entry) {
  46. 46 this.entry = entry;
  47. 47 }
  48. 48
  49. 49 public String delete() {
  50. 50 String result;
  51. 51 Long localId = null;
  52. 52
  53. 53 localId = (Long) getSession().getAttribute(TIME_ENTRY_ID);
  54. 54 if (localId == null) {
  55. 55 localId =
  56. 56 (Long) getRequest().getAttribute(TIME_ENTRY_ID);
  57. 57 }
  58. 58 if (localId == null) {
  59. 59 result = ERROR;
  60. 60 } else {
  61. 61 timeEntryMgr.remove(localId);
  62. 62 saveMessage(getText("timeEntry.deleted"));
  63. 63 result = SUCCESS;
  64. 64 }
  65. 65
  66. 66 return result;
  67. 67 }
  68. 68
  69. 69 public String edit() {
  70. 70 Long localId = null;
  71. 71
  72. 72 localId = (Long) getSession().getAttribute(TIME_ENTRY_ID);
  73. 73
  74. 74 if (localId != null) {
  75. 75 entry = timeEntryMgr.get(localId);
  76. 76 } else {
  77. 77 entry = new TimeEntry();
  78. 78 }
  79. 79
  80. 80 return SUCCESS;
  81. 81 }
  82. 82
  83. 83 public String add() {
  84. 84 entry = new TimeEntry();
  85. 85 return SUCCESS;
  86. 86 }
  87. 87
  88. 88 public String save() throws Exception {
  89. 89 Long localId = null;
  90. 90
  91. 91 if (cancel != null) {
  92. 92 return "cancel";
  93. 93 }
  94. 94
  95. 95 if (delete != null) {
  96. 96 return delete();
  97. 97 }
  98. 98
  99. 99 localId = (Long) getSession().getAttribute(TIME_ENTRY_ID);
  100. 100
  101. 101 boolean isNew = (localId == null);
  102. 102
  103. 103 if (!isNew) {
  104. 104 entry.setId(localId);
  105. 105 log.debug("Stored session id value is: " + localId);
  106. 106 } else {
  107. 107 log.debug("Stored session id value is: null");
  108. 108 }
  109. 109
  110. 110 log.debug("Attempting to save entry: " + entry);
  111. 111
  112. 112 entry = timeEntryMgr.save(entry);
  113. 113
  114. 114 log.debug("Saved entry: " + entry);
  115. 115
  116. 116 String key = (isNew) ? "timeEntry.added" : "timeEntry.updated";
  117. 117 saveMessage(getText(key));
  118. 118
  119. 119 if (!isNew) {
  120. 120 return INPUT;
  121. 121 } else {
  122. 122 return SUCCESS;
  123. 123 }
  124. 124 }
  125. 125 }

Now add JSP's to expose a view of the basic CRUD functionality for the TimeEntry entity. Do this by adding two jsp's which support all of our necessary functionality, a timeEntryList.jsp and a timeEntryForm.jsp. Starting with timeEntryList below:

  1. 1 <%@ include file="/common/taglibs.jsp"%>
  2. 2
  3. 3 <head>
  4. 4 <title><fmt:message key="timeEntryList.title"></fmt:message></title>
  5. 5 <meta name="heading"
  6. 6 content="<fmt:message key='timeEntryList.heading'></fmt:message>"/>
  7. 7 </head>
  8. 8
  9. 9 <c:set var="buttons">
  10. 10
  11. 11 <input type="button"
  12. 12 style="margin-right: 5px"
  13. 13 onclick="location.href='<c:url value="/addEntry.html"></c:url>'"
  14. 14 value="<fmt:message key="button.add"></fmt:message>"/>
  15. 15
  16. 16 <input type="button"
  17. 17 onclick="location.href='<c:url value="/mainMenu.html"></c:url>'"
  18. 18 value="<fmt:message key="button.done"></fmt:message>"/>
  19. 19
  20. 20 </c:set>
  21. 21
  22. 22 <c:out value="${buttons}" escapeXml="false" ></c:out>
  23. 23
  24. 24 <s:set name="entries" value="entries" scope="request"></s:set>
  25. 25
  26. 26 <display:table name="entries"
  27. 27 class="table"
  28. 28 requestURI=""
  29. 29 id="entryList"
  30. 30 export="true"
  31. 31 pagesize="25">
  32. 32
  33. 33 <display:column property="id"
  34. 34 sortable="true"
  35. 35 href="editEntry.html"
  36. 36 paramId="id"
  37. 37 paramProperty="id"
  38. 38 titleKey="timeEntry.id"></display:column>
  39. 39
  40. 40 <display:column property="project"
  41. 41 sortable="true"
  42. 42 titleKey="timeEntry.project"></display:column>
  43. 43
  44. 44 <display:column property="description"
  45. 45 sortable="true"
  46. 46 titleKey="timeEntry.description"></display:column>
  47. 47
  48. 48 <display:setProperty
  49. 49 name="paging.banner.item_name"
  50. 50 value="entity"></display:setProperty>
  51. 51 <display:setProperty
  52. 52 name="paging.banner.items_name"
  53. 53 value="entities"></display:setProperty>
  54. 54
  55. 55 <display:setProperty
  56. 56 name="export.excel.filename"
  57. 57 value="TimeEntryList.xls"></display:setProperty>
  58. 58 <display:setProperty
  59. 59 name="export.csv.filename"
  60. 60 value="TimeEntryList.csv"></display:setProperty>
  61. 61 <display:setProperty
  62. 62 name="export.pdf.filename"
  63. 63 value="TimeEntryList.pdf"></display:setProperty>
  64. 64
  65. 65 </display:table>
  66. 66
  67. 67 <c:out value="${buttons}" escapeXml="false" ></c:out>
  68. 68
  69. 69 <script type="text/javascript">
  70. 70 highlightTableRows("entryList");
  71. 71 </script>

One thing to notice about the timeEntryList.jsp is the following line <s:set name="entries" value="entries" scope="request"/> which is calling the list() method against the action class we coded above. It knows how to do this via a configuration entry provided in the struts.xml configuration file. The entries are shown below:

  1. <action name="entries"
  2. class="com.oci.jnb.timeEntry.actions.TimeEntryAction"
  3. method="list">
  4. <result>/WEB-INF/pages/timeEntryList.jsp</result>
  5. </action>

Now in order to hookup the form actions for creating, updating, and deleting we will drop in the timeEntryForm.jsp. The same thing applies for the Form that applied above, an entry is needed in the struts.xml configuration file to wire the jsp to the action method it will call. In this case, there are a number of methods to wire up, these can be seen below from the fragment of struts.xml.

  1. <action name="editEntry"
  2. class="com.oci.jnb.timeEntry.actions.TimeEntryAction"
  3. method="edit">
  4. <result>/WEB-INF/pages/timeEntryForm.jsp</result>
  5. <result name="error">/WEB-INF/pages/timeEntryList.jsp</result>
  6. </action>
  7.  
  8. <action name="addEntry"
  9. class="com.oci.jnb.timeEntry.actions.TimeEntryAction"
  10. method="add">
  11. <result>/WEB-INF/pages/timeEntryForm.jsp</result>
  12. <result name="error">/WEB-INF/pages/timeEntryList.jsp</result>
  13. </action>
  14.  
  15. <action name="saveEntry"
  16. class="com.oci.jnb.timeEntry.actions.TimeEntryAction"
  17. method="save">
  18. <result name="input" type="redirect">entries.html</result>
  19. <result name="cancel" type="redirect">entries.html</result>
  20. <result name="delete" type="redirect">entries.html</result>
  21. <result name="success" type="redirect">entries.html</result>
  22. </action>
  23.  

The main thing to remember concerning the struts.xml file is the "actions" are dispatched when certain url patterns come in on the request. So the "entries" action corresponds to entries.html, so on and so forth. Please see the timeEntryForm.jsp below:

  1. 1 <%@ include file="/common/taglibs.jsp"%>
  2. 2
  3. 3 <head>
  4. 4 <title><fmt:message key="timeEntryDetail.title"></fmt:message></title>
  5. 5 <meta name="heading"
  6. 6 content="<fmt:message key='timeEntryDetail.heading'></fmt:message>"/>
  7. 7 </head>
  8. 8
  9. 9 <s:form id="timeEntryForm"
  10. 10 action="saveEntry"
  11. 11 method="post"
  12. 12 validate="true">
  13. 13 <s:hidden name="entry.id" value="%{entry.id}"></s:hidden>
  14. 14
  15. 15 <s:textfield key="entry.projectNumber"
  16. 16 required="true"
  17. 17 cssClass="text medium"></s:textfield>
  18. 18 <s:textfield key="entry.project"
  19. 19 required="true"
  20. 20 cssClass="text medium"></s:textfield>
  21. 21 <s:textfield key="entry.hours"
  22. 22 required="true"
  23. 23 cssClass="text medium"></s:textfield>
  24. 24 <s:textfield key="entry.description"
  25. 25 required="true"
  26. 26 cssClass="text medium"></s:textfield>
  27. 27
  28. 28 <li class="buttonBar bottom">
  29. 29 <s:submit cssClass="button"
  30. 30 method="save"
  31. 31 key="button.save"
  32. 32 theme="simple"></s:submit>
  33. 33 <c:if test="${not empty entry.id}">
  34. 34 <s:submit cssClass="button"
  35. 35 method="delete"
  36. 36 key="button.delete"
  37. 37 onclick="return confirmDelete('entry')"
  38. 38 theme="simple"></s:submit>
  39. 39 </c:if>
  40. 40 <s:submit cssClass="button"
  41. 41 method="cancel"
  42. 42 key="button.cancel"
  43. 43 theme="simple"></s:submit>
  44. 44 </li>
  45. 45 </s:form>
  46. 46
  47. 47 <script type="text/javascript">
  48. 48 Form.focusFirstElement($("timeEntryForm"));
  49. 49 </script>

The most important piece of this form is the following tag. This says, any time the form is submitted the action related to saveEntry will be executed. From the snippet of struts.xml above based on the result of the action the user will get redirected to entries.html. In fact, no matter what the result is, the user will get redirected to entries.html.

Struts 2 Basic, The Sequel

I believe nothing could punctuate the power of appfuse like repeating the steps above (seriously). With one caveat, it will only require writing one class and omitting the findByProjectId() method.

So to begin, go back and revisit, "Introduction and Setup," and while there perform each of the steps listed. Only this time, replace the timeEntry artifactId with timeEntrySequel so none of the new work will conflict with the previous work.

Once the timeEntrySequel application is up and running, copy the TimeEntry.java class from the old project (timeEntry) to the new project (timeEntrySequel).

The TimeEntry.java class should be put in the model directory, so be sure the package structure is modified appropriately.

One thing to note the directory/package in the previous project was com.oci.jnb.timeEntry.model, for the sequel the TimeEntry class should be copied to the com.oci.jnb.model package.

Running the command mvn appfuse:gen -Dentity= should generate everything now. After running the command look for the following output:

[info] [AppFuse] Installing generated files (pattern: **/*.java)...
[info] [AppFuse] Installing sample data for DbUnit...
[info] [AppFuse] Installing Spring bean definitions...
[info] [AppFuse] Installing Struts views and configuring...
[info] [AppFuse] Installing generated files (pattern: **/model/*.xml)...
[info] [AppFuse] Installing generated files (pattern: **/webapp/action/*.xml)...
[info] [AppFuse] Installing i18n messages...
[info] [AppFuse] Installing menu...
[info] [AppFuse] Installing UI tests...
------------------------------------------------------------------------
BUILD SUCCESSFUL
------------------------------------------------------------------------  

At this point the application server will need be restarted.

Navigate to http://localhost:8080 and notice the tab labeled "time entry list."

I won't argue that AppFuse is extremely powerful; I think it has been proven.

In the sequel project we generated our implementation code from a POJO; however code can also be generated from a table in the database. This can be done by running a mvn appfuse:gen-model. It will prompt the user for database table names. From the table names it will generate classes. These classes can then be passed to the mvn appfuse:gen command. The mvn appfuse:gen command can be customized in many different ways, controlling where source is generated, where templates can be found, whether or not installation occurs, etc.

If you happen to be working with an existing codebase, you can add the following option to the gen command from above -> appfuse:gen -Dentity=[Entity] -DfullPath=[Path to model classes]. The fullPath option should be expressed in java package format..

Architecture

There really isn't one architecture or design characteristic to AppFuse. A distinction could be made between the different types of project either basic or modular. However, it really is not an architecture, rather it is more like a collection of tools to help implement code for standard open source java frameworks. I've listed the AppFuse classes we have depended upon in order to build out the example, these include:

Application Dependencies

  1. com.oci.jnb.model.BaseObject
  2. com.oci.jnb.dao.GenericDao
  3. com.oci.jnb.dao.hibernate.GenericDaoHibernate
  4. com.oci.jnb.service.GenericManager
  5. com.oci.jnb.service.impl.GenericManagerImpl
  6. com.oci.jnb.webapp.action.BaseAction

Test Dependencies

  1. com.oci.jnb.webapp.action.BaseActionTestCase
  2. com.oci.jnb.dao.BaseDaoTestCase
  3. com.oci.jnb.service.impl.BaseManagerMockTestCase

It is interesting to note while these are "AppFuse" provided classes, they have the 'com.oci.jnb' extension you might expect the project codebase to have. This is by design, since the goal of the AppFuse project is to create a working shell project very quickly. Go back and re-examine the output from the mvn appfuse:full-source goal, notice a line which states 'Refactored all org.appfuse packages and paths to com.oci.jnb'. Since the project is designed using struts2, some of these classes are naturally extending struts2 classes. If another Archetype is chosen, the classes downloaded will support the Framework of that Archetype.

Conclusion

I think there is something almost magical about code generation – a bit like Houdini or Copperfield. We can now beg for requirements, only to wiggle free much faster than our project sponsors imagine.

AppFuse is definitely worth a look. There is nothing like being able to start out with a working application while avoiding all sorts of code and configuration.

Even if you don't use AppFuse for actual project work, it is the easiest way to get instant exposure to four of today's most popular web frameworks.

With this, I will leave a few parting thoughts I had while working with AppFuse:

  1. Join the mailing list

    Matt Raible and the AppFuse team care deeply about what they do, and it shows. After all a tool is only as good as the support which backs it up.

  2. Visit the forums

    Chances are, there are people out there who have "been there, and done that" before.

  3. The AppFuse project is a very ambitious project which has achieved a remarkable goal.

    The AppFuse framework brings together many "best of breed" technologies, in a way that makes it easy to get up and going with web development.

  4. Automation is not a replacement for knowledge, don't ever take knowledge for granted.

    The AppFuse framework should be seen as a productivity tool, it is not a replacement for knowledge in any of the technologies it packages.

Finally, I would like to give a special thanks to Bob Adelmann who happened to mention AppFuse in a conversation across the cubicle wall. Without his comment, this article would never have happened. I would also like to thank Lance Finney, Michael Kimberlin, Bob Adelmann, and Mark Volkmann for their reviews of the article.

References