View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.changes.jira;
20  
21  import java.io.UnsupportedEncodingException;
22  import java.net.URLEncoder;
23  import java.util.Arrays;
24  import java.util.List;
25  import java.util.Locale;
26  
27  import org.apache.commons.lang3.ArraySorter;
28  import org.apache.maven.plugin.logging.Log;
29  
30  /**
31   * Builder for a JIRA query using the JIRA query language. Only a limited set of JQL is supported.
32   *
33   * @author ton.swieb@finalist.com
34   * @version $Id$
35   * @since 2.8
36   */
37  public class JqlQueryBuilder {
38  
39      /**
40       * JQL <a href="https://confluence.atlassian.com/gsgtest/advanced-searching-815566220.html">reserved words</a>.
41       */
42      private static final String[] RESERVED_JQL_WORDS = ArraySorter.sort(new String[] {
43          "abort",
44          "access",
45          "add",
46          "after",
47          "alias",
48          "all",
49          "alter",
50          "and",
51          "any",
52          "as",
53          "asc",
54          "audit",
55          "avg",
56          "before",
57          "begin",
58          "between",
59          "boolean",
60          "break",
61          "by",
62          "byte",
63          "catch",
64          "cf",
65          "char",
66          "character",
67          "check",
68          "checkpoint",
69          "collate",
70          "collation",
71          "column",
72          "commit",
73          "connect",
74          "continue",
75          "count",
76          "create",
77          "current",
78          "date",
79          "decimal",
80          "declare",
81          "decrement",
82          "default",
83          "defaults",
84          "define",
85          "delete",
86          "delimiter",
87          "desc",
88          "difference",
89          "distinct",
90          "divide",
91          "do",
92          "double",
93          "drop",
94          "else",
95          "empty",
96          "encoding",
97          "end",
98          "equals",
99          "escape",
100         "exclusive",
101         "exec",
102         "execute",
103         "exists",
104         "explain",
105         "false",
106         "fetch",
107         "file",
108         "field",
109         "first",
110         "float",
111         "for",
112         "from",
113         "function",
114         "go",
115         "goto",
116         "grant",
117         "greater",
118         "group",
119         "having",
120         "identified",
121         "if",
122         "immediate",
123         "in",
124         "increment",
125         "index",
126         "initial",
127         "inner",
128         "inout",
129         "input",
130         "insert",
131         "int",
132         "integer",
133         "intersect",
134         "intersection",
135         "into",
136         "is",
137         "isempty",
138         "isnull",
139         "join",
140         "last",
141         "left",
142         "less",
143         "like",
144         "limit",
145         "lock",
146         "long",
147         "max",
148         "min",
149         "minus",
150         "mode",
151         "modify",
152         "modulo",
153         "more",
154         "multiply",
155         "next",
156         "noaudit",
157         "not",
158         "notin",
159         "nowait",
160         "null",
161         "number",
162         "object",
163         "of",
164         "on",
165         "option",
166         "or",
167         "order",
168         "outer",
169         "output",
170         "power",
171         "previous",
172         "prior",
173         "privileges",
174         "public",
175         "raise",
176         "raw",
177         "remainder",
178         "rename",
179         "resource",
180         "return",
181         "returns",
182         "revoke",
183         "right",
184         "row",
185         "rowid",
186         "rownum",
187         "rows",
188         "select",
189         "session",
190         "set",
191         "share",
192         "size",
193         "sqrt",
194         "start",
195         "strict",
196         "string",
197         "subtract",
198         "sum",
199         "synonym",
200         "table",
201         "then",
202         "to",
203         "trans",
204         "transaction",
205         "trigger",
206         "true",
207         "uid",
208         "union",
209         "unique",
210         "update",
211         "user",
212         "validate",
213         "values",
214         "view",
215         "when",
216         "whenever",
217         "where",
218         "while",
219         "with"
220     });
221 
222     private String filter = "";
223 
224     private boolean urlEncode = true;
225 
226     /**
227      * Log for debug output.
228      */
229     private Log log;
230 
231     private StringBuilder orderBy = new StringBuilder();
232 
233     private StringBuilder query = new StringBuilder();
234 
235     public JqlQueryBuilder(Log log) {
236         this.log = log;
237     }
238 
239     public String build() {
240         try {
241             String jqlQuery;
242             // If the user has defined a filter, use that
243             if (filter != null && !filter.isEmpty()) {
244                 jqlQuery = filter;
245             } else {
246                 jqlQuery = query.toString() + orderBy.toString();
247             }
248 
249             if (urlEncode) {
250                 getLog().debug("Encoding JQL query " + jqlQuery);
251                 String encodedQuery = URLEncoder.encode(jqlQuery, "UTF-8");
252                 getLog().debug("Encoded JQL query " + encodedQuery);
253                 return encodedQuery;
254             } else {
255                 return jqlQuery;
256             }
257         } catch (UnsupportedEncodingException e) {
258             getLog().error("Unable to encode JQL query with UTF-8", e);
259             throw new RuntimeException(e);
260         }
261     }
262 
263     public JqlQueryBuilder components(String components) {
264         addCommaSeparatedValues("component", components);
265         return this;
266     }
267 
268     public JqlQueryBuilder components(List<String> components) {
269         addValues("component", components);
270         return this;
271     }
272 
273     public JqlQueryBuilder filter(String filter) {
274         this.filter = filter;
275         return this;
276     }
277 
278     /**
279      * When both {@code #fixVersion(String)} and {@link #fixVersionIds(String)} are used, then you will probably end up
280      * with a JQL query that is valid, but returns nothing. Unless they both only reference the same fixVersion
281      *
282      * @param fixVersion a single fix version
283      * @return the builder.
284      */
285     public JqlQueryBuilder fixVersion(String fixVersion) {
286         addSingleValue("fixVersion", fixVersion);
287         return this;
288     }
289 
290     /**
291      * When both {@link #fixVersion(String)} and {@link #fixVersionIds(String)} are used then you will probably end up
292      * with a JQL query that is valid, but returns nothing. Unless they both only reference the same fixVersion
293      *
294      * @param fixVersionIds a comma-separated list of version ids.
295      * @return the builder.
296      */
297     public JqlQueryBuilder fixVersionIds(String fixVersionIds) {
298         addCommaSeparatedValues("fixVersion", fixVersionIds);
299         return this;
300     }
301 
302     /**
303      * Add a sequence of version IDs already in a list.
304      *
305      * @param fixVersionIds the version ids.
306      * @return the builder.
307      */
308     public JqlQueryBuilder fixVersionIds(List<String> fixVersionIds) {
309         addValues("fixVersion", fixVersionIds);
310         return this;
311     }
312 
313     public Log getLog() {
314         return log;
315     }
316 
317     public JqlQueryBuilder priorityIds(String priorityIds) {
318         addCommaSeparatedValues("priority", priorityIds);
319         return this;
320     }
321 
322     public JqlQueryBuilder priorityIds(List<String> priorityIds) {
323         addValues("priority", priorityIds);
324         return this;
325     }
326 
327     public JqlQueryBuilder project(String project) {
328         addSingleValue("project", project);
329         return this;
330     }
331 
332     public JqlQueryBuilder resolutionIds(String resolutionIds) {
333         addCommaSeparatedValues("resolution", resolutionIds);
334         return this;
335     }
336 
337     public JqlQueryBuilder resolutionIds(List<String> resolutionIds) {
338         addValues("resolution", resolutionIds);
339         return this;
340     }
341 
342     public JqlQueryBuilder sortColumnNames(String sortColumnNames) {
343         if (sortColumnNames != null) {
344             orderBy.append(" ORDER BY ");
345 
346             String[] sortColumnNamesArray = sortColumnNames.split(",");
347 
348             for (int i = 0; i < sortColumnNamesArray.length - 1; i++) {
349                 addSingleSortColumn(sortColumnNamesArray[i]);
350                 orderBy.append(", ");
351             }
352             addSingleSortColumn(sortColumnNamesArray[sortColumnNamesArray.length - 1]);
353         }
354         return this;
355     }
356 
357     public JqlQueryBuilder statusIds(String statusIds) {
358         addCommaSeparatedValues("status", statusIds);
359         return this;
360     }
361 
362     public JqlQueryBuilder statusIds(List<String> statusIds) {
363         addValues("status", statusIds);
364         return this;
365     }
366 
367     public JqlQueryBuilder typeIds(String typeIds) {
368         addCommaSeparatedValues("type", typeIds);
369         return this;
370     }
371 
372     public JqlQueryBuilder typeIds(List<String> typeIds) {
373         addValues("type", typeIds);
374         return this;
375     }
376 
377     public JqlQueryBuilder urlEncode(boolean doEncoding) {
378         urlEncode = doEncoding;
379         return this;
380     }
381 
382     public boolean urlEncode() {
383         return urlEncode;
384     }
385 
386     /* --------------------------------------------------------------------- */
387     /* Private methods */
388     /* --------------------------------------------------------------------- */
389 
390     private void addCommaSeparatedValues(String key, String values) {
391         if (values != null) {
392             if (query.length() > 0) {
393                 query.append(" AND ");
394             }
395 
396             query.append(key).append(" in (");
397 
398             String[] valuesArr = values.split(",");
399 
400             for (int i = 0; i < (valuesArr.length - 1); i++) {
401                 trimAndQuoteValue(valuesArr[i]);
402                 query.append(", ");
403             }
404             trimAndQuoteValue(valuesArr[valuesArr.length - 1]);
405             query.append(")");
406         }
407     }
408 
409     private void addValues(String key, List<String> values) {
410         if (values != null && !values.isEmpty()) {
411             if (query.length() > 0) {
412                 query.append(" AND ");
413             }
414 
415             query.append(key).append(" in (");
416 
417             for (int i = 0; i < (values.size() - 1); i++) {
418                 trimAndQuoteValue(values.get(i));
419                 query.append(", ");
420             }
421             trimAndQuoteValue(values.get(values.size() - 1));
422             query.append(")");
423         }
424     }
425 
426     private void addSingleSortColumn(String name) {
427         boolean descending = false;
428         name = name.trim().toLowerCase(Locale.ENGLISH);
429         if (name.endsWith("desc")) {
430             descending = true;
431             name = name.substring(0, name.length() - 4).trim();
432         } else if (name.endsWith("asc")) {
433             descending = false;
434             name = name.substring(0, name.length() - 3).trim();
435         }
436         // Strip any spaces from the column name, or it will trip up JIRA's JQL parser
437         name = name.replaceAll(" ", "");
438         orderBy.append(name);
439         orderBy.append(descending ? " DESC" : " ASC");
440     }
441 
442     private void addSingleValue(String key, String value) {
443         if (value != null) {
444             if (query.length() > 0) {
445                 query.append(" AND ");
446             }
447             query.append(key).append(" = ");
448             trimAndQuoteValue(value);
449         }
450     }
451 
452     private void trimAndQuoteValue(String value) {
453         String trimmedValue = value.trim();
454         if (trimmedValue.contains(" ") || trimmedValue.contains(".") || isReservedJqlWord(trimmedValue)) {
455             query.append("\"").append(trimmedValue).append("\"");
456         } else {
457             query.append(trimmedValue);
458         }
459     }
460 
461     /**
462      * JQL <a href="https://confluence.atlassian.com/gsgtest/advanced-searching-815566220.html">reserved words</a>.
463      *
464      * @param value a string
465      * @return whether the given string is a JQL reserved word.
466      */
467     private boolean isReservedJqlWord(String value) {
468         return Arrays.binarySearch(RESERVED_JQL_WORDS, value.toLowerCase(Locale.ROOT)) > 0;
469     }
470 }