JIRA Plugins: JQL en JIRA

JIRA es una solución para la gestión integral de proyectos que permite elaborar filtros complejos para el seguimiento de proyectos y generación de Informes. En el siguiente artículo mostramos cómo desarrollar funciones JQL ad-hoc para complementar la larga lista que proporciona la herramienta a partir de la versión 4.

¿Qué es JQL?

JQL (JIRA Query Language) es un lenguaje de sintaxis similar a SQL para realizar búsquedas complejas sobre el modelo de datos de JIRA desde la versión 4.0. Una búsqueda en JQL consiste en la concatenación lógica (OR, AND) de cláusulas cada una de las cuales está formada por un campo seguida de un operador y un conjunto de valores o funciones. Por ejemplo:

project = "New office" AND status = "open"

La sentencia JQL anterior retorna todos los issues del proyecto «New office» en estado «open». También es posible emplear funciones que recuperen valores dinamicamente:

assignee != currentUser()

Mediante esta sentencia obtendríamos todos los issues asignados a un usuario diferente al que ha iniciado la sesión actual en la herramienta, cuyo nombre es retornado por la función currentUser().

¿Es posible extender la sintáxis de JQL?

Desde su introducción en la versión 4.0, JQL es una de las funcionalidades de JIRA en la que más se ha avanzado en cada nueva actualización, sobre todo con la introducción de nuevos operadores para búsquedas en el histórico de cambios (WAS y CHANGED). En el siguiente ejemplo buscamos los issues resueltos por el usuario actual durante el mes de Octubre de 2011.

status CHANGED by currentUser() FROM Open TO Resolved DURING("2011/10/01","2011/10/31")

Aunque no es posible crear nuevos operadores mediante el uso de plugins, JIRA permite la creación de funciones JQL que permitan la creación de sentencias complejas o, simplemente, imposibles con las sintaxis que proporciona la herramienta. En este artículo os enseñaremos como crear una función JQL que retorne todas las issues que están bloquedas utilizando el mecanismo de vinculación (issue linking) nativo de JIRA.

Funciones JQL en un plugin

Para crear la nueva función JQL utilizaremos un plugin de JIRA siguiendo las directrices que os contamos en el blog: Atlassian plugins con eclipse y maven.

1. Crea la clase BlockedIssuesJqlFunction.java

Para crear una función JQL es necesario implementar la interfaz JqlFunction definida en el API de JIRA. Afortunadamente, el API de JIRA proporciona una clase abstracta que nos simplifica esta tarea: AbstractJqlFunction. Para extender la clase AbstractJqlFunction bastará con implementar los siguientes métodos:

  • getMinimumNumberOfExpectedArguments(): establece el número de argumentos que soporta la función, en nuestro ejemplo, 0.
  • getDataType(): Especifica el tipo de campo que retorna, en el ejemplo, JiraDataTypes.ISSUE.
  • validate(…): valida los argumentos proporcionados a la función.
  • getValues(): retorna la lista de objetos en forma de literales.

Utilizando el interfaz de eclipse creamos la clase BlockedIssuesJqlFunction.java que extiende la clase base AbstractJqlFunction:

public class BlockedIssuesJqlFunction extends AbstractJqlFunction {
    @Override
    public JiraDataType getDataType() {
            return JiraDataTypes.ISSUE;
   }

    @Override
    public int getMinimumNumberOfExpectedArguments() {
        return 0;
    }

   @Override
   public List<QueryLiteral> getValues(QueryCreationContext queryCreationContext, FunctionOperand operand, TerminalClause terminalClause) {
   // TODO Auto-generated method stub
	return null;
    }
    
    @Override
    public MessageSet validate(User searcher, FunctionOperand operand, TerminalClause terminalClause) {
	// TODO Auto-generated method stub
	return null;
    }
}

2. Implementa validate(…)

El método validate(…) debe comprobar la validez de los argumentos que se suministran en la llamada a la función Jql y generar los mensajes de error (mediante un objecto MessageSet) oportunos en caso de error. La validación que realizaremos en nuestro ejemplo consistirá en comprobar:

  1. Que la vinculación de issues en JIRA está activada.
  2. Que el tipo de vinculación ‘is blocked by‘ retorna algún candidato para lo cual emplearemos las clases IssueLinkTypeManager e IssueLinkManager que serán suministradas en construcción mediante la inyección de tipos.
 
public class BlockedIssuesJqlFunction extends AbstractJqlFunction {
    private final IssueLinkTypeManager issueLinkTypeManager;
    private final IssueLinkManager issueLinkManager;
    private final static String LINK_NAME = "is blocked by";
    public BlockedIssuesJqlFunction(IssueLinkManager issueLinkManager, IssueLinkTypeManager issueLinkTypeManager) {
		super();
		this.issueLinkTypeManager = issueLinkTypeManager;
		this.issueLinkManager = issueLinkManager;
	}
	@Override
	public MessageSet validate(User searcher, FunctionOperand operand, TerminalClause terminalClause) {
        MessageSet messageSet = new MessageSetImpl();
        if (!issueLinkManager.isLinkingEnabled()) {
            messageSet.addErrorMessage("Invalid argument for " + getFunctionName());
            return messageSet;
        }
        final List<String> args = operand.getArgs();
        if (args.isEmpty()) {
            messageSet.addErrorMessage("Invalid argument for " + getFunctionName());
            return messageSet;
        }
        return messageSet;
    }
...
}

3. getValues(…): Retorna los valores como literales

Empleando los objetos Manager suministrados en creación recuperamos los issues que tienen una vinculación de tipo «is blocked by». A continuación convertimos la colección de issues en una colección de literales (objetos de la clase QueryLiteral).

@Override
public List<QueryLiteral> getValues(QueryCreationContext queryCreationContext, FunctionOperand operand, TerminalClause terminalClause) {
    notNull("queryCreationContext", queryCreationContext);
    final List<QueryLiteral> literals = new LinkedList<QueryLiteral>();
    Collection<IssueLinkType> linkTypes = issueLinkTypeManager.getIssueLinkTypesByOutwardDescription(LINK_NAME);
    if (null == linkTypes || linkTypes.isEmpty() || null == issueLinkManager.getIssueLinks(linkTypes.iterator().next().getId())) {           
        return null;
    }
    Set<Issue> linkedIssues = new LinkedHashSet<Issue>();
    for (IssueLinkType linkType : linkTypes)    {
        List<Issue> issues = new LinkedList<Issue>();
        Collection<IssueLink> links = issueLinkManager.getIssueLinks(linkType.getId());
        for (IssueLink link: links) {
            issues.add(link.getSourceObject());
        }
        if (issues != null) {
            linkedIssues.addAll(issues);
        }
    }
    for (Issue issue : linkedIssues) {
        literals.add(new QueryLiteral(operand, issue.getId()));
    }
    return literals;
}

4. Descriptor del plugin

Los plugin desplegables desde la interfaz de JIRA emplean un descriptor XML para permitir al plugin registrarse en JIRA mediante Osgi. Para que la clase que acabamos de escribir esté disponible como función Jql en JIRA necesitamos incluir la siguiente información en el fichero src/main/resources/atlassian-plugin.xml.

<jql-function key="blocked-issues" name="Blocked Issues Function" class="com.novagenia.jira.plugins.jql.function.BlockedIssuesJqlFunction">
  <!--The name of the function-->
  <fname>blockedIssues</fname>
  <description>Provides a JQL function to returned issues with is blocked http://www.novagenia.com/wp-admin/post.php?post=223&action=edit# by link.</description>
  <!--Whether this function returns a list or a single value-->  
<list>true</list>
</jql-function>
 

Despliega el plugin y sobre la interfaz de búsqueda avanzada introduce la siguiente query:

issue in blockedIssues()

Una imagen vale más que mil palabras…

El toque final

A continuación describimos algunas mejoras que darán más potencia a tu primer plugin además de mejorar las tareas de depuración una vez instalado.

1. Añade trazas de log

Las trazas son muy útiles para poder depurar un plugin desplegado en una instancia de JIRA. Además, desde la interfaz de administración de JIRA podemos modificar el nivel de traza de todos los logger establecidos en arranca a través del fichero log4j.properties sin necesidad de rearrancar la herramienta.

Para generar trazas vinculadas a una clase de nuestro plugin instanciaremos un campo de la clase mediante la siguiente sentencia:

private final Logger log = Logger.getLogger(BlockedIssuesJqlFunction.class);

En los métodos de la clase donde queramos volcar trazas o notificar errores invocaremos los métodos debug, warn, error o fatal para especificar un mensaje con la correspodiente severidad:

log.warn("Linking is not enabled!");

2. Refactoriza para incluir otros vínculos (links)

A partir del ejemplo anterior podemos extender la funcionalidad de nuestro plugin refactorizando el código para hacerlo más genérico. Aunque no es el objetivo de este tutorial, utilizaremos la refactorización «Extract Superclass» para parametrizar el nombre del vínculo entre issues.

Conseguir que ambas clases compilen y funcionen sin errores es relativamente sencillo y lo dejamos como ejercicio. A continuación exportamos la nueva clase mediante el descriptor del plugin:

 
<jql-function key="linked-issues" name="Linked Issues With Function" class="com.novagenia.jira.plugins.jql.function.BaseLinkedIssuesJqlFunction">
    <!--The name of the function-->    
<fname>linkedIssuesWith</fname>
    <description>Provides a JQL function to get all issues with an outward link.</description>    <!--Whether this function returns a list or a single value-->
    <list>true</list></jql-function>
 

3. Preserva la información (sanitize)

Por último vamos a describir un procedimiento (sanitising) opcional pero muy conveniente cuando nuestras funciones van a ser desplegadas en una instancia en producción. El objetivo es ocultar la información sobre campos y argumentos de nuestra función. Esto es importante porque no todos los usarios tienen el mismo nivel de acceso a todos los proyectos o issues y no queremos que un usuario pueda inferir la existencia de estos elementos a partir de la query de un filtro compartido. Por este motivo se sustituyen las cadenas que sirven de clave para issues, proyectos, etc. por su id en la tabla correspondiente. Si una función Jql requiere de este mecanismo de ocultación deberá implementar la interfaz ClauseSanitisingJqlFunction.

En nuestro ejemplo esto no es necesario ya que la información sobre los distintos tipos de vínculo es pública. En el siguiente enlace podéis encontrar información detallada sobre cómo implementar este mecanismo.

Comparte

Como en el resto de los tutoriales de El Manifiesto, podeis descargar el código completo desde el repositorio público que hemos abierto en bitbucket.org: http://git.novagenia.com. Para los que no conocéis bitbucket, se trata de un servidor público de repositorios de código para Mercurial y Git que ofrece hosting gratuito para proyectos públicos y privados hasta 5 usuarios.

Como siempre agradecemos los comentarios constructivos que nos ayuden a mejorar.

Saludos,

Eduardo Mayor

Eduardo Mayor

CEO

Eduardo Mayor is a software engineer with 20+ years of experience. He is also the founder of Novagenia Information Technologies, a company focused on introducing agile methods and tools to companies worldwide.

Si continuas utilizando este sitio aceptas el uso de cookies. más información

Los ajustes de cookies de esta web están configurados para "permitir cookies" y así ofrecerte la mejor experiencia de navegación posible. Si sigues utilizando esta web sin cambiar tus ajustes de cookies o haces clic en "Aceptar" estarás dando tu consentimiento a esto.

Cerrar