This guide will help you get familiar with updating your object model using the Active Objects plugin upgrade tasks.
Source code of the tutorial available
The code that is used throughout this tutorial is hosted on BitBucket. You might want to checkout or even fork this code to help you with the task at hand:
1 2git clone git@bitbucket.org:serverecosystem/ao-tutorial.git
There are tags for each stage in the source repository, e.g. to see the progress at the end of stage 5, do:
1 2git checkout stage5
This tutorial resumes from stage 4 of the code, as the previous stages have all been covered in the Getting Started tutorial. It is strongly recommended that you go through this first tutorial if you have done so already.
So far our TODO plugin doesn't care about who the user is and even whether the user is actually logged in. We're going to remedy to that right now by:
We check whether the user is authenticated in the servlet:
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77package com.atlassian.tutorial.ao.todo; import com.atlassian.plugin.spring.scanner.annotation.component.Scanned; import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; import com.atlassian.sal.api.user.UserManager; import javax.inject.Inject; 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.*; @Scanned public final class TodoServlet extends HttpServlet { private final TodoService todoService; @ComponentImport private final UserManager userManager; // (1) @Inject public TodoServlet(TodoService todoService, UserManager userManager) { this.todoService = checkNotNull(todoService); this.userManager = checkNotNull(userManager); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { enforceLoggedIn(req, res); // (2) final PrintWriter w = res.getWriter(); w.printf("<h1>Todos (%s)</h1>", userManager.getRemoteUser().getUsername()); // 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()) { 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 { enforceLoggedIn(req, res); // (2) final String description = req.getParameter("task"); todoService.add(description); res.sendRedirect(req.getContextPath() + "/plugins/servlet/todo/list"); } private void enforceLoggedIn(HttpServletRequest req, HttpServletResponse res) throws IOException { if (userManager.getRemoteUser() == null) // (3) { res.sendRedirect(req.getContextPath() + "/plugins/servlet/login"); } } }
GET
and POST
method,null
, if it is redirect to the login page.We now need to record which user the TODO item belongs to. A simple way to do this is to add a username
field to the Todo
class:
1 2@Preload public interface Todo extends Entity { void setUserName(String userName); String getUserName(); String getDescription(); void setDescription(String description); boolean isComplete(); void setComplete(boolean complete); }
And then we also need to update the TodoService
implementation to properly handle this username:
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 com.atlassian.sal.api.user.UserManager; import net.java.ao.Query; import javax.inject.Inject; import javax.inject.Named; import java.util.List; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.Lists.newArrayList; @Scanned @Named public class TodoServiceImpl implements TodoService { @ComponentImport private final ActiveObjects ao; @ComponentImport private final UserManager userManager; // (1) @Inject public TodoServiceImpl(ActiveObjects ao, UserManager userManager) { this.ao = checkNotNull(ao); this.userManager = checkNotNull(userManager); } @Override public Todo add(String description) { final Todo todo = ao.create(Todo.class); todo.setDescription(description); todo.setComplete(false); todo.setUserName(currentUserName()); // (2) todo.save(); return todo; } @Override public List<Todo> all() { return newArrayList(ao.find(Todo.class, Query.select().where("USER_NAME = ?", currentUserName()))); // (3) } private String currentUserName() { return userManager.getRemoteUser().getUsername(); } }
UserManager
injected as we use it to get the current user,That's it. Active Objects will take care of adding the missing column for us. Let's see it in action, but first let's add some SQL logging to the Refapp so that we can easily check the column is actually added.
Since stage 4 we already have a log4j.properties
file, in the src/test/resources
directory. Let's configure the refapp-maven-plugin
to also use it.
1 2... <plugin> <groupId>com.atlassian.maven.plugins</groupId> <artifactId>refapp-maven-plugin</artifactId> <version>${amps.version}</version> <extensions>true</extensions> <configuration> ... <log4jProperties>src/test/resources/log4j.properties</log4jProperties> </configuration> </plugin> ...
Now that we've changed the data model, the tests we wrote in Stage 4 are no longer up-to-date. You can either choose to update them or skip compiling/running the tests via adding the following to your pom.xml
1 2<properties> ... <maven.test.skip>true</maven.test.skip> </properties>
Let's check that this works:
atlas-mvn package
from the command line, in the ao-tutorial
directory, this will upgrade the pluginadmin (pwd:admin)
There are 2 things you should notice when doing this:
ALTER TABLE AO_3F7D93_TODO ADD COLUMN USER_NAME VARCHAR(255)
being logged,Note, for the plugin to correctly be upgraded, you need to make sure that the plugin descriptor (atlassian-plugin.xml
) key is the same in both plugins.
So we have one issue, we lost the TODO items we created. Fear not, the data is still there, it's simply that we didn't update the items to be associated with any user. So when logged in as the admin
user, we don't see any items.
For this we're going to need our first ActiveObjectsUpgradeTask
. Here is how it looks:
1 2package com.atlassian.tutorial.ao.todo.upgrade.v1; // (1) import com.atlassian.activeobjects.external.ActiveObjects; import com.atlassian.activeobjects.external.ActiveObjectsUpgradeTask; import com.atlassian.activeobjects.external.ModelVersion; public final class TodoUpgradeTask001 implements ActiveObjectsUpgradeTask { @Override public ModelVersion getModelVersion() { return ModelVersion.valueOf("1"); // (2) } @Override public void upgrade(ModelVersion currentVersion, ActiveObjects ao) // (3) { ao.migrate(Todo.class); // (4) for (Todo todo : ao.find(Todo.class)) // (5) { todo.setUserName("admin"); todo.save(); } } }
We've created a specific package for this upgrade task, and this version of the data model. Also note that the Todo
class used here is the com.atlassian.tutorial.ao.todo.upgrade.v1.Todo class, we copied the model corresponding to that upgrade task into that same package. This code is never to be touched and should remain like so for the whole life of the plugin.
Setting the model version, here 1
. This is used by Active Objects to figure out which upgrade tasks to run and in which order. By default plugins that never defined any upgrade tasks are assigned the version 0
.
The Active Objects component passed a parameter here is the only one you should use for the upgrade task. You should never inject your plugin's Active Objects component in any upgrade task. This instance is specific to that one upgrade task and will be discarded after the upgrade task has run.
When writing your upgrade task, you must call the migrate
method yourself to let Active Objects know about the data model you want to work this. This will update the database to match the model. Note that if you omit part of the model objects here, their respective tables in the database will be dropped!
Then we can update our data model as we need.
The last step is to reference this upgrade task in the Active Objects module descriptor:
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> <upgradeTask>com.atlassian.tutorial.ao.todo.upgrade.v1.TodoUpgradeTask001</upgradeTask> </ao>
We've now completed stage 5 of this tutorial. Let's check that everything is working:
atlas-mvn package
from the command line, this will upgrade the plugin to version 1admin (pwd:admin)
, you should retrieve all the TODO items you previously entered,
- as fred (pwd:fred)
, you shouldn't have any TODO item there yetYou now have an Active Objects plugin capable of handling older version of its model.
Note, if you’re having issues, please compare with our version of the code.
Now we're not so happy about our decision to add the username as a field in the Todo
class. Let's introduce a new User
object.
So what we want to do is:
User
model class,userName
field from the Todo
class,User
field to the Todo
class.Let's first have a look at the model we want get to:
1 2package com.atlassian.tutorial.ao.todo; import net.java.ao.Entity; public interface User extends Entity { String getName(); void setName(String name); }
1 2package com.atlassian.tutorial.ao.todo; import net.java.ao.Entity; import net.java.ao.Preload; import net.java.ao.schema.NotNull; @Preload public interface Todo extends Entity { @NotNull void setUser(User user); @NotNull User getUser(); String getDescription(); void setDescription(String description); boolean isComplete(); void setComplete(boolean complete); }
In order to get there we're going to need an ActiveObjectsUpgradeTask
, but also an intermediate model where both the userName
field and the User
field are present on the Todo
class. Let's see what it looks like:
1 2package com.atlassian.tutorial.ao.todo.upgrade.v2; // (1) import net.java.ao.Entity; public interface User extends Entity { String getName(); void setName(String name); }
1 2package com.atlassian.tutorial.ao.todo.upgrade.v2; // (1) import net.java.ao.Entity; import net.java.ao.Preload; @Preload public interface Todo extends Entity { void setUserName(String userName); // (2) String getUserName(); void setUser(User user); // (3) User getUser(); String getDescription(); void setDescription(String description); boolean isComplete(); void setComplete(boolean complete); }
Todo
class specific for the upgradeuserName
as for the old version of the modelUser
field as per the new version of the modelAnd then we have the corresponding upgrade task:
1 2package com.atlassian.tutorial.ao.todo.upgrade.v2; // (1) import com.atlassian.activeobjects.external.ActiveObjects; import com.atlassian.activeobjects.external.ActiveObjectsUpgradeTask; import com.atlassian.activeobjects.external.ModelVersion; import com.google.common.collect.ImmutableMap; import net.java.ao.Query; public final class TodoUpgradeTask002 implements ActiveObjectsUpgradeTask { @Override public ModelVersion getModelVersion() { return ModelVersion.valueOf("2"); // (2) } @Override public void upgrade(ModelVersion currentVersion, ActiveObjects ao) { ao.migrate(Todo.class, User.class); // (3) Todo[] todos = ao.find(Todo.class); // (4) for (Todo todo : todos) { todo.setUser(getOrCreateUser(ao, todo.getUserName())); todo.save(); } } private User getOrCreateUser(ActiveObjects ao, String userName) { User[] users = ao.find(User.class, Query.select().where("NAME = ?", userName)); if (users.length == 0) { return createUser(ao, userName); } else if (users.length == 1) { return users[0]; } else { throw new IllegalStateException("There shouldn't be 2 users with the same username! " + userName); } } private User createUser(ActiveObjects ao, String userName) { return ao.create(User.class, ImmutableMap.<String, Object>of("NAME", userName)); } }
2
,User
class, and a new field to the Todo
class (for the User
reference)User
to correspond to the userName
.Then we simply need to declare the upgrade task in the Active Objects module descriptor:
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> <entity>com.atlassian.tutorial.ao.todo.User</entity> <upgradeTask>com.atlassian.tutorial.ao.todo.upgrade.v1.TodoUpgradeTask001</upgradeTask> <upgradeTask>com.atlassian.tutorial.ao.todo.upgrade.v2.TodoUpgradeTask002</upgradeTask> </ao>
And we're good to go. Obviously something we've not shown here is how to update the TodoService
implementation. This is left as an exercise to the reader . If you're stuck you can look at the source repository.
This complete stage 6 of this tutorial. Let's check that everything is working as expected:
atlas-mvn package
from the command line, check that the upgrade to version 2 occurred:1 2[INFO] [talledLocalContainer] INFO - assian.activeobjects.internal.ActiveObjectUpgradeManagerImpl - Starting upgrade of data model, current version is 1 [INFO] [talledLocalContainer] DEBUG - net.java.ao.sql - CREATE TABLE AO_3F7D93_USER ( [INFO] [talledLocalContainer] ID INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL, [INFO] [talledLocalContainer] NAME VARCHAR(255), [INFO] [talledLocalContainer] PRIMARY KEY(ID) [INFO] [talledLocalContainer] ) [INFO] [talledLocalContainer] DEBUG - net.java.ao.sql - ALTER TABLE AO_3F7D93_TODO ADD COLUMN USER_ID INTEGER [INFO] [talledLocalContainer] DEBUG - net.java.ao.sql - ALTER TABLE AO_3F7D93_TODO ADD CONSTRAINT fk_ao_3f7d93_todo_user_id FOREIGN KEY (USER_ID) REFERENCES AO_3F7D93_USER(ID) [INFO] [talledLocalContainer] DEBUG - net.java.ao.sql - CREATE INDEX index_ao_3f7d93_todo_user_id ON AO_3F7D93_TODO(USER_ID) [INFO] [talledLocalContainer] DEBUG - net.java.ao.sql - SELECT * FROM AO_3F7D93_TODO [INFO] [talledLocalContainer] DEBUG - net.java.ao.sql - SELECT ID FROM AO_3F7D93_USER WHERE NAME = ? [INFO] [talledLocalContainer] DEBUG - net.java.ao.sql - INSERT INTO AO_3F7D93_USER (NAME) VALUES (?) [INFO] [talledLocalContainer] DEBUG - net.java.ao.sql - UPDATE AO_3F7D93_TODO SET USER_ID = ? WHERE ID = ? [INFO] [talledLocalContainer] DEBUG - net.java.ao.sql - SELECT ID FROM AO_3F7D93_USER WHERE NAME = ? [INFO] [talledLocalContainer] DEBUG - net.java.ao.sql - UPDATE AO_3F7D93_TODO SET USER_ID = ? WHERE ID = ? [INFO] [talledLocalContainer] DEBUG - net.java.ao.sql - SELECT ID FROM AO_3F7D93_USER WHERE NAME = ? [INFO] [talledLocalContainer] DEBUG - net.java.ao.sql - INSERT INTO AO_3F7D93_USER (NAME) VALUES (?) [INFO] [talledLocalContainer] DEBUG - net.java.ao.sql - UPDATE AO_3F7D93_TODO SET USER_ID = ? WHERE ID = ? [INFO] [talledLocalContainer] DEBUG - net.java.ao.sql - SELECT ID FROM AO_3F7D93_USER WHERE NAME = ? [INFO] [talledLocalContainer] DEBUG - net.java.ao.sql - UPDATE AO_3F7D93_TODO SET USER_ID = ? WHERE ID = ? [INFO] [talledLocalContainer] INFO - assian.activeobjects.internal.ActiveObjectUpgradeManagerImpl - Finished upgrading, model is up to date at version 2 [INFO] [talledLocalContainer] DEBUG - net.java.ao.sql - ALTER TABLE AO_3F7D93_TODO DROP COLUMN USER_NAME [INFO] [talledLocalContainer] DEBUG - net.java.ao.sql - ALTER TABLE AO_3F7D93_TODO DROP CONSTRAINT fk_ao_3f7d93_todo_user_id [INFO] [talledLocalContainer] DEBUG - net.java.ao.sql - ALTER TABLE AO_3F7D93_TODO ALTER COLUMN USER_ID INTEGER NOT NULL [INFO] [talledLocalContainer] DEBUG - net.java.ao.sql - ALTER TABLE AO_3F7D93_TODO ADD CONSTRAINT fk_ao_3f7d93_todo_user_id FOREIGN KEY (USER_ID) REFERENCES AO_3F7D93_USER(ID)
Note, if you’re having issues, please compare with our version of the code.
Rate this page: