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