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