Wednesday 18 April 2012

class com.atlassian.jira.issue.customfields.impl.NumberCFType passed an invalid value of type: class java.lang.Long

        Double value pass in case of update or create Customfields of NumberCFType  Type.
  
        Double oldValue = null;
        Double newValue = null;
        IssueChangeHolder changeHolder = new DefaultIssueChangeHolder();

        CustomField custom_1 = customManager.getCustomFieldObjectByName("Custom_Fields1");

        oldValue = Double.parseDouble(String.valueOf(issue.getCustomFieldValue(custom_1)));

        newValue = Double.parseDouble(String.valueOf(250));

        custom_1.updateValue(null, issue, new ModifiedValue(oldValue,newValue), changeHolder);

Monday 16 April 2012

How to Create JIRA Custom Select Field ?


There are many issue tracking systems. In my humble opinion JIRA (http://www.atlassian.com) is the most sophisticated one.
Recenty, I tried to integrate a new field into the structure af an issue. Here are my requirements:
  • the content may appear inside of a select control
  • the content is pulled every time when they insert a new issue or change the existing ones
  • the content could be pretty large. Maybe 1000 items. The user should be able to filter.
As you already know JIRA offers some default field types. For further information see this documentation:http://confluence.atlassian.com/display/JIRA/Adding+a+Custom+Field
There is also a select control with configurable list of options. But what about a long list of options you don’t know when creating the field?
My fear became true. JIRA out of the box features aren’t sufficient for me. Fortunately, there is also the way to implement own plugins. So this is what I did.  Here’s my cookbook:
Go to the JIRA page and download the SDK. Follow the instructions. At the end you have a pom.xml which is very similar to this one:
<?xml version=”1.0″ encoding=”UTF-8″?>
<project xmlns=”http://maven.apache.org/POM/4.0.0″
xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”
xsi:schemaLocation=”http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd”>
<modelVersion>4.0.0</modelVersion>
<groupId>com.jira.customfields.impl</groupId>
<artifactId>sophisticated-select-field</artifactId>
<version>1.0-SNAPSHOT</version>
<organization>
<name>softwarechaos</name>
<url>softwarechaos.wordpress.com</url>
</organization>
<name>sophisticated-select-field</name>
<description>This is my first but very clever select field plugin for Atlassian JIRA.</description>
<packaging>atlassian-plugin</packaging>
<dependencies>
<dependency>
<groupId>com.atlassian.jira</groupId>
<artifactId>atlassian-jira</artifactId>
<version>${jira.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.atlassian.jira</groupId>
<artifactId>jira-func-tests</artifactId>
<version>${jira.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.atlassian.maven.plugins</groupId>
<artifactId>maven-jira-plugin</artifactId>
<version>3.7.2</version>
<extensions>true</extensions>
<configuration>
<productVersion>${jira.version}</productVersion>
<productDataVersion>${jira.data.version}</productDataVersion>
</configuration>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<jira.version>4.4.1</jira.version>
<jira.data.version>4.4</jira.data.version>
<amps.version>3.7.2</amps.version>
</properties>
</project>
Then implement your classes in src/main/java. The API of JIRA contains all default field types. Browse to the class SelectCFType that fulfills my requirements. The constructor of that class has one parameter calle optionsManager about which I assume that it’s able to manage the items (about 1000). There is also a very magical method called getVelocityParameters. Here are passed the issue, field itself (?) and field layout item.  The optionsManager is encapsulated so you can use it by this operator. Note: Apache velocity plays a crucial role in JIRA when defining howto show the content or lock some features for some users. Read more below.
The final implementation of the class looks like this:
package com.jira.customfields.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.dom4j.DocumentException;

import org.dom4j.io.SAXReader;
import org.xml.sax.InputSource;

import com.atlassian.jira.issue.Issue;
import com.atlassian.jira.issue.customfields.converters.SelectConverter;
import com.atlassian.jira.issue.customfields.converters.StringConverter;
import com.atlassian.jira.issue.customfields.impl.SelectCFType;
import com.atlassian.jira.issue.customfields.manager.GenericConfigManager;
import com.atlassian.jira.issue.customfields.manager.OptionsManager;
import com.atlassian.jira.issue.customfields.persistence.CustomFieldValuePersister;
import com.atlassian.jira.issue.fields.CustomField;
import com.atlassian.jira.issue.fields.config.FieldConfig;
import com.atlassian.jira.issue.fields.layout.field.FieldLayoutItem;
import com.atlassian.jira.issue.search.SearchContextImpl;
import com.atlassian.jira.issue.customfields.option.Option;

/**
 * This is a very sophisticated SELECT field
 * @author kg
 * @since 02.02.2012
 */
public class SophisticatedFieldPlugin extends SelectCFType
{
 /**
  * Custom field specific logger
  */
    private static final Logger log = Logger.getLogger(com.jira.customfields.impl.SophisticatedFieldPlugin.class);

    /**
     * URL to query all items
     */
    public String url;

    /**
     * options manager passed in constructor
     */
    private OptionsManager optionsManager;

 public SophisticatedFieldPlugin(
   CustomFieldValuePersister customFieldValuePersister,
   StringConverter stringConverter, SelectConverter selectConverter,
   OptionsManager optionsManager,
   GenericConfigManager genericConfigManager) {
  super(customFieldValuePersister, stringConverter, selectConverter,
    optionsManager, genericConfigManager);
  this.optionsManager = optionsManager;

  log.setLevel(Level.ALL);
  log.info("SophisticatedFieldPlugin constructor");
 }

 static <K,V extends Comparable> SortedSet<Map.Entry> entriesSortedByValues(Map map) {
     SortedSet<Map.Entry> sortedEntries = new TreeSet<Map.Entry>(
         new Comparator<Map.Entry>() {

            private String prepare( Object o )
            {
              return ((String)o).toLowerCase().replace( 'ä', 'a' )
                                              .replace( 'ö', 'o' )
                                              .replace( 'ü', 'u' )
                                              .replace( 'ß', 's' );
            }
             @Override public int compare(Map.Entry e1, Map.Entry e2) {
                 return prepare( e2.getValue() ).compareTo( prepare( e1.getValue() ) );
             }
         }
     );
     sortedEntries.addAll(map.entrySet());
     return sortedEntries;
 }

 static boolean containsItem( List optionList, String value )
 {
  if( optionList == null ) return false;
  for( Option option : optionList )
  {
   if( option.getValue().indexOf( value ) != -1 )
    return true;
  }
  return false;
 }

 public SortedSet<Map.Entry> searchItems()
 {
  Properties properties = new Properties();
  File file = new File("/srv/jira-data/plugins/sophisticated_field_plugin.txt");
  try {
      properties.load(new FileInputStream(file));
      this.url = (String) properties.get("url");
  } catch (IOException e) {
   log.error( "Could not read properties file: " + file.getAbsolutePath() );
  }
  log.info("url:" + url);

  log.info("calling searchItems");
  Map m = new HashMap();
     try
     {
         SAXReader saxReader = new SAXReader();
         org.dom4j.Document d = saxReader.read( new URL(url) );

         log.info(url);

         List dataNodes = d.getRootElement().elements("data");

         log.info( "dataNodes.length=" + dataNodes.size());

         for (Iterator iterator = dataNodes.iterator(); iterator.hasNext();)
         {
          org.dom4j.Element dataNode = (org.dom4j.Element) iterator.next();

          org.dom4j.Element idNode = dataNode.element("id");
          org.dom4j.Element nameNode = dataNode.element("name");      

          String id = idNode.getStringValue();
          String name = nameNode.getStringValue();

          m.put(id, name);
         }
     }
     catch (Exception e)
     {
      log.error("searchItems exception");
      log.error(e.getMessage());
         e.printStackTrace();
     }
     return entriesSortedByValues(m);
 } 

 @Override
 public Map getVelocityParameters(Issue issue,
            CustomField field,
            FieldLayoutItem fieldLayoutItem)
    {
  log.info("calling getVelocityParameters");

  Map parameters = super.getVelocityParameters(issue, field, fieldLayoutItem);

  FieldConfig fieldConfig = null;
  if(issue == null)
  {
   log.info("issue is null");
   fieldConfig = field.getReleventConfig(new SearchContextImpl());
  } else {
   log.info( "issue is not null" );
   log.info( "issue-id:" + issue.getId() );
   fieldConfig = field.getRelevantConfig(issue);
  }

  log.info("before search items");
  SortedSet<Map.Entry> set = searchItems();
  log.info("==> " + set.size());

  List existingOptions = optionsManager.getOptions(fieldConfig);
  log.info("EXISTING-OPTIONS: " + existingOptions.size() );
  /*
  for( Option option : existingOptions )
  {
   log.info( option.getOptionId() + " ==> " + option.getValue() );
  }
  */

  for( Map.Entry el : set )
  {
   String key = el.getKey();
   String val = el.getValue();

   String itm = val + " [" + key + "]";

   if(containsItem( existingOptions, key ) ) {
    //log.info( itm + " found ==> continue" );
    continue;
   }
   log.warn( itm + " NOT FOUND!!!");
   Option option = optionsManager.createOption(fieldConfig, null, null, itm);
  }
  return parameters;
 }
}
Note:  the method createOption saves the new items in the backend of JIRA. You don’t have any chance to set the value (hidden value, not the text) of an item. They are managed by JIRA! As you can see the itm is a concatanation of val and key. If you need the key part of the composed value you may need some velocity skills. See below.
Okay. The class is ready to use. For troubleshooting myself the log level is activated so that I can trace the execution of my code by screening the JIRA log.
The next step is defining of velocity templates. If you need a solution in a short time you don’t have time for studying the documentations of all software of this world. So what I did was:
find / -name "*.vm"
I found the default velocity templates used by JIRA for displaying select fields. I copied them into my src/main/resources folder and made some changes.
Note: there is a plugin descriptor created by Maven in your src/main/resources directory. You have to link it with the velocity templates.
<atlassian-plugin key=”${project.groupId}.${project.artifactId}” name=”${project.name}” plugins-version=”2″>
<plugin-info>
<description>${project.description}</description>
<version>${project.version}</version>
<vendor name=”software chaos” url=”softwarechaos.wordpress.com”/>
</plugin-info>
<customfield-type key=”sophisticated-select-field” name=”Sophisticated Select Field”>
<description>Dropdown field filled with many items.</description>
<resource type=”velocity” name=”view” location=”templates/com/jira/customfields/impl/view-opportunity.vm”/>
<resource type=”velocity” name=”edit” location=”templates/com/jira/customfields/impl/edit-opportunity.vm”/>
<resource type=”velocity” name=”xml” location=”templates/com/jira/customfields/impl/xml-opportunity.vm”/>
</customfield-type>
</atlassian-plugin>
The velocity templates look like this:
view: the original key is parsed to show a hyperlink to an external application.
#if ($value)
#set ($index1 = $value.toString().indexOf("["))
#if($index1 != -1)
 #set ($index1 = $index1 + 1)
#end
#set ($index2 = $value.toString().indexOf("]"))
 #if($index1 != -1 && $index2 != -1)
  #set ($opp = $value.toString().substring( $index1, $index2 ))
 #end

<a href="http://myurl/targetapplication?key=${opp}" target="_blank"> 
$!value.toString() 
</a>
#end
edit: I wrote some javascript code to filter the items.
#customControlHeader ($action $customField.id $customField.name $fieldLayoutItem.required $displayParameters $auiparams)

<input type="button" value="Filter" onclick='javascript:
var filterInputField = document.getElementById("inputFilter");
var f = document.getElementById("$customField.id");
function refresh() {
 var f = arguments[0];
 var v = arguments[1];
 var o = f.options;
 if( !this.__allValues ) {
  this.__allValues = {};
  for(var i=0;i=0;i--) {
  f.options[i] = null;
 }
 for(var key in this.__allValues ) {
  var value = this.__allValues[key];
  if( value.toLowerCase().indexOf( v.toLowerCase() ) != -1 ) {
   var newO = new Option(value, key, false, true);
   f.options[f.options.length] = newO;
  }
 }
};
refresh(f,filterInputField.value);'/>

    #if (!$fieldLayoutItem || $fieldLayoutItem.required == false)
        $i18n.getText("common.words.none")
    #else
        $i18n.getText("common.words.none")
    #end
    #foreach ($option in $configs.options)
        #if(!$option.disabled || $option.disabled == false || ($value && $value == $option.optionId.toString()))
            $option.value
        #end
    #end

Number of items: $configs.options.size()
#customControlFooter ($action $customField.id $fieldLayoutItem.fieldDescription $displayParameters $auiparams)
xml: this file is unchanged.
#if ($value)
    $value.toString()
#end

Now, you are probably curious about the result. Here is it!
Issue in the edit mode:

Issue in the view mode: