b8afa7e3d0ba80ea1a2b391ae24160a757553880
[aai/search-data-service.git] / search-data-service / 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>
32  * A term query takes an operator, a field to apply the query to and a value to match against the query contents.
33  *
34  * <p>
35  * 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 term query:
41  *
42  * <p>
43  * 
44  * <pre>
45  *     // Single Field Match Query:
46  *     {
47  *         "match": {"field": "searchTags", "value": "abcd"}
48  *     }
49  *
50  *     // Single Field Not-Match query:
51  *     {
52  *         "not-match": {"field": "searchTags", "value": "efgh"}
53  *     }
54  * </pre>
55  *
56  * <p>
57  * 
58  * <pre>
59  *     // Multi Field Match Query With A Single Value:
60  *     {
61  *         "match": {"field": "entityType searchTags", "value": "pserver"}
62  *     }
63  *
64  *     // Multi Field Match Query With Multiple Values:
65  *     {
66  *         "match": {"field": "entityType searchTags", "value": "pserver tenant"}
67  *     }
68  * </pre>
69  */
70 public class TermQuery {
71
72     /**
73      * The name of the field to apply the term query to.
74      */
75     private String field;
76
77     /**
78      * The value which the field must contain in order to have a match.
79      */
80     private Object value;
81
82     /**
83      * For multi field queries only. Determines the rules for whether or not a document matches the query, as follows:
84      *
85      * <p>
86      * "and" - At least one occurrence of every supplied value must be present in any of the supplied fields.
87      *
88      * <p>
89      * "or" - At least one occurrence of any of the supplied values must be present in any of 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 that is understandable by ElasticSearch and is
135      * suitable for inclusion in an ElasticSearch query string.
136      *
137      * @return - ElasticSearch syntax string.
138      */
139     public String toElasticSearch() {
140
141         StringBuilder sb = new StringBuilder();
142
143         sb.append("{");
144
145         // Are we generating a multi field query?
146         if (isMultiFieldQuery()) {
147
148             // For multi field queries, we have to be careful about how we handle
149             // nested fields, so check to see if any of the specified fields are
150             // nested.
151             if (field.contains(".")) {
152
153                 // Build the equivalent of a multi match query across one or more nested fields.
154                 toElasticSearchNestedMultiMatchQuery(sb);
155
156             } else {
157
158                 // Build a real multi match query, since we don't need to worry about nested fields.
159                 toElasticSearchMultiFieldQuery(sb);
160             }
161         } else {
162
163             // Single field query.
164
165             // Add the necessary wrapping if this is a query against a nested field.
166             if (fieldIsNested(field)) {
167                 sb.append("{\"nested\": { \"path\": \"").append(pathForNestedField(field)).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 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) ? operator.toLowerCase() : "and").append("\"");
241
242         if (searchAnalyzer != null) {
243             sb.append(", \"analyzer\": \"").append(searchAnalyzer).append("\"");
244         }
245
246         sb.append("}");
247     }
248
249
250     /**
251      * Constructs the equivalent of an ElasticSearch multi match query across multiple nested fields.
252      *
253      * <p>
254      * Since ElasticSearch doesn't really let you do that, we have to be clever and construct an equivalent query using
255      * boolean operators to produce the same result.
256      *
257      * @param sb - The string builder to use to build the query.
258      */
259     public void toElasticSearchNestedMultiMatchQuery(StringBuilder sb) {
260
261         // Break out our whitespace delimited list of fields and values into a actual lists.
262         List<String> fields = Arrays.asList(field.split(" "));
263         List<String> values = Arrays.asList(((String) value).split(" ")); // GDF: revisit this cast.
264
265         sb.append("\"bool\": {");
266
267         if (operator != null) {
268
269             if (operator.toLowerCase().equals("and")) {
270                 sb.append("\"must\": [");
271             } else if (operator.toLowerCase().equals("or")) {
272                 sb.append("\"should\": [");
273             }
274
275         } else {
276             sb.append("\"must\": [");
277         }
278
279         AtomicBoolean firstField = new AtomicBoolean(true);
280         for (String f : fields) {
281
282             if (!firstField.compareAndSet(true, false)) {
283                 sb.append(", ");
284             }
285
286             sb.append("{ ");
287
288             // Is this a nested field?
289             if (fieldIsNested(f)) {
290
291                 sb.append("\"nested\": {");
292                 sb.append("\"path\": \"").append(pathForNestedField(f)).append("\", ");
293                 sb.append("\"query\": ");
294             }
295
296             sb.append("{\"bool\": {");
297             sb.append("\"should\": [");
298
299             AtomicBoolean firstValue = new AtomicBoolean(true);
300             for (String v : values) {
301                 if (!firstValue.compareAndSet(true, false)) {
302                     sb.append(", ");
303                 }
304                 sb.append("{\"match\": { \"");
305                 sb.append(f).append("\": {\"query\": \"").append(v).append("\"");
306
307                 if (searchAnalyzer != null) {
308                     sb.append(", \"analyzer\": \"").append(searchAnalyzer).append("\"");
309                 }
310                 sb.append("}}}");
311             }
312
313             sb.append("]");
314             sb.append("}");
315
316             if (fieldIsNested(f)) {
317                 sb.append("}");
318                 sb.append("}");
319             }
320
321             sb.append("}");
322         }
323
324         sb.append("]");
325         sb.append("}");
326     }
327
328
329     @Override
330     public String toString() {
331         return "field: " + field + ", value: " + value + " (" + value.getClass().getName() + ")";
332     }
333
334     public boolean fieldIsNested(String field) {
335         return field.contains(".");
336     }
337
338     public String pathForNestedField(String field) {
339         int index = field.lastIndexOf('.');
340         return field.substring(0, index);
341     }
342 }