Create
cancel
Showing results for 
Search instead for 
Did you mean: 
Sign up Log in

Validator support for Edit, Quick Edit and Soap

uvoellger September 2, 2012

Edit validators are very helpfull to verify that all issue data is consistent. While validation based on a single field is simple in Jira (it is done by the field type itself), it becomes difficult when valid field values depend on other field content. Edit validators help in those, but also in other cases as they allow to validate the content of multiple fields.

Examples for edit validators are:

  • validate that affects version is released earlier than fix version
  • validate that only one version is selected (which is because system fields cannot be configure for single selection, and custom renderer are not supported for system version fields)


Existing approaches


There are several approaches to implement edit validators, e.g.

It would be nice to have edit validators too. But existing solutions have some disadvantages:

  • Edit provided as transitions in the issue workflow
    * Operations are no transitions! This approach would mean that "Edit" is shown at the wrong location in the issue menu (right side). Quick edit does not work.
    * Greenhopper has a different way to show operations and transitions. While we managed to get "Edit" at the right location in the plain Jira issue menu with a custom solution, we could not fix this for Greenhopper.
  • Javascript-based solutions
    * Heavily depends on Jira UI (HTML element ids etc.)
    * Events seem to happen in different order in some browsers
    * REST-based validation results might return after forms are submitted
    * Users could switch off Javascript (ok - users could not do anything with Jira in that case...)
  • Jython-script-based solution
    * Non-trivial installation
    * Another script-based language
    * Another 3rd-party plugin

Our requirements

So we decided to implement our own solution to fulfill the following requirements:

  • Works for Edit, Quick edit and SOAP
  • Same approach also possible for other operations as well as post functions
  • The solution must allow to define an issue type- and status-dependent list of validators
  • Server-side validation
  • Java-based implementation via version-2-plugin(s)
  • Updates of validators and configuration without Jira restart


Our solution

Note that the code is extracted from real source without compilation and tests...
Please note also that we are using Ant as build tool. This means the Ant variables in descriptors are replaced during the build process in our environment.

Utility plugin

For our solution, we need an interface to implement as well as an utility class to get validator instances. Both reside in a general utility plugin. Other users might add this class to the plugin which is explained in the next step.

Please make sure the code resides in a version-2-plugin and is exported so it can be used by another version-2-plugin. In our environment, we use a descriptor very similar to this:

<atlassian-plugin key="${plugin.groupid}.${plugin.artifactid}"
                  name="${plugin.name}"
                  plugins-version="2">
    <plugin-info>
        <description>${plugin.description}</description>
        <version>${plugin.version}</version>
        <vendor name="INTERSHOP Communications AG"
                url="http://www.intershop.de"/>
        <bundle-instructions>
            <Export-Package>
                com.intershop*
            </Export-Package>
        </bundle-instructions>
    </plugin-info>
</atlassian-plugin>


Utility plugin - Validator interface

We need an interface which has to be implemented by all operation validators.

The interface looks like this:

package com.intershop.jira.operation.util;

/**
 * Interface for operation validators.
 */
public interface OperationValidator
{
    /**
     * Validates the values in the field values holder for this operation, user and issue.
     * Returns a collection of errors or null in case everything is fine.
     * @param user
     * @param issue
     * @param mapFieldValuesHolder
     * @return
     */
    public ErrorCollection isValid(User user, Issue issue, Map<String, Object> mapFieldValuesHolder);
}

Utility plugin - Validator utility

The validator utility class helps to make it easier to call all related operation validators later.

In our environment, we create validator instances via reflections. We parse our Jira configuration file for validator class names to execute. The configuration is stored into a property file and automatically reloaded if needed. The properties are then converted into a Java data structure, which allows for fast access. In the end, this allows us to quickly find the list of validator class names for an issue type and status. Since the file is automatically reloaded, changes can be done very easily. Configuration files can also be added to a VCS like SVN and deployed to several instances. The list of validators is created via reflections from the resulting list of class names.

The reason for using reflections to create instances is that validators can be added and changed at runtime. Together with reloaded configuration data, the list of validators to execute for an issue type and status can be adjusted very easily.

Other users might prefer to load all classes that implement the OperationValidator interface. They could then ask each instance whether the itis a validator for an issue of this type and status. Or, even simpler, add parameters to the existing isValid method accordingly so this method verifies the issue type and status.

Back to our solution: the utility class provides two methods:

  • getValidators returns all relevant validators. As already mentioned, we get the list of validator class names from a configuration file using the first parameter (strSection) together with the issue status and issue type. It is important that we get the validator class using the class loader of the plugin that contains the validator implementation. The validator classes would not be found if they reside in different plugins and another class loader is used. This unfortunately means, that we currently support only one validator plugin.
  • The result is used by isValid to execute the corresponding method for each validator. As soon as there is an error, the validation is stopped and the error collection is returned.
/**
 * Base calls for operation validators.
 */
public abstract class OperationValidatorUtils
{
    private static final PluginAccessor pluginAccessor = ComponentAccessor.getPluginAccessor();

    /**
     * Creates validator instances from all validator classes configured in jira.properties for an operation.
     * @param strSection
     * @param issue
     * @return
     * @throws ClassNotFoundException
     * @throws NoSuchMethodException
     * @throws SecurityException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     * @throws InstantiationException
     * @throws IllegalArgumentException
     */
    protected static List<OperationValidator> getValidators(String strSection,
                                                            Issue issue)
        throws ClassNotFoundException,
               SecurityException,
               NoSuchMethodException,
               IllegalArgumentException,
               InstantiationException,
               IllegalAccessException,
               InvocationTargetException
    {
        List<OperationValidator> listValidators = new ArrayList<OperationValidator>();
        Plugin plugin = pluginAccessor.getPlugin("YOUR PLUGIN KEY"); //TODO: Define here the ID of the plugin that contains the
                                                                     //validator implementation (it is the ID of the next plugin)
        if ( ( null == plugin ) || ( ! plugin.isEnabled() ) ) //Don't know how to avoid deprecated code
            return listValidators;
        @SuppressWarnings("unchecked")
        List<Map<String, Object>> listValidatorData;//TODO: Define how to get the list of validators here. A map that contains at least the classname is assumed
        if ( null != listValidatorData)
        {
            for ( Map<String, Object> mapValidatorData : listValidatorData)
            {
                String strClassName = (String)mapValidatorData.get("classname");
                @SuppressWarnings("rawtypes")
                Class clazz = Class.forName(strClassName, true, plugin.getClassLoader());
                @SuppressWarnings("rawtypes")
                Class arParameterTypes[] = new Class[0];
                @SuppressWarnings("rawtypes")
                Constructor constructor = clazz.getConstructor(arParameterTypes);
                Object arArguments[] = new Object[0];
                Object objInstance = constructor.newInstance(arArguments);
                listValidators.add((OperationValidator)objInstance);
            }
        }
        return listValidators;
    }

    /**
     * Validates the values in the field values holder for this operation, user and issue against all configured validators.
     * Returns a list of problems or null in case everything is fine.
     * @param strSection
     * @param issue
     * @param user
     * @param mapFieldValuesHolder
     * @return
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     * @throws InstantiationException
     * @throws NoSuchMethodException
     * @throws ClassNotFoundException
     * @throws IllegalArgumentException
     * @throws SecurityException
     */
    public static ErrorCollection isValid(String strSection,
                                          Issue issue,
                                          User user,
                                          Map<String, Object> mapFieldValuesHolder)
        throws ClassNotFoundException,
               SecurityException,
               NoSuchMethodException,
               IllegalArgumentException,
               InstantiationException,
               IllegalAccessException,
               InvocationTargetException
    {
        List<OperationValidator> listValidator = getValidators(strSection, issue);
        for ( OperationValidator validator : listValidator )
        {
            ErrorCollection errorCollection = validator.isValid(user, issue, mapFieldValuesHolder);
            if ( ( null != errorCollection ) && ( errorCollection.hasAnyErrors() ) )
            {
                return errorCollection;
            }
        }
        return null;
    }
}

Validator plugin

We introduced a new plugin named jira-issue-operation-workflow. It is a version-2 plugin and can therefore be easily updated without having to restart Jira. It exports all its content.

The plugin descriptor looks like this:

<atlassian-plugin key="${plugin.groupid}.${plugin.artifactid}"
                  name="${plugin.name}"
                  plugins-version="2">
    <plugin-info>
        <description>${plugin.description}</description>
        <version>${plugin.version}</version>
        <vendor name="INTERSHOP Communications AG"
                url="http://www.intershop.de"/>
        <bundle-instructions>
            <Export-Package>
                com.intershop*
            </Export-Package>
        </bundle-instructions>
    </plugin-info>
</atlassian-plugin>

Validator plugin - classes

The plugin contains currently only one example validator. The validator could verify that value for affects version is an earlier version than that for fix versions. It looks like the following example:

public class VersionAndBuildNumberEditOperationValidator
    implements OperationValidator
{
    private final Logger logger = Logger.getLogger(VersionAndBuildNumberEditOperationValidator.class);

    public ErrorCollection isValid(User user, Issue issue, Map<String, Object> mapFieldValuesHolder)
    {
        ErrorCollection errorCollection = new SimpleErrorCollection();

        //TODO: Your validation code

        if ( errorCollection.hasAnyErrors() )
        {
            return errorCollection;
        }
        return null;
    }
}

Edit action

After introducing all relevant classes, we have to make sure the validators are called when an issue is edited. To support this, we provide a plugin which overwrites the edit action. This plugin must also be a version-2 plugin. The descriptor starts very similar to the descriptors mentioned above. Later on, it contains a section related to the Edit operation:

<resource type="i18n"
              name="${plugin.name} - Edit issue - i18n"
              location="com.intershop.jira.web.action.issue.edit.EditIssue"/>
    <webwork1 key="${plugin.artifactid}-editissue.webwork"
              name="${plugin.name} - Edit Issue Webwork Module">
        <actions>
            <action name="com.intershop.jira.web.action.issue.edit.EditIssue"
                    alias="EditIssue">
                <view name="error">/secure/views/issue/editissue.jsp</view>
                <view name="input">/secure/views/issue/editissue.jsp</view>
                <view name="issue-permission-error">/issue-permission-error.jsp</view>
            </action>
        </actions>
    </webwork1>

This code has been copied from atlassian-jira\WEB-INF\classes\actions.xml. Note that we do not introduce a new web item. We only replace the edit action, to make sure the changes also work for Greenhopper. The Ant variables are replaced with respective strings by our build process.

Edit action - the edit action class

The edit action is used only for the normal edit operation, but not by the quick edit operation. The implementation extends the normal edit action and basically only overwrites doValidation().

package com.intershop.jira.web.action.issue.edit;

import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.Map;

import org.apache.log4j.Logger;

import com.atlassian.jira.bc.issue.IssueService;
import com.atlassian.jira.bc.issue.comment.CommentService;
import com.atlassian.jira.config.ConstantsManager;
import com.atlassian.jira.config.SubTaskManager;
import com.atlassian.jira.issue.Issue;
import com.atlassian.jira.issue.fields.layout.field.FieldLayoutManager;
import com.atlassian.jira.issue.fields.screen.FieldScreenRendererFactory;
import com.atlassian.jira.user.util.UserUtil;
import com.atlassian.jira.util.ErrorCollection;
import com.atlassian.jira.util.SimpleErrorCollection;
import com.atlassian.jira.workflow.WorkflowManager;
import com.intershop.jira.operation.util.OperationValidatorUtils;

/**
 * Edit operation.
 * Overwritten to be able to support general validators in addition to field-based validators.
 */
@SuppressWarnings("serial")
public class EditIssue
    extends com.atlassian.jira.web.action.issue.EditIssue
{
    private static final Logger logger = Logger.getLogger(EditIssue.class);

    private Map<String, Object> mapFieldValuesHolder = null;

    public EditIssue(SubTaskManager subTaskManager,
                     ConstantsManager constantsManager,
                     FieldLayoutManager fieldLayoutManager,
                     WorkflowManager workflowManager,
                     FieldScreenRendererFactory fieldScreenRendererFactory,
                     CommentService commentService,
                     IssueService issueService,
                     UserUtil userUtil)
    {
        super(subTaskManager, constantsManager, fieldLayoutManager, workflowManager, fieldScreenRendererFactory,
              commentService, issueService, userUtil);
    }

    /**
     * This method is overwritten to get the field values to validate during quick edit.
     */
    @Override
    public void setFieldValuesHolder(@SuppressWarnings("rawtypes") final Map mapFieldValuesHolder)
    {
        super.setFieldValuesHolder(mapFieldValuesHolder);
        this.mapFieldValuesHolder = Collections.unmodifiableMap(mapFieldValuesHolder);
    }

    @Override
    protected void doValidation()
    {
        super.doValidation();
        if ( ! hasAnyErrors())
        {
            final Issue issue = getIssueObject();

            ErrorCollection errorCollection;
            try
            {
                errorCollection = OperationValidatorUtils.isValid("editissue",
                                                                  issue,
                                                                  getLoggedInUser(),
                                                                  mapFieldValuesHolder);
            }
            catch (Exception e)
            {
                logger.error(e.getMessage(), e);
                errorCollection = new SimpleErrorCollection();
                errorCollection.addErrorMessage(e.getMessage());
            }
            if ( ( null != errorCollection ) && ( errorCollection.hasAnyErrors() ) )
            {
                addErrorCollection(errorCollection);
            }
        }
    }
}

When there are no problems from super's validation, the operation validator utility class is used to execute all validations. We use the first argument ("editissue") here to find the configuration mentioned previously. Exceptions are converted to errors.

As one can see overwriting a plain Jira action is simple. Thank you, Atlassian.

Quick Edit action

Quick edit support is much more difficult. There is no action which can easily be replaced in an own plugin. In our environment, we had a lot of trouble if either

  • the quick edit action resides in an own plugin, while the original quick edit plugin is still available (non-unique beans), or
  • the quick edit action resides in an own plugin, while the original quick edit plugin is disabled (resource lookup seems to happen via the plugin key).

So we decided to replace the original quick edit plugin - sorry, Atlassian.

Because this there is another version-2 plugin, which is named exactly as the original plugin (including version number).

Please note that we're using the original plugin descriptor - see later in this section.

Quick Edit action - The action class

The quick edit action class is very similar to the edit action.

package com.intershop.jira.quickedit.action;

import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.Map;

import org.apache.commons.httpclient.HttpStatus;
import org.apache.log4j.Logger;

import webwork.action.ActionContext;

import com.atlassian.jira.bc.issue.IssueService;
import com.atlassian.jira.issue.Issue;
import com.atlassian.jira.issue.fields.rest.FieldHtmlFactory;
import com.atlassian.jira.quickedit.user.UserPreferencesStore;
import com.atlassian.jira.user.UserIssueHistoryManager;
import com.atlassian.jira.util.ErrorCollection;
import com.atlassian.jira.util.SimpleErrorCollection;
import com.atlassian.plugins.rest.common.json.DefaultJaxbJsonMarshaller;
import com.atlassian.plugins.rest.common.json.JaxbJsonMarshaller;
import com.intershop.jira.operation.util.OperationValidatorUtils;

/**
 * This class extends the standard quick edit operation by own validation features.
 * Note that the class is only used because the original quick edit plugin atlassian-plugin.xml file is patched.
 */
@SuppressWarnings("serial")
public class QuickEditIssue
    extends com.atlassian.jira.quickedit.action.QuickEditIssue
{
    private static final Logger logger = Logger.getLogger(QuickEditIssue.class);

    private final IssueService issueService;
    private Map<String, Object> mapFieldValuesHolder = null;
    private ErrorCollection errorCollection = null;

    public QuickEditIssue(IssueService issueService,
                          UserPreferencesStore userPreferencesStore,
                          UserIssueHistoryManager userIssueHistoryManager,
                          FieldHtmlFactory fieldHtmlFactory)
    {
        super(issueService, userPreferencesStore, userIssueHistoryManager, fieldHtmlFactory);
        this.issueService = issueService;
    }

    /**
     * This method is overwritten to get the field values to validate during quick edit.
     */
    @Override
    public void setFieldValuesHolder(final Map<String, Object> mapFieldValuesHolder)
    {
        super.setFieldValuesHolder(mapFieldValuesHolder);
        this.mapFieldValuesHolder = Collections.unmodifiableMap(mapFieldValuesHolder);
    }

    /**
     * Overwrite this method so our error collection is returned if needed.
     */
    @Override
    public String getErrorJson()
    {
        String strJson = null;
        if ( null == errorCollection )
        {
            strJson = super.getErrorJson();
        }
        else
        {
            final JaxbJsonMarshaller marshaller = new DefaultJaxbJsonMarshaller();
            strJson = marshaller.marshal(com.atlassian.jira.rest.api.util.ErrorCollection.of(errorCollection));
        }
        return strJson;
    }

    @Override
    protected void doValidation()
    {
        super.doValidation();
        if ( ! hasAnyErrors())
        {
            final IssueService.IssueResult result = issueService.getIssue(getLoggedInUser(), getIssueId());
            final Issue issue = result.getIssue();

            ErrorCollection errorCollection = null;
            try
            {
                errorCollection = OperationValidatorUtils.isValid("editissue",
                                                                  issue,
                                                                  getLoggedInUser(),
                                                                  mapFieldValuesHolder);
            }
            catch (Exception e)
            {
                logger.error(e.getMessage(), e);
                errorCollection = new SimpleErrorCollection();
                errorCollection.addErrorMessage(e.getMessage());
            }
            if ( ( null != errorCollection ) && ( errorCollection.hasAnyErrors() ) )
            {
                this.errorCollection = new SimpleErrorCollection();
                this.errorCollection.addErrorCollection(errorCollection);
                addErrorCollection(this.errorCollection);
                ActionContext.getResponse().setStatus(HttpStatus.SC_BAD_REQUEST);
            }
        }
    }
}

Quick edit action - patched plugin

While build scripts are generally shared between all plugins, a plugin-specific build script for quick edit is used to patch the original plugin and adjust its content.

In addition to adding our overwritten action class, three things have to be changed in the plugin descriptor. In our environment, this happens using Ant (I guess it is also simple via Maven):

  • Extract the whole content of the original plugin:
    <unjar dest="${build.target}/${component.pathname}/${plugin.pathname}/target"
                   overwrite="true">
                <fileset dir="../static/lib"
                         includes="jira-quick-edit-plugin-*.jar">
                </fileset>
                <patternset>
                    <include name="**/*"/>
                </patternset>
            </unjar>
  • Extending the plugin description so we can see which plugin is used:
    <replaceregexp file="${build.target}/${component.pathname}/${plugin.pathname}/target/atlassian-plugin.xml"
                           flags="s"
                           match="^(.*?<description>.*?)(</description>.*?)$"
                           replace="\1 - Patched by Intershop to support edit validator.\2"
                           byline="false">
            </replaceregexp>
  • Replacing the quick edit action with our action:
    <replaceregexp file="${build.target}/${component.pathname}/${plugin.pathname}/target/atlassian-plugin.xml"
                           flags="s"
                           match="^(.*?)com.atlassian.jira.quickedit.action.QuickEditIssue(.*?)$"
                           replace="\1com.intershop.jira.quickedit.action.QuickEditIssue\2"
                           byline="false">
            </replaceregexp>

Finally the content is stored into a jar with exactly the same name as the original quick edit plugin.

When this patched plugin version is uploaded into Jira, the original version is automatically disabled - despite being a system plugin. This helps to avoid trouble with bundled-plugins.zip, which is extracted each time Jira is restarted.

SOAP

After providing a solution for Edit and Quick Edit, SOAP issue updates should be validated as well. It is important to replace the original SOAP service by a custom service so that all 3rd party tools are using the new version.

We already extended SOAP by some methods, as providing information about SVN changes attached to an issue. But describing each step we did is beyond the focus of this article. As an introduction https://developer.atlassian.com/display/JIRADEV/Creating+a+JIRA+SOAP+Client can be used. In the end we have the following section in the plugin descriptor of our custom SOAP plugin:

<component key="rpcIssueService"
               name="Issue Service"
               class="com.intershop.jira.rpc.soap.service.IssueServiceImpl">
        <interface>com.intershop.jira.rpc.soap.service.IssueService</interface>
    </component>

In addition to other methods not related to this article, the issue service implementation contains the following method to validate input.

    /**
     * Before updating the issue, this method calls edit validators.
     */
    @Override
    public RemoteIssue updateIssue(User user, String strIssueKey, Map mapActionParams) throws RemoteException
    {
        ErrorCollection errorCollection = null;
        final IssueResult issueResult = issueService.getIssue(user, strIssueKey);
        try
        {
            errorCollection = OperationValidatorUtils.isValid("editissue",
                                                              issueResult.getIssue(),
                                                              user,
                                                              mapActionParams);
        }
        catch (Exception e)
        {
            throw new RemoteException(e);
        }
if ( ( null != errorCollection ) && ( errorCollection.hasAnyErrors() ) ) { StringBuilder sb = new StringBuilder(); if ( null != errorCollection.getErrors() ) { for ( String strKey : errorCollection.getErrors().keySet()) { if (0 < sb.length()) sb.append("\n"); sb.append(strKey + ": " + errorCollection.getErrors().get(strKey)); } } if ( null != errorCollection.getErrorMessages() ) { for ( String strMessage : errorCollection.getErrorMessages()) { if (0 < sb.length()) sb.append("\n"); sb.append(strMessage); } } throw new RemoteException(sb.toString()); } return super.updateIssue(user, strIssueKey, mapActionParams); }

This code calls the validator in the same manner as for Edit and Quick Edit.

Inline Edit

Inline editing is done via REST services. See next topic.

We plan to switch off inline editing completely (both for Jira and Greenhopper - note that Greenhopper does not support this at the moment). This is because permissions to edit an issue in our environment also depend on issue type, status and role. Atlassians current approach to configure those kind of permissions (see http://docs.atlassian.com/software/jira/docs/api/latest/com/atlassian/jira/security/WorkflowBasedPermissionManager.html) is not very convenient. It requires to deal with role IDs etc. We plan to provide an own security type, but for now inline editing will be switched off and is therefore not supported by our solution.

REST services


REST services are implemented in different classes. Without inspecting it in detail, it seems that Greenhopper uses an own REST implementation e.g. to update issues. As there is not only one service to handle, adding validators seems to be non-trivial and is currently not supported by our solution.

Bulk Edit

Not supported yet. Sorry.

Summary

As one can see with AUI and Greenhopper things are starting to become more complex. In addition to Edit, there is also Quick Edit and Inline Edit. Issues can also be updated via SOAP and REST.

As described our solution works for Edit, Quick Edit and SOAP. It seems to be simple to also do this for e.g. link actions. And since it's possible to overwrite the doExecute method in an action, one could also implement post functions.
The list of validators is (at least in our environment) issue type- and status-dependent. The validation happens on the server side so it cannot be hacked. Only Java is used. Updates of validators are possible since the plugins are all version-2 plugins, which can be reloaded. The related configuration can also be reloaded at runtime.

There are still some open topics: one system plugin was patched (Quick Edit).

A much better way for such kind of customisation would be a hook concept (or something similar) in the issue service implementation. While our solution requires overwriting several actions (and even then it is incomplete due to missing Inline Edit, REST and Bulk Edit support), it would be much more efficient to trigger data validation on the issue service's update method. Registering a custom issue service with according enhancements would allow to provide a fully integrated operation validation concept. But replacing a core service is not possible/recommended at the moment. Or does anybody know how to do this using only public parts of the Jira API?

Or, as another approach, why is it impossible to configure conditions, validators and post functions for operations like Edit? From the users point of view, operations and transitions are exactly the same, except that transitions change the issue status. So why is it impossible to handle both the same way and configure validators for Edit? Those validators could be executed too if an issue is updated via REST or Bulk Edit.

Despite this, hopefully this article will help someone when discussing validator concepts for issue editing in Jira.

12 answers

1 accepted

Comments for this post are closed

Community moderators have prevented the ability to post new answers.

Post a new question

2 votes
Answer accepted
JamieA
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
September 2, 2012

Interesting discussion. It sounds like you have implemented it pretty much the same as JSS. The same method of using edit functions is also in the script runner, I completed it several months ago but have not got round to writing the docs so not released yet. In the same way you did, when my plugin starts it installs a patched version of the quick edit plugin. I did this mostly as a fallback in case I had to abandon the behaviours plugin.

I had not done anything for soap, but REST is the way forward.

You mention bulk edit is not supported - this is quite a hole. How do you intend to deal with this?

Seems clear to me that there is a big gap in jira's API. You should be able to easily hook into the issue service to validate edits, rather than having to patch or override various system plugins. What's worse is the various methods of editing an issue are not consistent, they use different APIs, eg greenhopper, quick edit, bulk edit etc, which means you end up patching much more than you should have to (which would be nothing ideally). The method you have chosen is the only thing that could work currently, but is far from ideal. Changes need to be merged into the quick edit plugin every release, and just generally seems flaky. But that is not a criticism of your approach, as I say, Atlassian need to deal with this imho.

uvoellger September 2, 2012

Thanks. It seems that we did not go into the completely wrong direction.

So we ended both with the same topic: the API (in this case IssueService) should be extended. You perfectly summarized the reasons. Any ideas how to get Atlassian to extend the API? What does Atlassian say?

JamieA
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
September 2, 2012

I don't know... it's difficult to get things like this across to them through their jira instance. Needs someone who has some kind of relationship with the decision makers on the jira side. They may read this stuff though.

1 vote
davead75 September 2, 2012

~~~ spam ~~~

uvoellger September 3, 2012

For me, validators look much like an aspect on according service methods. So what's about provide validators as annotation to insert an aspect, similar to ideas in http://stackoverflow.com/questions/4829088/java-aspect-oriented-programming-with-annotations?

chintu Parameshwar September 30, 2012

Hi,

Thank you very much for writing this article!

I'm following this to write my own validator, btw I'm getting some compile errors:

[ERROR] BUILD FAILURE

[INFO] ------------------------------------------------------------------------

[INFO] Compilation failure

F:\Edit1\editvalidation\src\main\java\com\xxx\yyy\jiraplugins\EditIssu
e.java:[21,42] cannot find symbol
symbol  : class EditIssue
location: package com.atlassian.jira.web.action.issue

F:\Edit1\editvalidation\src\main\java\com\xxx\yyy\jiraplugins\EditIssu
e.java:[29,47] cannot find symbol
symbol  : class EditIssue
location: package com.atlassian.jira.web.action.issue

F:\Edit1\editvalidation\src\main\java\com\xxx\yyy\jiraplugins\EditIssu
e.java:[54,8] cannot find symbol
symbol  : variable super
location: class com.xxx.yyy.jiraplugins.EditIssue

F:\Edit1\editvalidation\src\main\java\com\xxx\yyy\jiraplugins\EditIssu
e.java:[51,4] method does not override or implement a method from a supertype

F:\Edit1\editvalidation\src\main\java\com\xxx\yyy\jiraplugins\EditIssu
e.java:[61,8] cannot find symbol
symbol  : variable super
location: class com.xxx.yyy.jiraplugins.EditIssue

F:\Edit1\editvalidation\src\main\java\com\xxx\yyy\jiraplugins\EditIssu
e.java:[62,15] cannot find symbol
symbol  : method hasAnyErrors()
location: class com.xxx.yyy.jiraplugins.EditIssue

F:\Edit1\editvalidation\src\main\java\com\xxx\yyy\jiraplugins\EditIssu
e.java:[64,32] cannot find symbol
symbol  : method getIssueObject()
location: class com.xxx.yyy.jiraplugins.EditIssue

I could solve most of the other dependancies except the above one.

FYI, I'm using atlassian sdk 4.0 and for the JIRA 5.1.5 version plugin.

Could you please help me on this?


Thanks a million in advance!

chintu Parameshwar September 30, 2012

Hi,

Thank you very much for writing this article!

I'm following this to write my own validator, btw I'm getting some compile errors:

[ERROR] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Compilation failure

F:\Edit1\editvalidation\src\main\java\com\teradata\engtools\jiraplugins\EditIssu
e.java:[21,42] cannot find symbol
symbol  : class EditIssue
location: package com.atlassian.jira.web.action.issue

F:\Edit1\editvalidation\src\main\java\com\teradata\engtools\jiraplugins\EditIssu
e.java:[29,47] cannot find symbol
symbol  : class EditIssue
location: package com.atlassian.jira.web.action.issue

F:\Edit1\editvalidation\src\main\java\com\teradata\engtools\jiraplugins\EditIssu
e.java:[54,8] cannot find symbol
symbol  : variable super
location: class com.teradata.engtools.jiraplugins.EditIssue

F:\Edit1\editvalidation\src\main\java\com\teradata\engtools\jiraplugins\EditIssu
e.java:[51,4] method does not override or implement a method from a supertype

F:\Edit1\editvalidation\src\main\java\com\teradata\engtools\jiraplugins\EditIssu
e.java:[61,8] cannot find symbol
symbol  : variable super
location: class com.teradata.engtools.jiraplugins.EditIssue

F:\Edit1\editvalidation\src\main\java\com\teradata\engtools\jiraplugins\EditIssu
e.java:[62,15] cannot find symbol
symbol  : method hasAnyErrors()
location: class com.teradata.engtools.jiraplugins.EditIssue

F:\Edit1\editvalidation\src\main\java\com\teradata\engtools\jiraplugins\EditIssu
e.java:[64,32] cannot find symbol
symbol  : method getIssueObject()
location: class com.teradata.engtools.jiraplugins.EditIssue

I could solve most of the other dependancies except the above one.

FYI, I'm using atlassian sdk 4.0 and for the JIRA 5.1.5 version plugin.

Could you please help me on this?

Thanks a million in advance!

0 votes
Striky Choong March 3, 2017

Did this solution still work in JIRA 7? I tried to extend the edit validation in JIRA 7.3.1 and it doesn't seem be to working anymore. sad

Matthias Schouten January 9, 2018

Were you able to do this with JIRA 7? I am currently trying this for JIRA 7.6 with no success unfortunately...

0 votes
Bharadwaj Jannu
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
December 8, 2013
public  void addJsonErrorMessages(String messages[])
{
	String arr$[] = messages;
	int len$ = arr$.length;
	for(int i$ = 0; i$ &lt; len$; i$++)
	{
		String message = arr$[i$];
		addErrorMessage(message);
		System.out.println(message);        
	}
	errors = ErrorCollection.of(this);
	if(getLoggedInUser() == null)
		ActionContext.getResponse().setStatus(401);
	else
		ActionContext.getResponse().setStatus(400);
}/* this code is influenced from https://answers.atlassian.com/questions/154953/adderrormessage-throws-exception-from-extended-quickcreateissue-action-in-jira-5*/

0 votes
Bharadwaj Jannu
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
December 8, 2013
protected void doValidation() 
{
	super.doValidation();

	...

	/* whenever we are required to place validation error, we place the following type of statements*/

	addError("components","Select Only One Component");

	addJsonErrorMessages(new String[0]);

   ...

0 votes
Bharadwaj Jannu
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
December 8, 2013
/* The following statements are added in  the doValiation() after validation code ends*/

Map&lt;String,String&gt; maperr= getErrors();

Iterator it=maperr.entrySet().iterator();

while (it.hasNext()) {

Map.Entry pairs = (Map.Entry)it.next();

 

System.out.println(pairs.getKey() + " = " + pairs.getValue()+"type");

 

temp.addError(pairs.getKey().toString(), pairs.getValue().toString());

}

temp2=ErrorCollection.of(temp); 

if (temp2.hasAnyErrors()==false ) {

System.out.println(issueInputParameters.onlyValidatePresentFieldsWhenRetainingExistingValues());

System.out.println(issueInputParameters.retainExistingValuesWhenParameterNotProvided());

System.out.println(super.isSingleFieldEdit());

System.out.println(issueInputParameters.onlyValidatePresentFieldsWhenRetainingExistingValues());

System.out.println(issueInputParameters.getSummary());

 

updateValidationResult = issueService.validateUpdate(getLoggedInUser(),  missue.getId(), issueInputParameters);

 

System.out.println(updateValidationResult.isValid()+"Has");

 

setFieldValuesHolder(updateValidationResult.getFieldValuesHolder());

 

IssueResult updateResult=null;  

 

System.out.println("UVR");

 

updateResult = issueService.update(getLoggedInUser(),updateValidationResult);

 

MutableIssue updatedIssue = updateResult.getIssue();

 

populateIssueFields(updatedIssue, false, temp2);//Very IMP to make it false

 

System.out.println(updatedIssue.getSummary());

}

 

else{

populateIssueFields(missue, true, temp2);

}

Bharadwaj Jannu
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
December 8, 2013

Continuation in page2

0 votes
Bharadwaj Jannu
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
December 8, 2013
private void populateIssueFields(Issue issue, boolean retainValues, ErrorCollection errorCollection)
    {
        long start = System.nanoTime();
        
        List editFields;
        
        if(issueService.isEditable(issue, getLoggedInUser()))
            editFields = this.fieldHtmlFactory.getEditFields(getLoggedInUser(), this, this, issue, retainValues);
        
        else
            editFields = Collections.emptyList();
        
        long fieldEnd = System.nanoTime();
        
        OpsbarBean opsbarBean = (new OpsbarBeanBuilder(issue, getApplicationProperties(), simpleLinkManager, authContext, issueManager, pluginAccessor)).build();
        
        IssueBean issueBean = new IssueBean(issue, issue.getProjectObject(), issue.getStatusObject(), opsbarBean);
        
        long issueEnd = System.nanoTime();
        
        IssueWebPanelsBean panels = this.webPanelMapperUtil.create(issue, this);
        //panels.getInfoPanels();
        //panels.getRightPanels();
        long panelsEnd = System.nanoTime();
        
        this.eventPublisher.publish(new IssueRenderTime(fieldEnd - start, issueEnd - fieldEnd, panelsEnd - issueEnd));
        
        this.fields = new IssueFields(editFields, getXsrfToken(), errorCollection, issueBean, panels);
    }
	
	public String getJson()
    {
		if(this.fields !=null)
		{	
			System.out.println("\nTest our logic in getJson()\n");
			JaxbJsonMarshaller marshaller = new DefaultJaxbJsonMarshaller();
			return marshaller.marshal(this.fields);
		}
		else
		{
			System.out.println("\nTest super class logic in getJson()\n");
			return super.getJson();
		}
    }

Bharadwaj Jannu
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
December 8, 2013

This code is required inorder to render the issue panels correctly in view-issue-page

0 votes
Bharadwaj Jannu
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
December 8, 2013

hello Uwe Voellger,

you need to unjar jira-issue-nav-plugin-*.jar and try to extend IssueAction class.

public class IssueAction extends com.atlassian.jira.plugin.issuenav.action.IssueAction
{
	private final UserIssueHistoryManager userIssueHistoryManager;
	private final WebPanelMapperUtil webPanelMapperUtil;
    private final PluginAccessor pluginAccessor;
    private final JiraAuthenticationContext authContext;
    private final IssueManager issueManager;
	private final SimpleLinkManager simpleLinkManager;
    private final EventPublisher eventPublisher;
    //private final FieldHtmlFactory fieldHtmlFactory;
    private IssueFields fields;
    private ErrorCollection temp2;
    private ErrorCollection errors;
    
    com.atlassian.jira.util.ErrorCollection temp =new SimpleErrorCollection();
	
    private com.atlassian.jira.bc.issue.IssueService.UpdateValidationResult updateValidationResult;
	
	public IssueAction(IssueService issueService,
			UserIssueHistoryManager userIssueHistoryManager,
			FieldHtmlFactory fieldHtmlFactory,
			WebPanelMapperUtil webPanelMapperUtil,
			PluginAccessor pluginAccessor,
			JiraAuthenticationContext authContext, IssueManager issueManager,
			SimpleLinkManager simpleLinkManager, EventPublisher eventPublisher) {
		super(issueService, userIssueHistoryManager, fieldHtmlFactory,
				webPanelMapperUtil, pluginAccessor, authContext, issueManager,
				simpleLinkManager, eventPublisher);
		//this.fieldHtmlFactory=fieldHtmlFactory;
		this.userIssueHistoryManager = userIssueHistoryManager;
		this.webPanelMapperUtil = webPanelMapperUtil;
		this.pluginAccessor = pluginAccessor;
		this.authContext = authContext;
		this.issueManager = issueManager;
		this.simpleLinkManager = simpleLinkManager;
		this.eventPublisher = eventPublisher;
	}
	
	IssueService issueService=ComponentAccessor.getIssueService();
0 votes
uvoellger December 5, 2013

Hello,

please note that it is no longer possible to switch off inline edit in Jira 6 so the disussed solution proposal finally does not help to guarantee consistent data.

Sorry, but I have no workaround for that.

Uwe

Bharadwaj Jannu
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
December 5, 2013

but using your solution for quick edit, I tried to implement for inline edit. It is working good for Jira 6.x version also.

uvoellger December 5, 2013

Hello,

did you successfully implement a validator support that runs during inline edit? Could you share your solution here for that? I thought this is nearly impossible...

Thanks a lot,

Uwe

0 votes
Dmitry Zagorovsky _StiltSoft_ April 16, 2013

It's seems like that approach did't work on Jira 5.2.10

Uwe, do you have any ideas how to make it work on Jira 5.2.10?

Thanks!

uvoellger April 16, 2013

Hello,

we are still on 5.2.8. There are no (known) issues with this version. You might mention problem details...

Migration of our Jira instance to latest Jira release takes some time...

Sorry.

0 votes
uvoellger October 15, 2012

Answer by myself - despite there is no comment from Atlassian.

0 votes
uvoellger September 30, 2012

Hello,

as already mentioned we do not use exactly the same sources as mentioned in this article. Because this you might get some compile problems. Basically it should be simple to solve these problems.

We do not use atlassian sdk, but I guess it also provides the classpath valid for compilation of a plugin. If so, the problem you mentioned might be related to missing plugin imports of your version-2-plugin that contains the edit operation. Try to specify them:

&lt;bundle-instructions&gt;
            &lt;Import-Package&gt;
                com.atlassian*,
            &lt;/Import-Package&gt;
        &lt;/bundle-instructions&gt;

Uwe

chintu Parameshwar October 2, 2012

Uwe/Jamie,

Thanks, it is compiling now, but I did not understand what to write/add at: .

List&lt;Map&lt;String, Object&gt;&gt; listValidatorData;//TODO: Define how to get the list of validators here. A map that contains at least the classname is assumed

I will be very happy if you can send me a sample plugin code(working). I'm in big need of this edit validation plugin.

And I'm waiting for Script Runner v 2.1 which has this implementation. Could you please let me know when it is releasing. We are waiting for JSS next version or Script Runner 2.1 for JIRA 5.1.4.

Thanks you very much in advance!

uvoellger October 3, 2012

Hello,

  • Purpose of listValidatorData
    The validators must somewhere be defined. We define them in our Jira configuration file which is located outside of any plugin and reloaded as soon as it is changed. There is one list of edit validators for each issue type and status. But this Jira configuration file is very specific - it does not make sense to copy according code. And I do not have other code. But you could define them in a resource file located in the utility plugin. While it should be simple for you to introduce such a file, define the properties there and add code to read and convert them, this approach still allows to dynamically change the list of validators at runtime (by reloading the plugin). At the end, the validator data is hold in this variable.

    The edit action should be able to run several validator. Because this it is a list.

    Commonly, each validator needs some parameter in addition to the class name. This is the reason for a list of maps.

    Here is an example for the data structure you could define:
    editissue.task.status.open.validator.0.classname=com.intershop.jira.operation.validator.FieldHasSingleSelectionEditOperationValidator
    editissue.task.status.open.validator.1.classname=com.intershop.jira.operation.validator.VersionEditOperationValidator
    editissue.task.status.open.validator.1.fieldid=fixVersions
    editissue.task.status.open.validator.1.checks=planning:true
  • And I'm waiting for Script Runner v 2.1 which has this implementation. Could you please let me know when it is releasing.
    Jamie?

chintu Parameshwar October 10, 2012

Jamie, Please let me know your release(probable) date of v 2.1

Comments for this post are closed

Community moderators have prevented the ability to post new answers.

Post a new question

TAGS
AUG Leaders

Atlassian Community Events