Applicable: | This tutorial applies to the Atlassian reference application (Refapp). |
Level of experience: | This is an intermediate tutorial. You should have completed at least one beginner tutorial before working through this tutorial. |
Time estimate: | It should take you approximately 1 hour to complete this tutorial. |
We encourage you to work through this tutorial, and consult the source code when you run into trouble. This tutorial is split up into stages, where each stage is clearly marked in this documentation and we hope it will help you progress easily through this guide.
The source repository can be found here. To clone the repository, issue the following command:
1 2git clone git@bitbucket.org:serverecosystem/ao-tutorial.git
There are tags corresponding to each stage of the tutorial, e.g. if you wanted to see the progress at the end of stage 3, do:
1 2git checkout stage3
For the purpose of this tutorial we're going to use the Atlassian Refapp.
Creating the plugin, once the SDK is installed is as simple as running atlas-create-refapp-plugin
, and answering the questions asked.
We're going to use the following values for this guide:
com.atlassian.tutorial.ao.todo
Note: You may need to use a different folder to the downloaded source to avoid conflict.
The following instructions should apply to using Active Objects across Atlassian products, whether in JIRA, Confluence, Bamboo or the Refapp. There are some minor differences between the products, however. For example, the name of the plugin referenced in your pom.xml
will differ, as it follows the pattern maven-product-plugin
. See Getting Started for common information on developing Atlassian plugins and using the SDK, or a particular product development space -- like JIRA developer documentation, or Confluence Cloud or Server -- for product-specific information, if necessary.
You only need to add one dependency to your plugin's pom.xml
to enable Active Objects:
1 2<dependency> <groupId>com.atlassian.activeobjects</groupId> <artifactId>activeobjects-plugin</artifactId> <version>${ao.version}</version> <scope>provided</scope> </dependency>
where ao.version
is the version of Active Objects you're using. Find a list of all the versions here.
This tutorial was tested with version 1.2.3 of Active Objects.
You will need the following additional dependency to your pom.xml
:
1 2<!-- Google Collections, useful utilities for manipulating collections --> <dependency> <groupId>com.google.collections</groupId> <artifactId>google-collections</artifactId> <version>1.0</version> <scope>provided</scope> </dependency>
Refapp version
Be sure to set the version of the Refapp to 2.9+. If you're using a Plugin SDK prior to 3.3, it might not be new enough.
Stage 1
We've now completed stage 1 of this guide. Here is how to make sure that everything is working as expected:
Launch atlas-run
from the command line, and you should see the following message:
1 2[INFO] refapp started successfully and available at http://localhost:5990/refapp [INFO] Type CTRL-C to exit
Don't use CTRL-C
to kill this instance! We will use atlas-mvn package
to recompile our plugin and QuickReload will reload it for us, alleviating the need to restart refapp every time we make a change.
Go to the following URL localhost:5990/refapp/plugins/servlet/upm (if asked to log in, use admin/admin) and check that all the expected plugins are installed and enabled:
NOTE: If you're having issues, you might want to compare with my version of the code.
We're now ready to proceed…
First, we need to define the AO module, in the plugin descriptor (atlassian-plugin.xml).
1 2<ao key="ao-module"> <description>The module configuring the Active Objects service used by this plugin</description> <entity>com.atlassian.tutorial.ao.todo.Todo</entity> </ao>
Now we can create that first entity, com.atlassian.tutorial.ao.todo.Todo
as referenced in the module descriptor. All new entities must be declared in the module descriptor.
In the Active Objects world, an entity is defined by an interface. Our Todo
entity looks like this:
1 2package com.atlassian.tutorial.ao.todo; import net.java.ao.Entity; public interface Todo extends Entity { String getDescription(); void setDescription(String description); boolean isComplete(); void setComplete(boolean complete); }
It defines two fields, a String
and a boolean
.
Note that it extends net.java.ao.Entity
, which defines the primary key as being ID
. Unless you have a really good reason to do otherwise, it is strongly advised to do so.
Note the total max length of an entity name is 30 characters (including the AO prefix of 10 characters)
Example
, the corresponding table name would be similar to: AO_B6C1B4_EXAMPLE
Let's add a servlet that actually does something. First we need to define it in the plugin descriptor. For that we update the servlet module already defined for the Example servlet that should still be there.
1 2 3 4 5<servlet name="Todo List & Add Servlet" class="com.atlassian.tutorial.ao.todo.TodoServlet" key="todo-list"> <description>A servlet to add and list todos</description> <url-pattern>/todo/list</url-pattern> </servlet>
And we refactor the servlet class so that it looks like this:
1 2package com.atlassian.tutorial.ao.todo; import com.atlassian.activeobjects.external.ActiveObjects; import com.atlassian.plugin.spring.scanner.annotation.component.Scanned; import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import static com.google.common.base.Preconditions.*; @Scanned public final class TodoServlet extends HttpServlet { @ComponentImport private final ActiveObjects ao; @Inject public TodoServlet(ActiveObjects ao) { this.ao = checkNotNull(ao); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.getWriter().write("Todo servlet, doGet"); res.getWriter().close(); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.getWriter().write("Todo servlet, doPost"); res.getWriter().close(); } }
As we've added the component definition in our module, we can constructor inject the servlet with the ActiveObjects service. Note that the checkNotNull
method here is statically imported from the google collections' Preconditions class.
At this stage I've removed the test classes and resources as we'll worry about those later.
Let's check that this servlet acutally works:
atlas-mvn package
from the command line,It should read Todo servlet, doGet on the screen!
doGet
Let's implement doGet
first.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34@Override protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { final PrintWriter w = res.getWriter(); w.write("<h1>Todos</h1>"); // the form to post more TODOs w.write("<form method=\"post\">"); w.write("<input type=\"text\" name=\"task\" size=\"25\"/>"); w.write(" "); w.write("<input type=\"submit\" name=\"submit\" value=\"Add\"/>"); w.write("</form>"); w.write("<ol>"); ao.executeInTransaction(new TransactionCallback<Void>() // (1) { @Override public Void doInTransaction() { for (Todo todo : ao.find(Todo.class)) // (2) { w.printf("<li><%2$s> %s </%2$s></li>", todo.getDescription(), todo.isComplete() ? "strike" : "strong"); } return null; } }); w.write("</ol>"); w.write("<script language='javascript'>document.forms[0].elements[0].focus();</script>"); w.close(); }
There are a few things going on here. Some very simple HTML is being written to the output, nothing that anyone should fear, so it won't be detailed here. Other things need to be:
doPost
In a similar fashion, this is how the doPost method is implemented:
1 2@Override protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { final String description = req.getParameter("task"); ao.executeInTransaction(new TransactionCallback<Todo>() // (1) { @Override public Todo doInTransaction() { final Todo todo = ao.create(Todo.class); // (2) todo.setDescription(description); // (3) todo.setComplete(false); todo.save(); // (4) return todo; } }); res.sendRedirect(req.getContextPath() + "/plugins/servlet/todo/list"); }
Here again the code is even simpler, as there is no need for any HTML.
TransactionalCallback
needs to be imported using:
1 2import com.atlassian.sal.api.transaction.TransactionCallback;
We now have enough code to list all the todos from the database and also add todos.
Stage 2
We've now completed stage 2 of this guide.
atlas-mvn package
from the command line and you should be able to access the URL localhost:5990/refapp/plugins/servlet/todo/list, there you will be able to:
NOTE: If you're having issues, you might want to compare with my version of the code.
Woohoo! You've got your first Active Objects capable plugin working.
Before we jump onto removing todos, we will talk about transactions.
NOTE: Please note, JIRA currently does not support transactions for Active Objects (as of JIRA 6.0).
As previously stated, every Active Objects interaction must happen within a transaction. Doing so is fairly easy, as per the following code:
1 2public void someMethod(final ActiveObjects ao) { ao.executeInTransaction(new TransactionCallback<Object>() { @Override public Object doInTransaction() { // do something with AO return null; } }); }
Doing by hand is going to be painful. So there is a declarative alternative, using the @Transactional annotation.
These annotations will only work on interfaces, so we won't be able to apply it on our servlet. No need to worry let's introduce the TodoService
:
1 2package com.atlassian.tutorial.ao.todo; import com.atlassian.activeobjects.tx.Transactional; import java.util.List; @Transactional public interface TodoService { Todo add(String description); List<Todo> all(); }
and its implementation looks like this:
1 2package com.atlassian.tutorial.ao.todo; import com.atlassian.activeobjects.external.ActiveObjects; import java.util.List; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.Lists.newArrayList; import com.atlassian.plugin.spring.scanner.annotation.component.Scanned; import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; import javax.inject.Inject; import javax.inject.Named; @Scanned @Named public class TodoServiceImpl implements TodoService { @ComponentImport private final ActiveObjects ao; @Inject public TodoServiceImpl(ActiveObjects ao) { this.ao = checkNotNull(ao); } @Override public Todo add(String description) { final Todo todo = ao.create(Todo.class); todo.setDescription(description); todo.setComplete(false); todo.save(); return todo; } @Override public List<Todo> all() { return newArrayList(ao.find(Todo.class)); } }
Now is the time to add a servlet class to use the service and make it a bit simpler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55package com.atlassian.tutorial.ao.todo; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import static com.google.common.base.Preconditions.*; public final class TodoServlet extends HttpServlet { private final TodoService todoService; public TodoServlet(TodoService todoService) { this.todoService = checkNotNull(todoService); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { final PrintWriter w = res.getWriter(); w.write("<h1>Todos</h1>"); // the form to post more TODOs w.write("<form method=\"post\">"); w.write("<input type=\"text\" name=\"task\" size=\"25\"/>"); w.write(" "); w.write("<input type=\"submit\" name=\"submit\" value=\"Add\"/>"); w.write("</form>"); w.write("<ol>"); for (Todo todo : todoService.all()) // (2) { w.printf("<li><%2$s> %s </%2$s></li>", todo.getDescription(), todo.isComplete() ? "strike" : "strong"); } w.write("</ol>"); w.write("<script language='javascript'>document.forms[0].elements[0].focus();</script>"); w.close(); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { final String description = req.getParameter("task"); todoService.add(description); res.sendRedirect(req.getContextPath() + "/plugins/servlet/todo/list"); } }
Stage 3
We've now completed stage 3 of this guide.
atlas-mvn package
from the command line and you should be able to access the URL localhost:5990/refapp/plugins/servlet/todo/list, there you will be able to:
NOTE: If you're having issues, you might want to compare with my version of the code.
We have the exact same features as in stage 2, but this time we've used declarative transactions to simplify our code.
Now that we've written a bit of code, it's time to do some testing. Active Objects provides a simple framework for integration testing on top of JUnit 4.8.
First thing we need to do is add the needed dependencies to the pom.xml
:
1 2<dependency> <groupId>com.atlassian.activeobjects</groupId> <artifactId>activeobjects-test</artifactId> <version>${ao.version}</version> <scope>test</scope> </dependency>
where ao.version
is the version of Active Objects you're using. If you followed this guide carefully it should already be defined. Note the scope of the dependency is correctly set to test
.
We will need as well a dependency on JUnit, of course, HSQL DB as this is the database we're going to test against, and an slf4j implementation. Here is an example of what you should add to your pom.xml
:
You must use JUnit 4.8 or later
The Active Objects test framework requires JUnit 4.8 or later. If you use an earlier version of JUnit, the tests may fail silently.
For more information, see AO-334.
1 2<dependency> <groupId>hsqldb</groupId> <artifactId>hsqldb</artifactId> <version>1.8.0.10</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.6.2</version> <scope>test</scope> </dependency>
The JUnit dependency has already been added when the pom.xml
was generated.
The Plugin SDK is based on maven - no surprise here - so create the standard directory for test sources src/test/java
if it does not already exist.
Now let's write those tests. We're going to test the com.atlassian.tutorial.ao.todo.TodoServiceImpl
class.
We create in the test source directory a class named com.atlassian.tutorial.ao.todo.TodoServiceImplTest
and it should look like this:
1 2package com.atlassian.tutorial.ao.todo; import org.junit.After; import org.junit.Before; import org.junit.Test; public class TodoServiceImplTest { @Before public void setUp() throws Exception { } @After public void tearDown() throws Exception { } @Test public void testAdd() throws Exception { } @Test public void testAll() throws Exception { } }
To make sure everything is configured correctly, run the test now using atlas-unit-test
, it should pass. Awesome, let's tell this test about Active Objects and setup our TodoServiceImpl
instance:
1 2package com.atlassian.tutorial.ao.todo; import com.atlassian.activeobjects.test.TestActiveObjects; import net.java.ao.EntityManager; import net.java.ao.test.junit.ActiveObjectsJUnitRunner; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.util.List; import static org.junit.Assert.*; @RunWith(ActiveObjectsJUnitRunner.class) // (1) public class TodoServiceImplTest { private EntityManager entityManager; // (2) private TodoServiceImpl todoService; // (3) @Before public void setUp() throws Exception { assertNotNull(entityManager); // (4) todoService = new TodoServiceImpl(new TestActiveObjects(entityManager)); // (5) } @Test public void testAdd() throws Exception { } @Test public void testAll() throws Exception { } }
Here are a few things to note and understand:
ActiveObjectsJUnitRunner
to run this test, this will help us access a correctly configured Active Objects instance for testing. It also wraps each tests in a transaction that is rolled-back after each of them. This will leave the test database in the same state for each test.EntityManager
that will be automatically injected by the ActiveObjectsJUnitRunner
. We will be able to use that entity manager to create an ActiveObjects
instance to use with our service implementation.EntityManager
is not null
to make sure we've configured our test correctly,TestActiveObjects
instance.Let's run the test again. It passes! Great, let's move on to actually writing some tests.
1 2package com.atlassian.tutorial.ao.todo; import com.atlassian.activeobjects.external.ActiveObjects; import com.atlassian.activeobjects.test.TestActiveObjects; import net.java.ao.EntityManager; import net.java.ao.test.junit.ActiveObjectsJUnitRunner; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.util.List; import static org.junit.Assert.*; @RunWith(ActiveObjectsJUnitRunner.class) public class TodoServiceImplTest { private EntityManager entityManager; private ActiveObjects ao; // (1) private TodoServiceImpl todoService; @Before public void setUp() throws Exception { assertNotNull(entityManager); ao = new TestActiveObjects(entityManager); todoService = new TodoServiceImpl(ao); } @Test public void testAdd() throws Exception { final String description = "This is a todo"; ao.migrate(Todo.class); // (2) assertEquals(0, ao.find(Todo.class).length); final Todo add = todoService.add(description); assertFalse(add.getID() == 0); ao.flushAll(); // (3) clear all caches final Todo[] todos = ao.find(Todo.class); assertEquals(1, todos.length); assertEquals(description, todos[0].getDescription()); assertEquals(false, todos[0].isComplete()); } @Test public void testAll() throws Exception { ao.migrate(Todo.class); // (2) assertTrue(todoService.all().isEmpty()); final Todo todo = ao.create(Todo.class); todo.setDescription("Some todo"); todo.save(); ao.flushAll(); // (3) clear all caches final List<Todo> all = todoService.all(); assertEquals(1, all.size()); assertEquals(todo.getID(), all.get(0).getID()); } }
A few comments:
ActiveObjects
instance into a field for using in our tests,Todo
entityflushAll
can be handy to make sure we're actally testing the DB and not the cache.The rest of the test should be self explanatory. As always run the tests, which should pass.
In this test all the migration and data is treated within each test. What if we wanted to seed the database with some data to simplify our tests? Well, let's do this:
1 2package com.atlassian.tutorial.ao.todo; import com.atlassian.activeobjects.external.ActiveObjects; import com.atlassian.activeobjects.test.TestActiveObjects; import net.java.ao.EntityManager; import net.java.ao.test.jdbc.Data; import net.java.ao.test.jdbc.DatabaseUpdater; import net.java.ao.test.junit.ActiveObjectsJUnitRunner; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.util.List; import static org.junit.Assert.*; @RunWith(ActiveObjectsJUnitRunner.class) @Data(TodoServiceImplTest.TodoServiceImplTestDatabaseUpdater.class) // (1) public class TodoServiceImplTest { private static final String TODO_DESC = "This is a todo"; private EntityManager entityManager; private ActiveObjects ao; private TodoServiceImpl todoService; @Before public void setUp() throws Exception { assertNotNull(entityManager); ao = new TestActiveObjects(entityManager); todoService = new TodoServiceImpl(ao); } @Test public void testAdd() throws Exception { final String description = TODO_DESC + "#1"; assertEquals(1, ao.find(Todo.class).length); final Todo add = todoService.add(description); assertFalse(add.getID() == 0); ao.flushAll(); // clear all caches final Todo[] todos = ao.find(Todo.class); assertEquals(2, todos.length); assertEquals(add.getID(), todos[1].getID()); assertEquals(description, todos[1].getDescription()); assertEquals(false, todos[1].isComplete()); } @Test public void testAll() throws Exception { assertEquals(1, todoService.all().size()); final Todo todo = ao.create(Todo.class); todo.setDescription("Some todo"); todo.save(); ao.flushAll(); // clear all caches final List<Todo> all = todoService.all(); assertEquals(2, all.size()); assertEquals(todo.getID(), all.get(1).getID()); } // (2) public static class TodoServiceImplTestDatabaseUpdater implements DatabaseUpdater { @Override public void update(EntityManager em) throws Exception { em.migrate(Todo.class); final Todo todo = em.create(Todo.class); todo.setDescription(TODO_DESC); todo.save(); } } }
Here it is. We've introduce the @Data
annotation of the Active Objects test framework:
migrate
entities under test and then add some data that tests can use.For now we haven't worried about the database used for testing at all. This was all taken care of by the Active Objects testing framework. Indeed by default it will use an in-memory HSQL DB. But we will want to test against other databases.
This is what the @Jdbc annotation is used for. Annotate your test class with this, it requires a single class parameter that implements JdbcConfiguration.
You can now test against any database of your choice with some simple configuration. I suggest you have a look in the net.java.ao.test.jdbc package as it contains multiple implementations of this interface that might be useful. Note the DynamicJdbcConfiguration which will let you change the database simply by setting a system property.
Active Objects defines a way to configure the strategy for naming database identifiers given an Entity
class (e.g. tables and column names) using the @NameConvertersannotation. The default configuration for testing is equivalent to that used when deployed in an Atlassian product except that the table name hash is always 000000
.
The strategy for converting table names and column names is explained in the Active Objects FAQ.
In order to see what SQL queries are being issued, let's add some better logging. We'll be using log4j
to replace the slf4j-simple
logger. In pom.xml
, remove the dependency on slf4j-simple
and replace with the following:
1 2<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.6.2</version> <scope>test</scope> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.16</version> <scope>test</scope> </dependency>
Now we simply need to configure log4j
for the test we wrote. Let's add a log4j.properties
configuration file to the src/test/resources
directory.
We want it to look like this:
1 2log4j.rootLogger = INFO, console log4j.appender.console = org.apache.log4j.ConsoleAppender log4j.appender.console.layout = org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern = %5p - %60.60c - %m%n # (1) log4j.logger.net.java.ao.sql = DEBUG, console log4j.additivity.net.java.ao.sql = false
Now when running the tests, you should see the executed SQL statements:
1 2DEBUG - net.java.ao.sql - CREATE TABLE PUBLIC.AO_000000_TODO ( COMPLETE BOOLEAN, DESCRIPTION VARCHAR(255), ID INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL, PRIMARY KEY(ID) ) DEBUG - net.java.ao.sql - INSERT INTO AO_000000_TODO (ID) VALUES (NULL) DEBUG - net.java.ao.sql - UPDATE PUBLIC.AO_000000_TODO SET DESCRIPTION = ? WHERE ID = ? DEBUG - net.java.ao.sql - SELECT * FROM PUBLIC.AO_000000_TODO DEBUG - net.java.ao.sql - INSERT INTO AO_000000_TODO (ID) VALUES (NULL) DEBUG - net.java.ao.sql - UPDATE PUBLIC.AO_000000_TODO SET DESCRIPTION = ?,COMPLETE = ? WHERE ID = ? DEBUG - net.java.ao.sql - SELECT * FROM PUBLIC.AO_000000_TODO
Stage 4
We've now completed stage 4 of this guide.
TodoServiceImplTest
should pass.NOTE: If you're having issues, you might want to compare with my version of the code.
You'll notice that there are other stages to the source code. This is because Handling AO Upgrade Tasks follows on from this tutorial.
Rate this page: