Jira App-Entwicklung: JQL verarbeiten ohne Kopfschmerzen

7. Jul. 2020Best Practices, Entwicklung, Jira Software

Die Herausforderung

In vielen Projekten haben wir Kundenanforderungen, die mit den normalen Bordmitteln von Jira nicht mehr zu erfüllen sind. Häufig ist die einfachste Lösung, selbst ein kleines Add-on zu implementieren. Eine solche Anforderung, die wir kürzlich umsetzen mussten ist, die Projekte herauszufinden, in denen ein Agile Board angezeigt wird:

In einer ersten Analyse hat sich gezeigt, dass es keine direkte Verknüpfung zwischen Agile Boards und Projekten gibt. Die Zugehörigkeit wird ausschliesslich durch den JQL – Filter bestimmt. Steht ein project = <name> im Filter – Query wird das Board angezeigt, sonst nicht. Die Lösung beinhaltet also, das Filter-Query nach project-Klauseln zu durchsuchen.

Vorbereitungen

Um überhaupt mal an den JQL-Filter zu kommen, benötigt man zwei Services, den RapidViewService und den SearchRequestManager:

List<RapidView> boards = rapidViewService.getRapidViews(adminUser).get();
 
for (RapidView board : boards) {
    SearchRequest filter = searchRequestManager.getSearchRequestById(board.getSavedFilterId());
    <... hier fahren wir weiter ...>
}

Erster Versuch

Der erste naive Anlauf in so einem Fall ist normalerweise, den String zu nehmen und mit Vergleichsoperationen nach project zu suchen.

String jql = filter.getQuery().getQueryString();
 
if(jql.contains("project =")) {
    "..."
}

Dieser Ansatz funktioniert, ist aber relativ aufwändig:

  1. Überprüfen, ob project im String vorkommt
  2. Position des nachgestellten Arguments finden (Name des Projektes)
  3. Ende des Projektnamens finden
  4. mit substring den Projektnamen extrahieren

Dazu kommen noch diverse Sonderfälle, die behandelt werden müssen, unter anderem:

  • Ist nach dem Vergleichsoperator = noch ein Leerzeichen? Oder zwei? Oder ein Tabulator?
  • Ist der Projektname in Anführungszeichen? Oder ein Projektkey ohne Anführungszeichen
  • project in (key1, key2, …) muss auch berücksichtigt werden

Und, nicht zuletzt, wir verlieren die Typsicherheit, die Java und JQL bieten.

Zweiter Versuch

Betrachtet man im obigen Code die Klasse SearchRequest genauer, sieht man, dass getQuery() nicht direkt den String zurückgibt, sondern ein Objekt der Klasse Query. Diese Klasse selbst ist noch nicht besonders interessant, wer schon einmal in seinem Java Code Suchanfragen an Jira gemacht hat, kennt sie schon. Interessant ist die Methode, getWhereClause() und das Clause-Objekt, dass sie zurückgibt. Leider ist die Google-Suche nicht gerade ergiebig mit Dokumentation und Beispielen. Nach ein bisschen Experimentieren ergab sich aber dann die folgende Lösung:

Clause Visitor

Ein JQL Filter kann mit der Methode accept(ClauseVisitor<R> visitor) verarbeitet werden. Der Parameter deutet schon darauf hin, dass das Besucher-Entwurfsmuster verwendet wird. Die abstrakte Klasse ClauseVisitor kann gleich als anonyme Klasse implementiert werden:

Clause jqlClause = filter.getQuery().getWhereClause();
 
jqlClause.accept(new ClauseVisitor<Object>() {
    @Override
    public Object visit(AndClause ac) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }
 
    @Override
    public Object visit(NotClause nc) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }
 
    @Override
    public Object visit(OrClause oc) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }
 
    @Override
    public Object visit(TerminalClause tc) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }
 
    @Override
    public Object visit(WasClause wc) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }
 
    @Override
    public Object visit(ChangedClause cc) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }
});

Für diese Anforderung sind wir prinzipiell an der TerminalClause interessiert:

@Override
public Object visit(TerminalClause tc) {
    if (tc.getName().equals("project")) {
        System.out.println("Found Project " + tc.getOperand().getDisplayString());
    }
    return null;
}

Das deckt z.B. folgenden Filter ab:

project = DEMO ORDER BY Rank ASC

Der folgende Filter geht aber noch nicht:

project = DEMO AND assignee = demouser ORDER BY Rank ASC

Dafür müssen die beiden Subklauseln der AND-Klausel ausgewertet werden. Damit man nicht auf eine oder zwei Ebenen eingeschränkt ist, macht man am besten gleich eine rekursive Funktion daraus:

private void checkClause(Clause clause) {
    clause.accept(new ClauseVisitor<Object>() {
        @Override
        public Object visit(AndClause ac) {
            if (clause.getClauses().size() > 0) {
                for (Clause c : clause.getClauses()) {
                    checkClause(c);
                }
            }
            return null;
        }
 
        @Override
        public Object visit(NotClause nc) {
            if (clause.getClauses().size() > 0) {
                for (Clause c : clause.getClauses()) {
                    checkClause(c);
                }
            }
 
            return null;
        }
 
        @Override
        public Object visit(OrClause oc) {
            if (clause.getClauses().size() > 0) {
                for (Clause c : clause.getClauses()) {
                    checkClause(c);
                }
            }
 
            return null;
        }
 
        @Override
        public Object visit(TerminalClause tc) {
            if (tc.getName().equals("project")) {
                System.out.println("Found Project " + tc.getOperand().getDisplayString());
            }
            if (clause.getClauses().size() > 0) {
                for (Clause c : clause.getClauses()) {
                    checkClause(c);
                }
            }
            return null;
        }
 
        @Override
        public Object visit(WasClause wc) {
            if (clause.getClauses().size() > 0) {
                for (Clause c : clause.getClauses()) {
                    checkClause(c);
                }
            }
            return null;
        }
 
        @Override
        public Object visit(ChangedClause cc) {
            if (clause.getClauses().size() > 0) {
                for (Clause c : clause.getClauses()) {
                    checkClause(c);
                }
            }
            return null;
        }
    });
}

Bitte beachten, dass zur Vollständigkeit auch die NOT-Klauseln hier mitverarbeitet werden. Das gibt unter Umständen nicht das gewünschte Resultat.

Operand Visitor

Der bisherige Code deckt den Fall  project = key ab. Nicht berücksichtigt sind Queries wie project in (key1, key2). Dafür braucht es einen zweiten Visitor, der die Operanden prüft. Da auch die Operanden verschachtelt sein können, legen wir diesen Visitor auch gleich als rekursive Funktion aus:

private List<String> checkOperand(Operand op) {
    op.accept(new OperandVisitor<Object>() {
        @Override
        public Object visit(EmptyOperand eo) {
            return null;
        }
 
        @Override
        public Object visit(FunctionOperand fo) {
            return null;
        }
 
        @Override
        public Object visit(MultiValueOperand mvo) {
            for (Operand o : mvo.getValues()) {
                checkOperand(o);
            }
            return null;
        }
 
        @Override
        public Object visit(SingleValueOperand svo) {
            System.out.println("Found Project " + svo.getStringValue());
 
            return null;
        }
    });
}

Die Methode für die TerminalClause im ClauseVisitor muss auch noch einmal angepasst werden:

@Override
public Object visit(TerminalClause tc) {
    if (tc.getName().equals("project")) {
        checkOperand(tc.getOperand());
    }
    if (clause.getClauses().size() > 0) {
        for (Clause c : clause.getClauses()) {
            checkClause(c);
        }
    }
    return null;
}

Fertiger Code

Hier noch einmal der fertige Code zur Übersicht:

private void checkClause(Clause clause) {
    clause.accept(new ClauseVisitor<Object>() {
        @Override
        public Object visit(AndClause ac) {
            if (clause.getClauses().size() > 0) {
                for (Clause c : clause.getClauses()) {
                    checkClause(c);
                }
            }
            return null;
        }
 
        @Override
        public Object visit(NotClause nc) {
            if (clause.getClauses().size() > 0) {
                for (Clause c : clause.getClauses()) {
                    checkClause(c);
                }
            }
 
            return null;
        }
 
        @Override
        public Object visit(OrClause oc) {
            if (clause.getClauses().size() > 0) {
                for (Clause c : clause.getClauses()) {
                    checkClause(c);
                }
            }
 
            return null;
        }
 
        @Override
        public Object visit(TerminalClause tc) {
            if (tc.getName().equals("project")) {
                checkOperand(tc.getOperand());
            }
            if (clause.getClauses().size() > 0) {
                for (Clause c : clause.getClauses()) {
                    checkClause(c);
                }
            }
            return null;
        }
 
        @Override
        public Object visit(WasClause wc) {
            if (clause.getClauses().size() > 0) {
                for (Clause c : clause.getClauses()) {
                    checkClause(c);
                }
            }
            return null;
        }
 
        @Override
        public Object visit(ChangedClause cc) {
            if (clause.getClauses().size() > 0) {
                for (Clause c : clause.getClauses()) {
                    checkClause(c);
                }
            }
            return null;
        }
    });
}
 
private List<String> checkOperand(Operand op) {
    op.accept(new OperandVisitor<Object>() {
        @Override
        public Object visit(EmptyOperand eo) {
            return null;
        }
 
        @Override
        public Object visit(FunctionOperand fo) {
            return null;
        }
 
        @Override
        public Object visit(MultiValueOperand mvo) {
            for (Operand o : mvo.getValues()) {
                checkOperand(o);
            }
            return null;
        }
 
        @Override
        public Object visit(SingleValueOperand svo) {
            System.out.println("Found Project " + svo.getStringValue());
 
            return null;
        }
    });
}

Fazit

Zugegeben, der Visitor-Code ist ein bisschen gewöhnungsbedürftig, und die Dokumentation zu diesen Klassen und Interfaces ist eher spärlich. Dafür haben wir nun zwei Methoden, die leicht an andere Bedürfnisse anpassbar sind. Sie sind typsicher, also kann uns die Entwicklungsumgebung unterstützen und der Compiler vor Fehlern bewahren, und sie ersparen uns, die Oben erwähnten Sonderfälle zu behandeln.

Haben Sie Fragen oder Anregungen zum diesem Blog-Beitrag? Dürfen wir Sie unterstützen?
Schreiben Sie uns auf hallo@zuara.ch oder rufen Sie uns an: 031 302 60 00. Wir freuen uns auf Ihre Anfrage!

Der Autor:

Roman Maire

Roman Maire

roman.maire@zuara.ch

Direkt: +41 79 307 60 29

      

Weitere Fachartikel und Neuigkeiten von Zuara

Wo befinden sich meine Daten bei Atlassian Cloud?

Wie im Blog «Atlassian setzt auf Cloud» beschrieben, wird ab dem 2. Februar 2021 keine neue Server-Lizenz für die Atlassian Produkte verkauft und die Lizenzen können maximal noch für 3 Jahre gelöst werden. Anschliessend stellt Atlassian das Produkt ein. Die Zeit bis...

5 Zeitdiebe im Arbeitsalltag

Wir alle kennen es: Wir nehmen uns vor, dies oder jenes an einem Tag, in einer Woche oder innerhalb eines bestimmten Monats zu erreichen, machen eine Planung, und trotzdem kommt alles anders. Wir erreichen unsere gesteckten Ziele nicht. Wie kommt es dazu?  Forscher...

Docker Images erstellen mit Packer

Wie in einem frühreren Blog-Post beschrieben, erstellen wir Test- und Schulungsumgebungen auf einer speziell dafür erstellten Plattform. Die Kernkomponente davon ist Docker. Wir ersparen uns so die manuellen Installationsschritte der Atlassian Applikationen. Wie die...

Jira Software Cloud – neu mit Performance-Messungen für Dev-Teams

Jira Software Cloud bietet 4 neue Features für Entwicklungs-Teams, die es endlich erlauben, Code und Code-Repositories mit Issues zu verknüpfen und die Deployments in verschiedenen Stages zu visualisieren. Die Folge davon: weniger Kontext-Wechsel, weniger...

Atlassian Cloud Datenschutz und -sicherheit – sind besonders schützenswerte Daten sicher?

Renato Furrer hat sich im vorgängigen Blog-Post bereits zum Datenstandort in der Atlassian Cloud geäussert. In diesem Blog möchte ich nun auf die Datensicherheit und den Datenschutz bei Atlassian generell eingehen und eine Einschätzung abgeben, ob die Cloud auch für...

Portfolio for JIRA® – Strategische Projektplanung direkt in JIRA®

Portfolio for JIRA® ist ein Add-on für die Projektmanagement- und Issue-Tracking Software JIRA®, die von Atlassian® entwickelt wird. Das Add-on ermöglicht eine GANTT-ähnliche Echtzeitplanung in JIRA®. Die Informationen für die Planung werden den JIRA® Issues...

Quo Vadis Jira Service Desk?

Ausgangslage Am 9. November hat Atlassian das neueste Produkt angekündigt: Jira Service Management. Es handelt sich dabei nicht um ein komplett neues Produkt, sondern um eine Weiterentwicklung von Jira Service Desk. Atlassian will damit auf geänderte Anforderungen und...

Auf 3 Ebenen effektiv zusammenarbeiten

Tools einzuführen oder zu optimieren bringt alleine gerade mal gar nichts. Die Tools müssen optimal auf die Teams und ihre Organisation abgestimmt werden, so dass Mitarbeitenden ihren Arbeitsalltag motiviert meistern. Dazu ist ein Blick "unter" die Tool-Ebene nötig...

Alles rund um Stichwörter in Confluence

Mithilfe von Stichwörtern in Confluence können Sie Ihre und auch die Effizienz Ihres kompletten Teams erhöhen. Viele User erstellen eine Seite, einen Blog-Artikel oder auch Anhänge, ohne diese mit einem Stichwort zu versehen. Je mehr Inhalte erstellt werden, umso...

Virtuelle Büros = weniger Home-Office-Einsamkeit?

Zwar sind wir aus dem Homeoffice heraus mit den bestehenden Chat- und Remote-Meeting-Tools schon ziemlich gut bedient - wie ich finde. Auch gibt es zahlreiche "Productivity"-Tools, die die verteilte Zusammenarbeit massiv erleichtern (Wikis, Whiteboards, usw.). Aber...

Pin It on Pinterest

Share This