com.almworks.jira.structure.api.query.StructureQueryBuilder Maven / Gradle / Ivy
Show all versions of structure-api Show documentation
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:
*
* - Query q1 matches all unresolved issues in a forest.
* - Queries q2 and q3 both match all top-level rows.
* - Query q4 matches parents of issue TS-129 and parents of issue TS-239.
* - Query q5 matches sub-rows of issue TS-129 and sub-rows of issue TS-239.
* - Queries q6, q7 both match issues with fix version 5.2.11 or 6.0 and their subtrees.
* - Query q8 matches siblings of TS-129's parents.
* - Query q9 matches rows on the 3rd level of the hierarchy.
* - 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.
* - Query q11 matches all such top-level rows and all such Epics that have unresolved sub-issues at
* any level.
* - Query q12 matches all rows (both issues and non-issues) under Epics that are not Tasks.
* - Query q13 matches the subtree of folder named "Future tasks".
*
* */
@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}:
*
* - {@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.
*
*
- {@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.
*
*
- {@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();
}
}