ccb1ac098463aa16e6d860d55b4099a864172d3f
[ccsdk/features.git] /
1 /*
2  * ============LICENSE_START=======================================================
3  * ONAP : ccsdk features
4  * ================================================================================
5  * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property.
6  * All rights reserved.
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  */
22 package org.onap.ccsdk.features.sdnr.wt.devicemanager.oran.impl;
23
24 import java.lang.reflect.Field;
25 import java.lang.reflect.InvocationTargetException;
26 import java.lang.reflect.Method;
27 import java.lang.reflect.Proxy;
28 import java.time.Instant;
29 import java.util.HashMap;
30 import java.util.List;
31 import org.eclipse.jdt.annotation.NonNull;
32 import org.opendaylight.yangtools.concepts.Identifier;
33 import org.opendaylight.yangtools.yang.binding.DataObject;
34 import org.opendaylight.yangtools.yang.binding.EventInstantAware;
35 import org.opendaylight.yangtools.yang.binding.Identifiable;
36 import org.opendaylight.yangtools.yang.binding.Notification;
37 import org.opendaylight.yangtools.yang.common.QName;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 public class ORanNotificationMapper {
42
43     /*
44      *  Converter of TR069 notifications to VES key, value hashmap.
45      *  Notifications are received as cascade if proxy object.
46      *  References: https://stackoverflow.com/questions/19633534/what-is-com-sun-proxy-proxy
47      *
48      *  Attributes are provided by getters starting with "get", "is", "key".
49      *  Proxy received: "com.sun.proxy.$ProxyNN". NN is a number.
50      *
51      *  Example result:
52      *
53      * Expected output via VES in JSON
54      *    {
55      *      "event": {
56      *         "commonEventHeader": {
57      *            "domain": "notification",
58      *            "eventId": "ABCD",
59      *            "eventName": "Notification_LTE_Enterprise_C-RANSC_Cntrl-ACME",
60      *            "eventType": "TR069_RAN_notification",
61      *            "sequence": 0,
62      *            "priority": "High",
63      *            "reportingEntityId": "0005B942CDB4",
64      *            "reportingEntityName": "ABCD",
65      *            "sourceId": "0005B942CDB4",
66      *            "sourceName": "ABCD",
67      *            "startEpochMicrosec": 1569579510211,
68      *            "lastEpochMicrosec": 1569579510211,
69      *            "nfcNamingCode": "",
70      *            "nfNamingCode": "",
71      *            "nfVendorName": "",
72      *            "timeZoneOffset": "+00:00",
73      *            "version": "4.0.1",
74      *            "vesEventListenerVersion": "7.0.1"
75      *         },
76      *         "notificationFields": {
77      *         "arrayOfNamedHashMap": [
78      *          {
79      *           "name": "VALUECHANGE",
80      *           "hashMap": {
81      *               "/notification/VALUECHANGE[@xmlns=\"urn:org:onap:ccsdk:features:sdnr:northbound:onecell-notification\"]/device/device-info/serial-number": "0005B94238A0",
82      *               "/notification/VALUECHANGE[@xmlns=\"urn:org:onap:ccsdk:features:sdnr:northbound:onecell-notification\"]/device/device-info/software-version": "4.3.00.244",
83      *               "/notification/VALUECHANGE[@xmlns=\"urn:org:onap:ccsdk:features:sdnr:northbound:onecell-notification\"]/device/device-info/hardware-version": "1",
84      *               "/notification/VALUECHANGE[@xmlns=\"urn:org:onap:ccsdk:features:sdnr:northbound:onecell-notification\"]/device/device-info/provisioning-code": "",
85      *               "/notification/VALUECHANGE[@xmlns=\"urn:org:onap:ccsdk:features:sdnr:northbound:onecell-notification\"]/device/device-info/manufacturer": "ACME",
86      *               "/notification/VALUECHANGE[@xmlns=\"urn:org:onap:ccsdk:features:sdnr:northbound:onecell-notification\"]/device/device-info/product-class": "LTE_Enterprise_C-RANSC_Cntrl",
87      *               "/notification/VALUECHANGE[@xmlns=\"urn:org:onap:ccsdk:features:sdnr:northbound:onecell-notification\"]/device/device-info/manufacturer-oui": "0005B9",
88      *               "/notification/VALUECHANGE[@xmlns=\"urn:org:onap:ccsdk:features:sdnr:northbound:onecell-notification\"]/device/services/fap-service[1]/index": "1",
89      *               "/notification/VALUECHANGE[@xmlns=\"urn:org:onap:ccsdk:features:sdnr:northbound:onecell-notification\"]/device/services/fap-service[1]/fap-control/lte/rf-tx-status": "false",
90      *               "/notification/VALUECHANGE[@xmlns=\"urn:org:onap:ccsdk:features:sdnr:northbound:onecell-notification\"]/device/services/fap-service[1]/fap-control/lte/op-state": "true",
91      *               "/notification/VALUECHANGE[@xmlns=\"urn:org:onap:ccsdk:features:sdnr:northbound:onecell-notification\"]/device/services/fap-service[2]/index": "2",
92      *               "/notification/VALUECHANGE[@xmlns=\"urn:org:onap:ccsdk:features:sdnr:northbound:onecell-notification\"]/device/services/fap-service[2]/fap-control/lte/rf-tx-status": "false",
93      *               "/notification/VALUECHANGE[@xmlns=\"urn:org:onap:ccsdk:features:sdnr:northbound:onecell-notification\"]/device/services/fap-service[2]/fap-control/lte/op-state": "true",
94      *               "/notification/VALUECHANGE[@xmlns=\"urn:org:onap:ccsdk:features:sdnr:northbound:onecell-notification\"]/device/services/fap-service[2]/cell-config/lte/ran/rf/phy-cell-id": "201",
95      *               "/notification/VALUECHANGE[@xmlns=\"urn:org:onap:ccsdk:features:sdnr:northbound:onecell-notification\"]/device/management-server/connection-request-url": "http://10.220.68.2/acscall",
96      *               "/notification/VALUECHANGE[@xmlns=\"urn:org:onap:ccsdk:features:sdnr:northbound:onecell-notification\"]/device/management-server/parameter-key": "none",
97      *               "/notification/VALUECHANGE[@xmlns=\"urn:org:onap:ccsdk:features:sdnr:northbound:onecell-notification\"]/device/management-server/connection-request-password": "password",
98      *               "/notification/VALUECHANGE[@xmlns=\"urn:org:onap:ccsdk:features:sdnr:northbound:onecell-notification\"]/device/management-server/connection-request-username": "0005B9-LTE_Enterprise_C-RANSC_Cntrl-0005B94238A0"
99      *           },
100      *          }
101      *         ],
102      *         "changeContact": "",
103      *         "changeIdentifier": "SessionID",
104      *         "changeType": "ValueChange",
105      *         "newState": "",
106      *         "oldState": "",
107      *         "stateInterface": "",
108      *         "notificationFieldsVersion": "2.0",
109      *         }
110      *      }
111      *    }
112      *
113      */
114         private static final Logger log = LoggerFactory.getLogger(ORanNotificationMapper.class);
115         private Notification notification;
116
117         public HashMap<String, String> performMapping(Notification notification)
118         /*throws ORanNotificationMapperException*/ {
119
120             try {
121                 return extractFields(notification);
122             } catch (IllegalArgumentException | SecurityException | NoSuchFieldException | IllegalAccessException
123                     | InvocationTargetException e) {
124                 //throw new ORanNotificationMapperException("Mapping/JSON Creation problem", e);
125                 log.info("Exception in performMapping method {}",e);
126                 return null;
127             }
128
129         }
130
131         private HashMap<String, String> extractFields(Object o) throws IllegalAccessException, IllegalArgumentException,
132                 InvocationTargetException, NoSuchFieldException, SecurityException {
133             String start = "/notification/" + getExtendedInterfaceName(Notification.class, o) + getXmlNameSpace(o);
134             return recurseExtractData(o, start, 0, new HashMap<String, String>());
135         }
136
137         private HashMap<String, String> recurseExtractData(Object o, String namePath, int level,
138                 HashMap<String, String> result)
139                 throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
140             log.debug("In recurseExtractData - {} {} {}", namePath, level, log.isTraceEnabled() ? result : result.size());
141             if (level > 20) {
142                 log.warn("Level to deep protection ended the recusive loop.");
143             } else {
144                 if (o != null) {
145                     Class<?> classz = o.getClass();
146                     //notification/VALUECHANGE$$$eventInstantAware[@xmlns=urn:org:onap:ccsdk:features:sdnr:northbound:onecell-notificationon] 0 {}
147                     //org.opendaylight.yang.gen.v1.urn.org.onap.ccsdk.features.sdnr.northbound.onecell.notification.rev200622.VALUECHANGE$$$eventInstantAware
148                     //if (Proxy.isProxyClass(classz)) {
149                     handleInterface(classz, o, namePath, level, result);
150                     //}
151                 } else {
152                     log.warn("Null not expected here.");
153                 }
154             }
155             return result;
156         }
157
158         private void handleInterface(Class<?> clazz, Object o, String namePath, int level, HashMap<String, String> result)
159                 throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
160             log.debug("In extract Interface {}", clazz);
161             if (clazz == null) {
162                 log.warn("Loop with null class");
163                 return;
164             }
165             if (level > 20) {
166                 log.warn("Level to deep protection ended the recusive loop.");
167             }
168             if (clazz.getName().contentEquals("org.opendaylight.mdsal.binding.dom.codec.impl.AugmentableCodecDataObject")) {
169                 log.trace("Leave AugmentableCodecDataObject");
170                 return;
171             }
172
173             Method[] methods = clazz.getDeclaredMethods();
174             if (methods != null) {
175                 for (Method method : methods) {
176                     String methodName = method.getName();
177                     log.trace("Method {}", methodName);
178                     if (methodName.startsWith("get")) {
179                         if (!methodName.equals("getImplementedInterface")) {
180                             handleGetterValue(method, methodName.substring(3), namePath, o, level, result);
181                         }
182                     } else if (methodName.startsWith("is")) {
183                         handleGetterValue(method, methodName.substring(2), namePath, o, level, result);
184                     } else if (methodName.equals("key")) {
185                         handleGetterValue(method, methodName, namePath, o, level, result);
186                     }
187                 }
188             }
189             Class<?> sc = clazz.getSuperclass();  //Sodium
190             log.trace("Superclass is - {}", sc);
191             if (sc != null && !(sc.getName().contains("java.lang.reflect.Proxy")) && !Proxy.isProxyClass(sc)) {
192                 handleInterface(sc, o, namePath, level + 1, result);
193             }
194         }
195
196         private void handleGetterValue(Method method, String name, String namePath, Object o, int level,
197                 HashMap<String, String> result)
198                 throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
199             log.debug("Begin: {}-{}-{}-{}", method.getName(), name, namePath, level);
200             if (!method.isAccessible()) {
201                 method.setAccessible(true);
202             }
203             Object value = method.invoke(o);
204             namePath += "/" + convertCamelToKebabCase(name);
205             log.trace("Namepath {}", namePath);
206             if (value != null) {
207                 Class<?> type = value.getClass();
208                 log.trace("Class {}", type.getSimpleName());
209                 if (List.class.isAssignableFrom(type)) {
210                     int idx = 0;
211                     String keyString;
212                     for (Object listObject : (List<?>) value) {
213                         if (listObject != null) {
214                             if (Identifiable.class.isAssignableFrom(listObject.getClass())) {
215                                 keyString = getKeyString((Identifiable<?>) listObject);
216                             } else {
217                                 keyString = String.valueOf(idx);
218                             }
219                             recurseExtractData(listObject, namePath + "[" + keyString + "]", level + 1, result);
220                         } else {
221                             log.warn("Null value received {} {} {}", namePath, idx, name);
222                         }
223                         idx++;
224                     }
225                 } else if (DataObject.class.isAssignableFrom(type)) {
226                     recurseExtractData(value, namePath, level + 1, result);
227                 } else if (Proxy.isProxyClass(type)) {
228                     recurseExtractData(value, namePath, level + 1, result);
229                 } else if (Identifier.class.isAssignableFrom(type)) {
230                     //don't put the key
231                 } else {
232                     result.put(namePath, value.toString());
233                 }
234             } else {
235                 log.trace("Null value");
236             }
237         }
238
239         private String getExtendedInterfaceName(Class<?> assignableClazz, Object o) {
240             Class<?> interfaces[] = o.getClass().getInterfaces();
241             for (Class<?> oneInterface : interfaces) {
242                 log.trace("In getExtendedInterfaceName, oneInterface = {}", oneInterface.getClass().getName());
243                 if (assignableClazz.isAssignableFrom(oneInterface)) {
244                     return oneInterface.getSimpleName().contains("eventInstantAware")?oneInterface.getSimpleName().substring(0, oneInterface.getSimpleName().indexOf("$")):oneInterface.getSimpleName();
245                 }
246             }
247             log.trace("In getExtendedInterfaceName, o.getClass().getName() = {}", o.getClass().getName());
248             return o.getClass().getSimpleName().contains("eventInstantAware")?o.getClass().getSimpleName().substring(0, o.getClass().getSimpleName().indexOf("$")):o.getClass().getSimpleName();
249         }
250
251         private String getXmlNameSpace(Object o)
252                 throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
253             Field f = o.getClass().getField("QNAME");
254             Object couldQName = f.get(o);
255             if (couldQName instanceof QName) {
256                 QName qname = (QName) couldQName;
257                 return "[@xmlns=" + qname.getNamespace().toString() + "]";
258             }
259             return "";
260         }
261
262         /*private String convertCamelToKebabCase(String camel) {
263             KebabCaseStrategy kbCase = new KebabCaseStrategy();
264             return kbCase.translate(camel);
265             //return camel.replaceAll("([a-z0-9])([A-Z])", "$1-$2").toLowerCase();
266         } */
267
268         /**
269          * @param input string in Camel Case
270          * @return String in Kebab case
271          * Inspiration from KebabCaseStrategy class of com.fasterxml.jackson.databind with an additional condition to handle numbers as well
272          * Using QNAME would have been a more fool proof solution, however it can lead to performance problems due to usage of Java reflection
273          */
274         public String convertCamelToKebabCase(String input)
275         {
276             if (input == null) return input; // garbage in, garbage out
277             int length = input.length();
278             if (length == 0) {
279                 return input;
280             }
281
282             StringBuilder result = new StringBuilder(length + (length >> 1));
283
284             int upperCount = 0;
285
286             for (int i = 0; i < length; ++i) {
287                 char ch = input.charAt(i);
288                 char lc = Character.toLowerCase(ch);
289
290                 if (lc == ch) { // lower-case letter means we can get new word
291                     // but need to check for multi-letter upper-case (acronym), where assumption
292                     // is that the last upper-case char is start of a new word
293                     if ((upperCount > 1)  ){
294                         // so insert hyphen before the last character now
295                         result.insert(result.length() - 1, '-');
296                     } else if ((upperCount == 1) && Character.isDigit(ch) && i != length-1) {
297                         result.append('-');
298                     }
299                     upperCount = 0;
300                 } else {
301                     // Otherwise starts new word, unless beginning of string
302                     if ((upperCount == 0) && (i > 0)) {
303                         result.append('-');
304                     }
305                     ++upperCount;
306                 }
307                 result.append(lc);
308             }
309             return result.toString();
310         }
311
312         /**
313          * Key format like this: "FapServiceKey{_index=2}"
314          *
315          * @return
316          */
317         private String getKeyString(Identifiable<?> indentifiableObject) {
318             String keyString = (indentifiableObject.key()).toString();
319             int start = keyString.indexOf("=") + 1;
320             int end = keyString.length() - 1;
321             if (start > 0 && start < end)
322                 return keyString.substring(keyString.indexOf("=") + 1, keyString.length() - 1);
323             else
324                 throw new IllegalArgumentException("indentifiable object without key");
325         }
326
327         public Instant getTime(Notification notification2) {
328             @NonNull
329             Instant time;
330             if (notification instanceof EventInstantAware) { // If notification class extends/implements the EventInstantAware
331                 time = ((EventInstantAware) notification).eventInstant();
332                 log.info("Event time {}", time);
333             } else {
334                 time = Instant.now();
335                 log.info("Defaulting to actual time of processing the notification - {}", time);
336             }
337             return time;
338         }
339 }