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