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