Showing posts with label Atlassian. Show all posts
Showing posts with label Atlassian. Show all posts

2009-05-27

Custom breadcrumbs in Confluence plugin

5th part in Confluence plugin development series..
Often pages in plugin have hierarhical structure, so if you for example have 2 pages - Departments and Edit Department, it's preferred that your visotors may navigate fast within these pages. The goal may be achived with a help of top navigation bar, but unfortunatelly confluence only provides backlink to Dashboard:

But what you really prefer is:

The idea is to create your own breadcrumbs content tag.

  • create new file breadcrumbs.vm in your departments directory:
    #requireResource("confluence.web.resources:yui-core")
    #requireResource("confluence.web.resources:ajs")
    #requireResource("confluence.web.resources:breadcrumbs")
    <content tag="breadcrumbs">
    <ol id="breadcrumbs">
    <li><span>#dashboardlink ()</span></li>
    <li>&gt;</li>
    <li><span><a href="$req.contextPath/
    admin/departments/observe.action ">Departments</a>
    </span></li>
    <li>&gt;</li>
    <li><span>Edit Department</span></li>
    </ol>
    </content>
  • include this file in the end of your page:
      ...
    #parse
    ("/templates/userinfo/departments/breadcrumbs.vm")
    </body>
    </html>
  • More extended example may dynamically load action names and URLs. The following code uses i18n for action names in breadcrumbs:
  • breadcrumbs.vm looks like this:
    #requireResource("confluence.web.resources:yui-core")
    #requireResource("confluence.web.resources:ajs")
    #requireResource("confluence.web.resources:breadcrumbs")
    <content tag="breadcrumbs">
    <ol id="breadcrumbs">
    <li><span>#dashboardlink () </span></li>
    <li>&gt;</li>
    #set ($parentClass = $action.getParentClass())
    <li><span><a href="$req.contextPath/admin/
    departments/observe.action">
    $action.getActionName($parentClass.getName())</a>
    </span></li>
    <li>&gt;</li>
    <li><span>
    $action.getActionName($action.getClass().getName())
    </span></li>
    </ol>
    </content>
  • breadcrumbs tag uses action's static function to get parent:
    public class EditDepartment 
    extends ConfluenceActionSupport {
    ...
    public static Class<?> getParentClass() {
    return ViewDepartments.class;
    }
    ...
    }
  • After we know parent's class name it's easy to get it's label:
    $action.getActionName($action.getClass().getName())

    Of course class names must be defined in i18n properties:
    userinfo.departments.action.ViewDepartments.action.name=
    Departments
    userinfo.departments.action.EditDepartment.action.name=
    Edit department

    There are lots of things you can do here, make universal code for any page in your plugin, multi level navigations etc!

    2009-05-13

    Ajax form validation in Confluence plugin using a JSON request

    4th part in Confluence plugin development series..
    Today I describe how to make Ajax requests to XWork actions inside a Confluence plugin. A good example is server-side form validation. User fills a form, clicks Submit, then we validate the form in background and if there are no errors we allow submitting the form, otherwise show field errors.

    Suppose we have a simple user info form with input fields like name, department, email etc:
    <form id="saveform" action="doUpdate.action" method="post">
    #tag( TextField "name='user.name'" "size='50'" )
    #tag( Select "name='user.department'"
    "list=departmentsDao.all"
    "listKey=name" "listValue=name"
    "emptyOption='true'" )
    #tag( TextField "name='user.email'" "size='50'" )
    ...
    #tag( Submit "id=userformsubmit"
    "name='save'" "value='dot.button.save'" )
    #tag( Submit "name='cancel'" "value='dot.button.cancel'" )
    </form>
    One way to make form validation is to write it in javascript, but what if you already have it implemented in action? Write everything again? Maybe it's better to use existing server side code so you can switch on/off Ajax at any time and you'll not have the same logic implemented twice.
    Here are the required steps:
    1. edit atlassian-plugin.xml and create a package for you application with validation enabled, declare an action doUpdate for submitting a form and editValidate for Ajax validation
    2. <package name="userinfo" extends="default" 
      namespace="/dot/users">
      <default-interceptor-ref name="validatingStack"/>
      ...
      <action name="doUpdate"
      class="dot.userinfo.users.action.EditUserInfo"
      method="save">
      <external-ref name="dotUsersDao">
      usersDao</external-ref>
      <external-ref name="dotDepartmentsDao">
      departmentsDao</external-ref>
      <external-ref name="dotCitiesDao">
      citiesDao</external-ref>
      <result name="input" type="velocity">
      /templates/dot/userinfo/users/edituser.vm</result>
      <result name="success" type="redirect">
      /dot/users/usercard.action?id=${user.id}</result>
      <result name="cancel" type="redirect">
      /dot/users/observe.action</result>
      </action>
      ...
      <action name="editValidate"
      class="dot.userinfo.users.action.EditUserInfo">
      <result name="input" type="json" />
      <result name="success" type="json" />
      </action>
      ...
      </package>
      I turn on validatingStack for the whole package so I could use WebWork validation everywhere I need it. Don't forget to turn off validation for actions where you don't need it, use defaultStack for that
      <interceptor-ref name="defaultStack"/>
      Action editValidate will be called in Ajax from javascript. In my experience, both input and success return types are required. input is returned in case of any field errors, success when everything is ok. Actually there is no need to return success to javascript, but instead I could save the form and redirect to another form. I'll try this in next post.

    3. create EditUserInfo action and implement JSONAction interface
    4. public class EditUserInfo 
      extends ConfluenceActionSupport
      implements JSONAction {
      ...
      public String getJSONString() {
      try {
      if (hasFieldErrors()) {
      StringBuffer sb = new StringBuffer();
      sb.append("[");
      Map fieldErrors = getFieldErrors();
      for (Object field : fieldErrors.keySet()) {
      List<Object> msg =
      (List<Object>) fieldErrors.get(field);
      if (field != null && msg != null
      && msg.get(0) != null) {
      sb.append("{field:'"+field+"',
      error:'"+msg.get(0)+"'},");
      }
      }
      if (sb.length() > 1) {
      sb.deleteCharAt(sb.length()-1);
      }
      sb.append("]");
      return sb.toString();
      }
      } catch (Exception e) {
      // handle exception
      }
      return "";
      }
      }
      By the time getJSONString() method is called validation was already done by WebWork so we can immediatelly process the errors. Here I generate a Javascript array of all field errors which I want to show next to input fieds.

    5. write EditUserInfo-validation.xml file in the same package as java class. My validation file looks something like this
    6. <!DOCTYPE validators PUBLIC 
      "-//OpenSymphony Group//XWork Validator 1.0.2//EN"
      "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
      <validators>
      <field name="user.name">
      <field-validator type="requiredstring">
      <message key="dot.field.error.required" />
      </field-validator>
      </field>
      <field name="user.department">
      <field-validator type="requiredstring">
      <message key="dot.field.error.required" />
      </field-validator>
      </field>
      <field name="user.email">
      <field-validator type="requiredstring">
      <message key="dot.field.error.required" />
      </field-validator>
      <field-validator type="email">
      <message key="dot.field.error.email" />
      </field-validator>
      </field>
      </validators>
    Main stuff is done, last thing we need is to call validation from Javascript and show error messages.
    1. attach click event to submit button
    2. $("#userformsubmit").click(function(e) {
      try {
      var form = $("#saveform");
      validateForm(form);
      } catch (e) { alert(e); }
      return false; // always!
      });
    3. my validateForm looks like this
    4. function validateForm(form) {
      $.ajax({url: "editValidate.action",
      data: form.serialize(),
      success: function(data) { try {
      var fields = $("#saveform span.fielderror");
      // delete old errors,
      // error spans are found by class name
      fields.html("");
      // set received errors, iterate through span IDs
      data = eval(data);
      if (data && data.length > 0) {
      for (i in data) {
      var id = data[i].field.replace(/\./, "\\.")+
      "-error";
      var fe = $("#"+id);
      if (fe) { fe.html(data[i].error); }
      }
      return false;
      }
      $("#saveform").submit();
      return true;
      } catch (e) { alert(e); } }
      });
      return false;
      }
      here for every input control I have span with id ending with '-error' and class 'fielderror'. I still haven't tried to write macros so I implemented a separate file fielderror.vm
      <span id="$!webwork.htmlEncode($f)-error" 
      class="fielderror">
      #set( $err = $fieldErrors.get($f) )
      #if( $err.size() > 0 )
      $err.get(0)
      #end
      </span>
      which I include in main page next to input fields. Example for user name field:
      #set( $f = "user.name" )
      #parse( "/templates/dot/userinfo/fielderror.vm" )
      not so nice solution but I can live with that for now.

    Next week I'll try to optimize the code. In current implementation I believe validation is called twice: 1st time in ajax, and 2nd time when form is submitted (this we can escape), for field errors I need macro..

    2009-05-12

    Ajax in Confluence plugin using a servlet

    3rd part in Confluence plugin development series..
    While working with Confluence I found 2 ways to make Ajax requests:
    1. make requests to servlets
    2. make JSON requests to XWork actions
    Today I'll describe the 1st way using a servlet. In my application I had 2 comboboxes: one for select of a city and other for a department. A list of departments is different for every city so when client selects a city, a list of departments needs to be reloaded.
    The implementation is very simple and requires only to properly configure the servlet:
    1. edit your atlassian-plugin.xml file and add a servlet definition, see Developer documentation for further info
    2. <servlet name="Ajax List Servlet"
      key="ajaxListServlet"
      class="dot.userinfo.users.servlets.AjaxListServlet">
      <description>Ajax List Servlet</description>
      <url-pattern>/getlist/</url-pattern>
      </servlet>
    3. implement AjaxListServlet class, I prefer to generate HTML code directly, but you can generate JSON array as well
    4. public class AjaxListServlet extends HttpServlet {
      private static final long serialVersionUID = 1L;

      @Override
      protected void service(HttpServletRequest req,
      HttpServletResponse resp)
      throws ServletException, IOException {

      String list = req.getParameter("list");

      if ("dep".equalsIgnoreCase(list)) {
      String cityName = req.getParameter("city");

      // generate departments list HTML
      StringBuffer sb = new StringBuffer();
      sb.append("<option value='' selected></option>");
      try {
      for (DepartmentInfo dep : getDepartmentsDao().
      getByCityName(cityName)) {
      sb.append("<option>");
      sb.append(dep.getName());
      sb.append("</option>");
      }
      } catch (Exception e) {
      // handle exception
      }
      resp.getOutputStream().print(sb.toString());
      return;
      }
      }

      private DepartmentsDao getDepartmentsDao() {
      return DaoFactory.getDepartmentsDao();
      }
      }
    5. put comboboxes for city and department in .vm page
    6. #tag( Select "id=city" "name='user.city'" 
      "list=citiesDao.all"
      "listKey=name" "listValue=name"
      "emptyOption='true'" "theme='notable'" )
      #tag( Select "id=department" "name='user.department'"
      "list=departmentsDao.all"
      "listKey=name" "listValue=name"
      "emptyOption='true'" "theme='notable'" )
    7. and, finally, attach javascript to city combobox. here is a JQuery code
    8. jQuery(function($) {
      $("document").ready(function () {

      $("#city").change(function() {
      var cityName = $("#city option:selected").val();
      $.get("/plugins/servlet/getlist/",
      {list: "dep", city: cityName}, function(data) {
      $("#department").html(data);
      });
      });

      });
      });
      don't forget to include JQuery in your .vm page header
      <html>
      <head>
      <title>...</title>
      #requireResource("confluence.web.resources:jquery")
      </head>

    2009-05-06

    Pagination in Confluence plugin

    2nd part in Confluence plugin development series..
    Confluence provides items pagination out of the box but I couldn't find any reference in developer documentation. Standard page navigation bar looks like this:

    Here are the easy steps to implement it for your own list of items:
    1. extend your action class from AbstractEntityPaginationAction
    2. public class ViewUsers 
      extends AbstractEntityPaginationAction {
      }
      AbstractEntityPaginationAction provides
      protected bucket.core.actions.PaginationSupport paginationSupport;
      which you can use in your action:
      public String execute() throws Exception {
      List users = ... // load users here
      paginationSupport.setItems(users);
      paginationSupport.setPageSize(preferredPageSize);
      return SUCCESS;
      }
    3. insert pagination macro in viewusers.vm page
    4. #pagination($action.paginationSupport "page.action?")
      where page.action is the action name which will be called when you select a page. Pagination macro then adds a parameter startIndex, that's why you need a question mark (?) at the end

    5. implement page.action
    6. public String page() throws Exception {
      HttpServletRequest req =
      ServletActionContext.getRequest();
      int startIndex =
      Integer.parseInt(req.getParameter("startIndex"));
      List users = ... // load users
      paginationSupport.setItems(users);
      paginationSupport.setPageSize(preferredPageSize);
      paginationSupport.setStartIndex(startIndex);
      return SUCCESS;
      }
    7. view current items in page
    8. #foreach( $user in 
      $action.paginationSupport.page.iterator())
      ...
      #end
      PaginationSupport allows to control page size (number of items per page) so you may provide a selector in web page for page size and save it in user session.
      That's it!

    2009-05-04

    Database Persistence in Confluence plugin

    Last month I was busy with a Atlassian Confluence plugin for a local company. Next few blogs I'll devote to some problems I found in my way.
    The plugin was about browsing existing Crowd users, create new users, attach pictures to users, make directory of cities (company is distributed to several cities), departments etc., similar to Userinfo example.

    Developer documentation is quite useful while not complete... First thing I needed was to persist my dictionaries to database. After some discovering I've learned that Atlassian doesn't recommend using Hibernate (I couldn't attach mappings anyway) but provides Bandana instead (XML persistence based on XStream). It isn't going for me because I need to select, search, make relations between tables etc.. Make it all with Java classes?
    Fortunatelly it is still possible to use pure SQL quieries. So these are the steps for using database in Confluence:
    1. declare DAO bean in atlassian-plugin.xml
    2. <spring name="citiesDao" key="citiesDao" 
      class="dot.userinfo.cities.dao.HibernateCitiesDao">
      <property name="sessionFactory">
      <ref bean="sessionFactory"/>
      </property>

      </spring>
      where sessionFactory is Hibernate's session factory provided by Confluence;
    3. implement DAO class and derive it from HibernateObjectDao (provided by Atlassian)
    4. public class HibernateCitiesDao 
      extends HibernateObjectDao {
      public Collection getAll() {
      List result = new ArrayList();
      Session session = null;
      try {
      session = getSessionFactory().openSession();
      ResultSet rs = session.connection().
      prepareStatement("select * from city").executeQuery();
      while (rs.next()) {
      CityInfo o = new CityInfo();
      fill(rs, o);
      result.add(o);
      }
      } finally {
      if (session != null) {
      session.disconnect();
      }
      }
      return result;
      }
      }
    5. connect DAO to an action in atlassian-plugin.xml:
    6. <action name="viewcities"
      class="dot.userinfo.cities.action.ViewCities">
      <external-ref name="citiesDao">citiesDao</external-ref>
      <result name="success" type="velocity">
      /templates/dot/userinfo/cities/viewcities.vm</result>
      </action>
      Class ViewCities must have a setter (and getter if you want to use it directly in page) methods for citiesDao:
      public void setCitiesDao(CitiesDao citiesDao) {
      this.citiesDao = citiesDao;
      }
      public CitiesDao getCitiesDao() {
      return citiesDao;
      }
    7. use cities on the page viewcities.vm:
    8. #set($cities = $action.citiesDao.all)
      #foreach( $item in $cities.iterator())
      ...
      #end