Remove backport dependency
[aai/search-data-service.git] / src / main / java / org / onap / aai / sa / searchdbabstraction / searchapi / TermQuery.java
1 /**
2  * ============LICENSE_START=======================================================
3  * org.onap.aai
4  * ================================================================================
5  * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved.
6  * Copyright © 2017-2018 Amdocs
7  * ================================================================================
8  * Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  *       http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  * ============LICENSE_END=========================================================
20  */
21 package org.onap.aai.sa.searchdbabstraction.searchapi;
22
23 import com.fasterxml.jackson.annotation.JsonProperty;
24 import java.util.Arrays;
25
26 import java.util.List;
27 import java.util.concurrent.atomic.AtomicBoolean;
28
29 /**
30  * This class represents a simple term query.
31  *
32  * <p>A term query takes an operator, a field to apply the query to and a value to match
33  * against the query contents.
34  *
35  * <p>Valid operators include:
36  * <ul>
37  * <li> match - Field must contain the supplied value to produce a match. </li>
38  * <li> not-match - Field must NOT contain the supplied value to produce a match. </li>
39  * </ul>
40  * The following examples illustrate the structure of a few variants of the
41  * term query:
42  *
43  * <p><pre>
44  *     // Single Field Match Query:
45  *     {
46  *         "match": {"field": "searchTags", "value": "abcd"}
47  *     }
48  *
49  *     // Single Field Not-Match query:
50  *     {
51  *         "not-match": {"field": "searchTags", "value": "efgh"}
52  *     }
53  * </pre>
54  *
55  * <p><pre>
56  *     // Multi Field Match Query With A Single Value:
57  *     {
58  *         "match": {"field": "entityType searchTags", "value": "pserver"}
59  *     }
60  *
61  *     // Multi Field Match Query With Multiple Values:
62  *     {
63  *         "match": {"field": "entityType searchTags", "value": "pserver tenant"}
64  *     }
65  * </pre>
66  */
67 public class TermQuery {
68
69   /**
70    * The name of the field to apply the term query to.
71    */
72   private String field;
73
74   /**
75    * The value which the field must contain in order to have a match.
76    */
77   private Object value;
78
79   /**
80    * For multi field queries only.  Determines the rules for whether or not a document matches
81    * the query, as follows:
82    *
83    * <p>"and" - At least one occurrence of every supplied value must be present in any of the
84    * supplied fields.
85    *
86    * <p>"or"  - At least one occurrence of any of the supplied values must be present in any of
87    * the supplied fields.
88    */
89   private String operator;
90
91   @JsonProperty("analyzer")
92   private String searchAnalyzer;
93
94
95   public String getField() {
96     return field;
97   }
98
99   public void setField(String field) {
100     this.field = field;
101   }
102
103   public Object getValue() {
104     return value;
105   }
106
107   public void setValue(Object value) {
108     this.value = value;
109   }
110
111   private boolean isNumericValue() {
112     return ((value instanceof Integer) || (value instanceof Double));
113   }
114
115   public String getOperator() {
116     return operator;
117   }
118
119   public void setOperator(String operator) {
120     this.operator = operator;
121   }
122
123   public String getSearchAnalyzer() {
124     return searchAnalyzer;
125   }
126
127   public void setSearchAnalyzer(String searchAnalyzer) {
128     this.searchAnalyzer = searchAnalyzer;
129   }
130
131   /**
132    * This method returns a string which represents this query in syntax
133    * that is understandable by ElasticSearch and is suitable for inclusion
134    * in an ElasticSearch query string.
135    *
136    * @return - ElasticSearch syntax string.
137    */
138   public String toElasticSearch() {
139
140     StringBuilder sb = new StringBuilder();
141
142     sb.append("{");
143
144     // Are we generating a multi field query?
145     if (isMultiFieldQuery()) {
146
147       // For multi field queries, we have to be careful about how we handle
148       // nested fields, so check to see if any of the specified fields are
149       // nested.
150       if (field.contains(".")) {
151
152         // Build the equivalent of a multi match query across one or more nested fields.
153         toElasticSearchNestedMultiMatchQuery(sb);
154
155       } else {
156
157         // Build a real multi match query, since we don't need to worry about nested fields.
158         toElasticSearchMultiFieldQuery(sb);
159       }
160     } else {
161
162       // Single field query.
163
164       // Add the necessary wrapping if this is a query against a nested field.
165       if (fieldIsNested(field)) {
166         sb.append("{\"nested\": { \"path\": \"").append(pathForNestedField(field))
167             .append("\", \"query\": ");
168       }
169
170       // Build the query.
171       toElasticSearchSingleFieldQuery(sb);
172
173       if (fieldIsNested(field)) {
174         sb.append("}}");
175       }
176     }
177
178     sb.append("}");
179
180     return sb.toString();
181   }
182
183
184   /**
185    * Determines whether or not the client has specified a term query with
186    * multiple fields.
187    *
188    * @return - true if the query is referencing multiple fields, false, otherwise.
189    */
190   private boolean isMultiFieldQuery() {
191
192     return (field.split(" ").length > 1);
193   }
194
195
196   /**
197    * Constructs a single field term query in ElasticSearch syntax.
198    *
199    * @param sb - The string builder to assemble the query string with.
200    * @return - The single term query.
201    */
202   private void toElasticSearchSingleFieldQuery(StringBuilder sb) {
203
204     sb.append("\"term\": {\"").append(field).append("\" : ");
205
206     // For numeric values, don't enclose the value in quotes.
207     if (!isNumericValue()) {
208       sb.append("\"").append(value).append("\"");
209     } else {
210       sb.append(value);
211     }
212
213     sb.append("}");
214   }
215
216
217   /**
218    * Constructs a multi field query in ElasticSearch syntax.
219    *
220    * @param sb - The string builder to assemble the query string with.
221    * @return - The multi field query.
222    */
223   private void toElasticSearchMultiFieldQuery(StringBuilder sb) {
224
225     sb.append("\"multi_match\": {");
226
227     sb.append("\"query\": \"").append(value).append("\", ");
228     sb.append("\"type\": \"cross_fields\",");
229     sb.append("\"fields\": [");
230
231     List<String> fields = Arrays.asList(field.split(" "));
232     AtomicBoolean firstField = new AtomicBoolean(true);
233     for (String f : fields) {
234       if (!firstField.compareAndSet(true, false)) {
235         sb.append(", ");
236       }
237       sb.append("\"").append(f.trim()).append("\"");
238     }
239     sb.append("],");
240
241     sb.append("\"operator\": \"").append((operator != null)
242         ? operator.toLowerCase() : "and").append("\"");
243
244     if (searchAnalyzer != null) {
245       sb.append(", \"analyzer\": \"").append(searchAnalyzer).append("\"");
246     }
247
248     sb.append("}");
249   }
250
251
252   /**
253    * Constructs the equivalent of an ElasticSearch multi match query across
254    * multiple nested fields.
255    *
256    * <p>Since ElasticSearch doesn't really let you do that, we have to be clever
257    * and construct an equivalent query using boolean operators to produce
258    * the same result.
259    *
260    * @param sb - The string builder to use to build the query.
261    */
262   public void toElasticSearchNestedMultiMatchQuery(StringBuilder sb) {
263
264     // Break out our whitespace delimited list of fields and values into a actual lists.
265     List<String> fields = Arrays.asList(field.split(" "));
266     List<String> values = Arrays.asList(((String) value).split(" ")); // GDF: revisit this cast.
267
268     sb.append("\"bool\": {");
269
270     if (operator != null) {
271
272       if (operator.toLowerCase().equals("and")) {
273         sb.append("\"must\": [");
274       } else if (operator.toLowerCase().equals("or")) {
275         sb.append("\"should\": [");
276       }
277
278     } else {
279       sb.append("\"must\": [");
280     }
281
282     AtomicBoolean firstField = new AtomicBoolean(true);
283     for (String f : fields) {
284
285       if (!firstField.compareAndSet(true, false)) {
286         sb.append(", ");
287       }
288
289       sb.append("{ ");
290
291       // Is this a nested field?
292       if (fieldIsNested(f)) {
293
294         sb.append("\"nested\": {");
295         sb.append("\"path\": \"").append(pathForNestedField(f)).append("\", ");
296         sb.append("\"query\": ");
297       }
298
299       sb.append("{\"bool\": {");
300       sb.append("\"should\": [");
301
302       AtomicBoolean firstValue = new AtomicBoolean(true);
303       for (String v : values) {
304         if (!firstValue.compareAndSet(true, false)) {
305           sb.append(", ");
306         }
307         sb.append("{\"match\": { \"");
308         sb.append(f).append("\": {\"query\": \"").append(v).append("\"");
309
310         if (searchAnalyzer != null) {
311           sb.append(", \"analyzer\": \"").append(searchAnalyzer).append("\"");
312         }
313         sb.append("}}}");
314       }
315
316       sb.append("]");
317       sb.append("}");
318
319       if (fieldIsNested(f)) {
320         sb.append("}");
321         sb.append("}");
322       }
323
324       sb.append("}");
325     }
326
327     sb.append("]");
328     sb.append("}");
329   }
330
331
332   @Override
333   public String toString() {
334     return "field: " + field + ", value: " + value + " (" + value.getClass().getName() + ")";
335   }
336
337   public boolean fieldIsNested(String field) {
338     return field.contains(".");
339   }
340
341   public String pathForNestedField(String field) {
342     int index = field.lastIndexOf('.');
343     return field.substring(0, index);
344   }
345 }