All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.almworks.jira.structure.api.query.StructureQueryBuilder Maven / Gradle / Ivy

There is a newer version: 17.25.3
Show newest version
package com.almworks.jira.structure.api.query;

import com.almworks.integers.LongList;
import com.almworks.jira.structure.api.util.JiraComponents;
import com.atlassian.annotations.PublicApi;
import com.atlassian.query.Query;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * 

{@code StructureQueryBuilder} allows you to build a {@link StructureQuery structure query} with a * fluent interface. You begin the building process with {@link #begin()} and end it with {@link Head#end()}; * if the Java compiler accepts the resulting expression, it is also a syntactically valid StructuredJQL expression.

* *

A structure query consists of constraints that select rows in a forest, connected with OR and AND; * each constraint can be negated. A constraint can be either a {@link BasicConstraintStep basic} or a * {@link StartStep relational constraint}.

* *

Working example (assumes that {@code begin()} is statically imported):

* *
     Query typeEpic = JqlQueryBuilder.newBuilder().where().issueType("Epic").buildQuery();
     Query typeTask = JqlQueryBuilder.newBuilder().where().issueType("Task").buildQuery();
     Query versionQuery = JqlQueryBuilder.newBuilder().where().fixVersion("5.2.11", "6.0").buildQuery();
     Query unresolved = JqlQueryBuilder.newBuilder().where().unresolved().buildQuery();
 
     StructureQuery q1 = begin().jql(unresolved).end();
     StructureQuery q2 = begin().root().end();
     StructureQuery q3 = begin().parent.is.empty().end();
     StructureQuery q4 = begin().child.in.issueKeys("TS-129", "TS-239").end();
     StructureQuery q5 = begin().child.of.issueKeys("TS-129", "TS-239").end();
     StructureQuery q6 = begin().ancestor.or().issue.in.jql(versionQuery).end();
     StructureQuery q7 = begin().issue.or().descendant.of.jql(versionQuery).end();
     StructureQuery q8 = begin().self.or().sibling.of.sub().parent.of.issueKey("TS-129").endsub().end();
     StructureQuery q9 = begin().child.of.sub().child.of.root().endsub().end();
     StructureQuery q10 = begin().prevSibling.is.sub().empty().or.jql(unresolved).endsub().end();
     StructureQuery q11 = begin().sub().root().or.jql(typeEpic).endsub().and.descendant.in.jql(unresolved).end();
     StructureQuery q12 = begin().parent.in.jql(typeEpic).and.issue.notIn.jql(typeTask).end();
     StructureQuery q13 = begin().self.or().descendant.of.constraint("folder", "Future tasks").end(); 
 * 
*
*

Explanation:

*
    *
  1. Query q1 matches all unresolved issues in a forest.
  2. *
  3. Queries q2 and q3 both match all top-level rows.
  4. *
  5. Query q4 matches parents of issue TS-129 and parents of issue TS-239.
  6. *
  7. Query q5 matches sub-rows of issue TS-129 and sub-rows of issue TS-239.
  8. *
  9. Queries q6, q7 both match issues with fix version 5.2.11 or 6.0 and their subtrees.
  10. *
  11. Query q8 matches siblings of TS-129's parents.
  12. *
  13. Query q9 matches rows on the 3rd level of the hierarchy.
  14. *
  15. Query q10 matches all rows such that either all the issues under the same parent that come before them * are unresolved, or there are no issues under the same parent that come before them.
  16. *
  17. Query q11 matches all such top-level rows and all such Epics that have unresolved sub-issues at * any level.
  18. *
  19. Query q12 matches all rows (both issues and non-issues) under Epics that are not Tasks.
  20. *
  21. Query q13 matches the subtree of folder named "Future tasks".
  22. *
* */ @PublicApi public class StructureQueryBuilder> { /** * This is the starting point for building a Structure query. * @return an intermediate object that lets you specify {@link BasicConstraintStep basic} and * {@link StartStep relation} constraints * */ @NotNull public static StartStep begin() { StructureQueryBuilderFactory factory = JiraComponents.getOSGiComponentInstanceOfType(StructureQueryBuilderFactory.class); if (factory == null) throw new IllegalStateException("No StructureQueryBuilderFactory"); return factory.builder(); } /** *

This class allows you to either build a {@link BasicConstraintStep basic constraint} or * start building a relational constraint.

* *

A basic constraint matches rows directly, whereas a relational constraint matches rows related to * rows that satisfy a condition. * (Related corresponds to a relation between rows induced by their positions in a forest.) * E.g., {@code issueKey("TS-129")} is a basic constraint matching all instances of issue TS-129 in the forest; * {@code child.in.issueKey("TS-129")} is a relational constraint matching all rows that have TS-129 among their children. * (See also queries q3, q4 and q5 in the {@link StructureQueryBuilder class documentation}.) * *

Relational constraint has the form {@code relation operator basicConstraint}; you begin with * selecting {@code relation} by referencing a field of this class inherited from {@link RelationConstraintStartStep}. * After that, you continue with the {@link OpStep resulting object} to add {@code operator} or combine with another relation * using {@link OpStep#or() or()}. In the latter case, a row is matched if it is related to rows that * satisfy a condition by at least one of the used relations; for examples, see queries q6, q7 and q8 in the * {@link StructureQueryBuilder class documentation}. * *

Note that this scheme is similar to JQL's {@code field operator value}, which matches issues * having {@code field} that is {@code operator} (e.g., equal, not equal) to {@code value}. * For example, JQL query {@code type in (Epic, Story)} matches issues having type * that is in values Epic, Story. Compare it to {@code parent.in.jql(typeEpic)}, which * matches rows having parent that is in basic JQL constraint type = Epic.

* */ public static abstract class StartStep> extends RelationConstraintStartStep implements BasicConstraintStep { /** * Negates the whole constraint, whether it is basic or relation-based. * */ public StartStep not() { return myHelper.not(); } protected StartStep(StartStepHelper helper) { super(helper); } } /** * This class allows you to continue building {@link StartStep relational constraint} by adding another * {@code relation}. * */ public static abstract class RelationConstraintStartStep> { /** Row is a child (sub-row) of another row in a forest. */ public final OpStep child; /** Row is a parent of another row in a forest. */ public final OpStep parent; /** Row is a descendant (sub- or sub-sub-...-row) of another row in a forest. */ public final OpStep descendant; /** Row is an ancestor (parent, parent-of-parent, or parent-of-parent-...-of-parent) of another row in a forest. */ public final OpStep ancestor; /** *

Row is a previous (preceding) sibling of another row in a forest. *

Row A is a preceding sibling of row B in a forest if: *

    *
  • they share the same parent, and
  • *
  • A is higher than B (A comes before B).
  • *
* */ public final OpStep prevSibling; /** *

Row is a next (following) sibling of another row in a forest. *

Row A is a following sibling of row B in a forest if: *

    *
  • they share the same parent, and
  • *
  • A is lower than B (A comes after B).
  • *
* */ public final OpStep nextSibling; /** * Row is a sibling of another row in a forest. Row A is a sibling of row B if they share the same parent. *

This is equivalent to {@code prevSibling.or().nextSibling}. * */ public final OpStep sibling; /** *

This is a relation of a row to itself. It makes a relational constraint work like its * {@code basicConstraint}, so, for example, {@code self.in.jql(someJql)} matches the same rows * as {@code jql(someJql)}. * *

It is useful when you combine it via {@link OpStep#or() or()} with another relation, so that you * add rows matched by {@code basicConstraint} to the result set. * *

For example, consider query q6 from the examples in the * {@link StructureQueryBuilder class documentation}. * Constraint {@code ancestor.in.jql(versionQuery)} returns all sub-(sub-...)-rows of issues * that match match JQL {@code versionQuery}, but not the issues themselves. * To match them, use {@code ancestor.or().self.in.jql(versionQuery)}. * *

Note that this relation works for all types of rows - issues, projects, users, etc. * To match only issues, use {@link #issue}. * *

For an illustration of difference between {@code self} and {@code issue}, * consider query q8 from the examples in the {@link StructureQueryBuilder class documentation}. * Relational constraint {@code parent.of.issueKey("TS-129")} yields parent rows of issue TS-129, * which may be issues, users, and all other kinds of items. To match these parents * and all their siblings, we use *

self.or().sibling.of.sub().<relational constraint>.
* But if we were to use *
issue.or().sibling.of.sub().<relational constraint>,
* we would still match all siblings, but parent rows will be matched only if they are issues. * */ public final OpStep self; /** *

This is a relation of an issue to itself - same as {@link #self}, but it matches only issues. * @see #self * */ public final OpStep issue; protected final StartStepHelper myHelper; protected RelationConstraintStartStep(StartStepHelper helper) { myHelper = helper; self = helper.self; issue = helper.issue; child = helper.child; parent = helper.parent; descendant = helper.descendant; ancestor = helper.ancestor; prevSibling = helper.prevSibling; nextSibling = helper.nextSibling; sibling = helper.sibling; } } /** *

This class lets you add {@code operator} to the {@link StartStep relational constraint} being built, * or to combine the already added relation with another one via {@link #or()}. * *

{@code operator} specifies how {@code basicConstraint} is applied to {@code relation}:

*
    *
  1. {@link #in}, {@link #is}, and {@link #equals} specify that a row is matched * if its relatives match the basic constraint. * *

    For example, consider {@code child.in.issueKeys("TS-129", "TS-239")}. * The relation is {@link RelationConstraintStartStep#child child}, so the relatives in question are sub-rows. * So, a row matches this query if at least one sub-row is {@code TS-129} or {@code TS-239}. * *

    There is no difference between these three operators; different forms exist for the purpose of a * more natural way to express different species of constraints. * *

  2. {@link #notIn}, {@link #isNot}, and {@link #notEquals} are negated versions of {@code in, is, equals}. * They specify that a row is matched if none of its relatives match the basic constraint. * Importantly, rows with no relatives are matched. * *

    For example, consider {@code child.notIn.issueKeys("TS-129", "TS-239")}. A row is matched if * no sub-row of it is {@code TS-129} or {@code TS-239}; thus, this constraint matches all rows * that either have no sub-rows or do not have these two issues among their sub-rows. * *

    Using a relational constraint with one of these operators is equivalent to using * a negation of relational constraint with the corresponding non-negated operator. E.g., the constraint * above is equivalent to {@code not().child.in.issueKeys("TS-129", "TS-239")}. * *

    But, using these operators is very not the same as negating * {@code basicConstraint}: first, having relatives other than X is not the same as not having * relatives X, second, rows with no children are not matched. * E.g., {@code child.in.not().issueKeys("TS-129", "TS-239")} matches all rows with sub-rows, * such that at least one of their sub-rows is not {@code TS-129} nor {@code TS-239}. * In other words, it matches all rows with sub-rows except those having only {@code TS-129} * or {@code TS-239} as sub-rows. * *

  3. {@link #of} matches relatives of rows matching the basic constraint. * Thus, the relational constraint behaves as if we first find all rows that satisfy {@code basicConstraint}, * then select their relatives. * *

    For example, consider {@code child.of.issueKeys("TS-129", "TS-239")}: a row matches if it * is a child of either {@code TS-129} or {@code TS-239}. *

* *

To illustrate the difference between {@code of} and {@code in} ({@code is, equals}), * let's compare queries q4 and q5 from the {@link StructureQueryBuilder class documentation} * using this forest: *

   *   project TS
   *     version 1.2     q4
   *       TS-129      *
   *         TS-48          q5
   *       TS-239      *
   *         TS-49          q5
   *       TS-50
   *     version 1.3     q4
   *       TS-239      *
   *         TS-49          q5
   *
   *   q4: child.in.issueKeys("TS-129", "TS-239")
   *   q5: child.of.issueKeys("TS-129", "TS-239")
   * 
* Asterisks mark rows matching the basic constraint, and q4/q5 mark rows matching the * corresponding queries. * *

One may note that for any relation, there is a corresponding "inverse" relation: for example, * {@link StartStep#child child}-{@link StartStep#parent parent}. A relational constraint using operator * {@code in} ({@code is, equals}) is equivalent to a relational constraint using an inverse relation * with operator {@code of}. That is,
* {@code child.in.issueKeys("TS-129", "TS-239")}
* is the same as
* {@code parent.of.issueKeys("TS-129", "TS-239")}.
* Compare also examples q6 and q7 from the {@link StructureQueryBuilder class documentation}. * */ public static class OpStep> { public final BasicConstraintStep in; public final BasicConstraintStep notIn; public final BasicConstraintStep of; public final BasicConstraintStep equals; public final BasicConstraintStep notEquals; public final BasicConstraintStep is; public final BasicConstraintStep isNot; /** * Use this method to combine several relations into one for use in a {@link StartStep relational constraint}. * */ public RelationConstraintStartStep or() { return myHelper.or(); } private final RelationStepHelper myHelper; public OpStep(RelationStepHelper helper) { myHelper = helper; in = equals = is = helper.invComp; isNot = notEquals = notIn = helper.invCompNeg; of = helper.comp; } } /** *

This class allows to specify a basic constraint, either on its own, or as the last step of building a * {@link StartStep relational constraint}. * *

A basic constraint is a simply a constraint on rows; it does not involve its relatives, as relation * constraint does. For examples, see queries q1 and q2 in the {@link StructureQueryBuilder class documentation}. * *

Note that a relational constraint, or even a Boolean combination thereof, can behave as a * basic constraint if taken into {@link #sub() parentheses}; this is useful if you are building a relation * constraint, and you need to have a complex constraint in place of {@code basicConstraint}. Examples * of it are queries q9 and q10 in the {@link StructureQueryBuilder class documentation}. * */ public interface BasicConstraintStep> { /** * Matches issues that satisfy the specified JQL query. It is also referred to as "nested JQL constraint." * @param query a JQL query; if {@code null}, matches all issues in the forest. * */ B jql(@Nullable Query query); /** @see #issues */ B issueKey(@NotNull String issueKey); /** @see #issues */ B issueKeys(@NotNull Iterable issueKeys); /** @see #issues */ B issueKeys(@NotNull String... issueKeys); /** @see #issues */ B issueId(long issueId); /** @see #issues */ B issueIds(@NotNull LongList issueIds); /** @see #issues */ B issueIds(@NotNull long... issueIds); /** * Matches the specified issues. If any of the specified issues is present several times in the forest, * all entries are matched. *

If both parameters are {@code null} or both are empty, matches no issues. * */ B issues(@Nullable Iterable issueKeys, @Nullable LongList issueIds); /** * Matches rows at the bottom level of the hierarchy. * Put otherwise, matches rows that do not have sub-rows. * */ B leaf(); /** * Matches rows at the top level of the hierarchy. * Put otherwise, matches rows that do not have a parent. * */ B root(); /** *

Matches no rows. * *

This basic condition is useful for {@link StartStep relational constraints}: * {@code relation.operator.empty()} matches all rows that do not have corresponding relatives. * For example: *

    *
  • {@code child.is.empty()} matches all rows that have no sub-rows * (equivalent of {@link #leaf()}); *
  • {@code child.isNot.empty()} matches all rows that have at least one sub-row * (equivalent of {@code not().leaf()}); *
  • {@code child.of.empty()} matches all rows that are not sub-rows of any row * (equivalent of {@link #root()}). * */ B empty(); /** * Matches all rows. Equivalent of {@code not().empty()}. * */ B all(); /** *

    Matches rows using a custom constraint specified by its name, supplying it with the specified arguments.

    * *

    Structure plugin comes bundled with a few constraints, see the list in S-JQL documentation. * Constraints can also be added by other plugins by the means of implementing {@code StructureQueryConstraint}.

    * *

    See also query q13 in the examples section in the class documentation.

    * @param name constraint name - should be either a name of the bundled constraint or, * in case of a custom constraint provided via a plugin, correspond to {@code fname} attribute * of {@code } module in {@code atlassian-plugin.xml} * @param arguments constraint arguments. Both the list and all of its elements must not be {@code null} , * otherwise {@code NullPointerException} is thrown * @throws NullPointerException if name, arguments, or any of the arguments list elements is {@code null} * @see StructureQueryConstraint */ B constraint(@NotNull String name, @NotNull String... arguments); /** * @see #constraint(String, String...) * */ B constraint(@NotNull String name, @NotNull Iterable arguments); /** *

    This method starts a new constraint, remembering the currently built constraint. When you finish * building the new constraint, call {@link Sub#endsub()}, and the new constraint will be attached to * this builder as if it were a basic constraint. * This is a programmatic equivalent of taking an expression into parentheses: this method "opens" * a new pair of parentheses. * *

    There are several cases when you would want to use this method: *

      *
    • overriding default precedence of Boolean operators AND and OR (for an example, see query q11 in the * {@link StructureQueryBuilder class documentation}); *
    • using a complex constraint (a relational constraint, or a Boolean combination thereof) in place of * {@code basicConstraint} in a {@link StartStep relational constraint}. For examples, see queries * q9 and q10 in the {@link StructureQueryBuilder class documentation}. *
    * */ StartStep> sub(); /** * Matches all rows that match the specified Structure query. Note that only those instances of * {@code StructureQuery} that have originated from this API will work; any other implementation * of {@code StructureQuery} will not be recognized, and this constraint will be equivalent to * {@link #all()}. * * @param query Structure query obtained either from {@code StructureQueryBuilder} or * {@link StructureQueryParser}. * */ B query(@NotNull StructureQuery query); } /** *

    Starts a new constraint, connected to the previous one with AND.

    * *

    Note that AND has higher precedence than OR, so that {@code X OR Y AND Z} will mean * {@code X OR (Y AND Z)}. * In order to get {@code (X OR Y) AND Z}, you'll need to use a sub-query ("parentheses.") * To open parentheses, use {@link BasicConstraintStep#sub() sub()}; to close, use {@link Sub#endsub() endsub()}. * You will thus get {@code sub().X.or.Y.endsub().and.Z}. * For an example, see query q11 in the {@link StructureQueryBuilder class documentation}.

    * */ public final StartStep and; /** *

    Starts a new constraint, connected to the previous one with OR.

    * *

    Note that OR has lower precedence than AND, so that {@code X AND Y OR Z} will mean * {@code (X AND Y) OR Z}. * In order to get {@code X AND (Y OR Z)}, you'll need to use a sub-query ("parentheses.") * To open parentheses, use {@link BasicConstraintStep#sub() sub()}; to close, use {@link Sub#endsub() endsub()}. * You will then get {@code X.and.sub().Y.or.Z.endsub()}. * For an example, see query q11 in the {@link StructureQueryBuilder class documentation}.

    * */ public final StartStep or; /** * Object of this class contains the state of the builder; you can finish building the query by calling {@link #end()}, * or add more constraints, connecting them with {@link #and} or {@link #or}. * */ public abstract static class Head extends StructureQueryBuilder { /** * Builds the query and returns it. * @return the built structure query * */ public abstract StructureQuery end(); protected Head(StartStep and, StartStep or) { super(and, or); } } /** * Object of this class contains the state of the builder inside the currently open parentheses; you can finish * building the query in the parentheses and return to the main builder by calling {@link #endsub()}, or * add more constraints inside the parentheses, connecting them with {@link #and} or {@link #or}. * */ public abstract static class Sub> extends StructureQueryBuilder> { /** * "Closes" the parentheses opened by the matching call to {@link BasicConstraintStep#sub()}, * so that the accumulated constraint will be inserted into the enclosing builder as if a * {@link BasicConstraintStep basic constraint}. * */ public abstract B endsub(); protected Sub(StartStep> and, StartStep> or) { super(and, or); } } // --- Implementation --- protected StructureQueryBuilder(StartStep and, StartStep or) { this.and = and; this.or = or; } protected static abstract class StartStepHelper> { protected OpStep self; protected OpStep issue; protected OpStep child; protected OpStep parent; protected OpStep descendant; protected OpStep ancestor; protected OpStep prevSibling; protected OpStep nextSibling; protected OpStep sibling; public StartStepHelper() {} protected abstract StartStep not(); } protected static abstract class RelationStepHelper> { protected BasicConstraintStep invComp; protected BasicConstraintStep invCompNeg; protected BasicConstraintStep comp; protected abstract StartStep or(); } }