Removed problems triggering Sonar errors
[aai/aai-common.git] / aai-core / src / main / java / org / onap / aai / serialization / queryformats / MultiFormatMapper.java
1 /**
2  * ============LICENSE_START=======================================================
3  * org.onap.aai
4  * ================================================================================
5  * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *    http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END=========================================================
19  */
20
21 package org.onap.aai.serialization.queryformats;
22
23 import com.google.gson.JsonArray;
24 import com.google.gson.JsonElement;
25 import com.google.gson.JsonObject;
26 import java.util.stream.Collectors;
27 import org.apache.commons.lang3.tuple.ImmutableTriple;
28 import org.apache.commons.lang3.tuple.Pair;
29 import org.apache.tinkerpop.gremlin.process.traversal.Path;
30 import org.apache.tinkerpop.gremlin.process.traversal.step.util.Tree;
31 import org.apache.tinkerpop.gremlin.structure.Vertex;
32 import org.onap.aai.serialization.queryformats.exceptions.AAIFormatQueryResultFormatNotSupported;
33 import org.onap.aai.serialization.queryformats.exceptions.AAIFormatVertexException;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
36
37 import java.util.*;
38
39 public abstract class MultiFormatMapper implements FormatMapper {
40
41     Logger logger = LoggerFactory.getLogger(MultiFormatMapper.class);
42
43     protected boolean isTree = false;
44     protected static final String PROPERTIES_KEY = "properties";
45     protected static final String NODE_TYPE_KEY = "node-type";
46
47     protected static final String RETURNED_EMPTY_JSONARRAY_MSG =
48         "Returned empty JsonArray - Could not populate nested json objects for wrapper: {}";
49
50     @Override
51     public Optional<JsonObject> formatObject(Object input)
52         throws AAIFormatVertexException, AAIFormatQueryResultFormatNotSupported {
53         if (input instanceof Vertex) {
54             logger.debug("Formatting vertex object");
55             return this.getJsonFromVertex((Vertex) input);
56         } else if (input instanceof Tree) {
57             logger.debug("Formatting tree object");
58             if (isTree) {
59                 return this.getRelatedNodesFromTree((Tree<?>) input, null);
60             } else {
61                 return this.getJsonFromTree((Tree<?>) input);
62             }
63         } else if (input instanceof Path) {
64             logger.debug("Formatting path object");
65             return this.getJsonFromPath((Path) input);
66         } else {
67             throw new AAIFormatQueryResultFormatNotSupported();
68         }
69     }
70
71     @Override
72     public Optional<JsonObject> formatObject(Object input, Map<String, List<String>> properties)
73         throws AAIFormatVertexException, AAIFormatQueryResultFormatNotSupported {
74         if (input instanceof Vertex) {
75             logger.debug("Formatting vertex object with properties map filter");
76             return this.getJsonFromVertex((Vertex) input, properties);
77         } else if (input instanceof Tree) {
78             logger.debug("Formatting tree object with properties map filter");
79             if (isTree) {
80                 return this.getRelatedNodesFromTree((Tree<?>) input, properties);
81             } else {
82                 return this.getJsonFromTree((Tree<?>) input);
83             }
84         } else if (input instanceof Path) {
85             logger.debug("Formatting path object");
86             return this.getJsonFromPath((Path) input);
87         } else {
88             throw new AAIFormatQueryResultFormatNotSupported();
89         }
90     }
91
92     protected abstract Optional<JsonObject> getJsonFromVertex(Vertex input) throws AAIFormatVertexException;
93     protected abstract Optional<JsonObject> getJsonFromVertex(Vertex input, Map<String, List<String>> properties) throws AAIFormatVertexException;
94
95     protected Optional<JsonObject> getJsonFromPath(Path input) throws AAIFormatVertexException {
96         List<Object> path = input.objects();
97
98         JsonObject jo = new JsonObject();
99         JsonArray ja = new JsonArray();
100
101         for (Object o : path) {
102             if (o instanceof Vertex) {
103                 Optional<JsonObject> obj = this.getJsonFromVertex((Vertex) o);
104                 obj.ifPresent(ja::add);
105             }
106         }
107
108         jo.add("path", ja);
109         return Optional.of(jo);
110     }
111
112     /**
113      * Returns an Optional<JsonObject> object using "nodes" as a wrapper to encapsulate json objects
114      * @param tree
115      * @return
116      * @throws AAIFormatVertexException
117      */
118     protected Optional<JsonObject> getJsonFromTree(Tree<?> tree) throws AAIFormatVertexException {
119         if (tree.isEmpty()) {
120             return Optional.of(new JsonObject());
121         }
122         String nodeIdentifier = "nodes";
123
124         JsonObject t = new JsonObject();
125         JsonArray ja = this.getNodesArray(tree, null, nodeIdentifier);
126         if (ja.size() > 0) {
127             t.add("nodes", ja);
128         } else {
129             logger.debug(RETURNED_EMPTY_JSONARRAY_MSG, nodeIdentifier);
130         }
131
132         return Optional.of(t);
133     }
134
135     /**
136      * Returns an Optional<JsonObject> object using "related-nodes" to encapsulate nested json objects.
137      * Primarily intended to be utilized by the "as-tree" query parameter feature
138      * @param tree
139      * @param properties
140      * @return
141      * @throws AAIFormatVertexException
142      */
143     protected Optional<JsonObject> getRelatedNodesFromTree(Tree<?> tree, Map<String, List<String>> properties) throws AAIFormatVertexException {
144         if (tree.isEmpty()) {
145             return Optional.of(new JsonObject());
146         }
147         String nodeIdentifier = "related-nodes";
148
149         // Creating another DS to help with calls in O(1)
150         Map<String, Set<String>> filterPropertiesMap = createFilteredPropertyMap(properties);
151
152         JsonObject t = new JsonObject();
153         JsonArray ja = this.getNodesArray(tree, filterPropertiesMap, nodeIdentifier);
154         if (ja.size() > 0) {
155             t.add("results", ja);
156             return Optional.of(t);
157         } else {
158             logger.debug(RETURNED_EMPTY_JSONARRAY_MSG, nodeIdentifier);
159         }
160
161         return Optional.empty();
162     }
163
164     /**
165      * Returns JsonArray Object populated with nested json wrapped by the nodeIdentifier parameter
166      * @param tree
167      * @param filterPropertiesMap
168      * @param nodeIdentifier
169      * @return
170      * @throws AAIFormatVertexException
171      */
172     protected JsonArray getNodesArray(Tree<?> tree, Map<String, Set<String>> filterPropertiesMap, String nodeIdentifier) throws AAIFormatVertexException {
173         JsonArray nodes = new JsonArray();
174         for (Map.Entry<?, ? extends Tree<?>> entry : tree.entrySet()) {
175             JsonObject me = new JsonObject();
176             if (entry.getKey() instanceof Vertex) {
177                 Optional<JsonObject> obj = this.getJsonFromVertex((Vertex) entry.getKey());
178                 if (obj.isPresent()) {
179                     me = getPropertyFilteredObject(obj, filterPropertiesMap);
180                 } else {
181                     continue;
182                 }
183             }
184             JsonArray ja = this.getNodesArray(entry.getValue(), filterPropertiesMap, nodeIdentifier);
185             if (ja.size() > 0) {
186                 me.add(nodeIdentifier, ja);
187             } else {
188                 logger.debug(RETURNED_EMPTY_JSONARRAY_MSG, nodeIdentifier);
189             }
190             nodes.add(me);
191         }
192         return nodes;
193     }
194
195     /**
196      * Returns a Map<String, Set<String>> object through converting given map parameter
197      * @param properties
198      * @return
199      */
200     protected Map<String, Set<String>> createFilteredPropertyMap(Map<String, List<String>> properties) {
201         if (properties == null)
202             return new HashMap<>();
203
204         return properties.entrySet().stream()
205             .map(entry -> {
206                     Set<String> newSet = entry.getValue().stream()
207                         .map(this::truncateApostrophes)
208                         .collect(Collectors.toSet());
209
210                     return Pair.of(entry.getKey(), newSet);
211                 }
212             ).collect(Collectors.toMap(Pair::getKey, Pair::getValue));
213     }
214
215     /**
216      * Returns a string with it's apostrophes truncated at the start and end.
217      * @param s
218      * @return
219      */
220     protected String truncateApostrophes(String s) {
221         if (s == null || s.isEmpty()) {
222             return s;
223         }
224         if (s.startsWith("'") && s.endsWith("'")) {
225             s = s.substring(1, s.length() - 1);
226         }
227         return s;
228     }
229
230     /**
231      * Filters the given Optional<JsonObject> with the properties under a properties field
232      * or the properties under its respective node type.
233      * @param obj
234      * @param filterPropertiesMap
235      * @return
236      */
237     protected JsonObject getPropertyFilteredObject(Optional<JsonObject> obj,
238         Map<String, Set<String>> filterPropertiesMap) {
239         return obj.map(
240             jsonObj -> {
241                 if (filterPropertiesMap == null || filterPropertiesMap.isEmpty()) {
242                     return jsonObj;
243                 } else {
244                     ImmutableTriple<JsonObject, Optional<String>, Optional<JsonObject>> triple =
245                         cloneObjectAndExtractNodeTypeAndProperties(jsonObj);
246
247                     JsonObject result = triple.left;
248                     Optional<String> nodeType = triple.middle;
249                     Optional<JsonObject> properties = triple.right;
250
251                     // Filter current object based on it containing fields: "node-type" and "properties"
252                     if (nodeType.isPresent() && properties.isPresent()) {
253                         filterByNodeTypeAndProperties(result, nodeType.get(), properties.get(), filterPropertiesMap);
254                     } else {
255                         // filter current object based on the: key - nodeType & value - JsonObject of nodes properties
256                         filterByJsonObj(result, jsonObj, filterPropertiesMap);
257                     }
258
259                     return result;
260                 }
261             }
262         ).orElseGet(JsonObject::new);
263     }
264
265     private ImmutableTriple<JsonObject, Optional<String>, Optional<JsonObject>> cloneObjectAndExtractNodeTypeAndProperties(
266         JsonObject jsonObj) {
267         JsonObject result = new JsonObject();
268         Optional<String> nodeType = Optional.empty();
269         Optional<JsonObject> properties = Optional.empty();
270
271         // clone object
272         for (Map.Entry<String, JsonElement> mapEntry : jsonObj.entrySet()) {
273             String key = mapEntry.getKey();
274             JsonElement value = mapEntry.getValue();
275
276             // also, check if payload has node-type and properties fields
277             if (key.equals(NODE_TYPE_KEY) && value != null) {
278                 nodeType = Optional.of(value.getAsString());
279             } else if (key.equals(PROPERTIES_KEY) && value != null && value.isJsonObject()) {
280                 properties = Optional.of(value.getAsJsonObject());
281             }
282             result.add(key, value);
283         }
284
285         return ImmutableTriple.of(result, nodeType, properties);
286     }
287
288     /**
289      * Returns a JsonObject with filtered properties using "node-type" and "properties"
290      * Used for formats with payloads similar to simple and raw
291      * @param result
292      * @param nodeType
293      * @param properties
294      * @param filterPropertiesMap
295      * @return
296      */
297     private JsonObject filterByNodeTypeAndProperties(JsonObject result, String nodeType, JsonObject properties, Map<String, Set<String>> filterPropertiesMap) {
298         if (result == null || nodeType == null || nodeType.isEmpty() || properties == null || filterPropertiesMap == null) {
299             return result;
300         }
301         if (filterPropertiesMap.containsKey(nodeType)) {    // filterPropertiesMap keys are nodeTypes - keys are obtained from the incoming query request
302             Set<String> filterSet = filterPropertiesMap.get(nodeType);
303             JsonObject filteredProperties = new JsonObject();
304             for (String property : filterSet) {             // Each nodeType should have a set of properties to be retained in the response
305                 if (properties.get(property) != null) {
306                     filteredProperties.add(property, properties.get(property));
307                 }
308             }
309             result.remove(PROPERTIES_KEY);
310             result.add(PROPERTIES_KEY, filteredProperties);
311         }
312         return result;
313     }
314
315     /**
316      * Returns a JsonObject with its properties filtered
317      * @param result
318      * @param jsonObj
319      * @param filterPropertiesMap
320      * @return
321      */
322     private JsonObject filterByJsonObj(JsonObject result, JsonObject jsonObj, Map<String, Set<String>> filterPropertiesMap) {
323         if (result == null || jsonObj == null || filterPropertiesMap == null) {
324             return result;
325         }
326
327         for (Map.Entry<String, JsonElement> mapEntry : jsonObj.entrySet()) {
328             String key = mapEntry.getKey();
329             JsonElement value = mapEntry.getValue();
330             JsonObject filteredProperties = new JsonObject();
331             if (value != null && value.isJsonObject() && filterPropertiesMap.containsKey(key)) {
332                 JsonObject joProperties = value.getAsJsonObject();
333                 Set<String> filterSet = filterPropertiesMap.get(key);
334                 for (String property : filterSet) {
335                     if (joProperties.get(property) != null) {
336                         filteredProperties.add(property, joProperties.get(property));
337                     }
338                 }
339                 result.remove(key);
340                 result.add(key, filteredProperties);
341             }
342         }
343         return result;
344     }
345
346     /**
347      * Returns a filtered JsonObject with properties contained in the parameter filterPropertiesMap
348      * @param properties
349      * @param filterPropertiesMap
350      * @return
351      */
352     protected JsonObject filterProperties(Optional<JsonObject> properties, String nodeType,
353         Map<String, Set<String>> filterPropertiesMap) {
354         return properties.map(jo -> {
355             if (filterPropertiesMap == null || filterPropertiesMap.isEmpty()) {
356                 return properties.get();
357             }
358
359             JsonObject result = new JsonObject();
360             // clone the object
361             for (Map.Entry<String, JsonElement> mapEntry : jo.entrySet()) {
362                 String key = mapEntry.getKey();
363                 JsonElement value = mapEntry.getValue();
364                 result.add(key, value);
365             }
366
367             // filter the object
368             if (filterPropertiesMap.containsKey(nodeType)) {
369                 Set<String> filterSet = filterPropertiesMap.get(nodeType);
370                 for (Map.Entry<String, JsonElement> mapEntry : jo.entrySet()) {
371                     String key = mapEntry.getKey();
372                     if (!filterSet.contains(key)) {
373                         result.remove(key);
374                     }
375                 }
376             }
377             return result;
378         }).orElseGet(JsonObject::new);
379     }
380
381     @Override
382     public int parallelThreshold() {
383         return 100;
384     }
385
386 }