063fae622cc7c7b19b6f1cd92361980f5d15c071
[ccsdk/features.git] /
1 /*
2  * ============LICENSE_START=======================================================
3  * ONAP : ccsdk features
4  * ================================================================================
5  * Copyright (C) 2021 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.dataprovider.database.sqldb.database;
23
24 import com.fasterxml.jackson.core.JsonProcessingException;
25 import com.fasterxml.jackson.databind.JsonMappingException;
26 import java.lang.reflect.InvocationTargetException;
27 import java.lang.reflect.Method;
28 import java.math.BigInteger;
29 import java.sql.ResultSet;
30 import java.sql.SQLException;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.HashMap;
34 import java.util.List;
35 import java.util.Map;
36 import org.onap.ccsdk.features.sdnr.wt.dataprovider.database.sqldb.query.filters.DBKeyValuePair;
37 import org.onap.ccsdk.features.sdnr.wt.yang.mapper.YangToolsMapper;
38 import org.onap.ccsdk.features.sdnr.wt.yang.mapper.YangToolsMapperHelper;
39 import org.onap.ccsdk.features.sdnr.wt.yang.mapper.mapperextensions.YangToolsBuilderAnnotationIntrospector;
40 import org.onap.ccsdk.features.sdnr.wt.yang.mapper.mapperextensions.YangToolsDeserializerModifier;
41 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.types.rev130715.DateAndTime;
42 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.data.provider.rev201110.Entity;
43 import org.opendaylight.yangtools.concepts.Builder;
44 import org.opendaylight.yangtools.yang.binding.DataObject;
45 import org.opendaylight.yangtools.yang.binding.Enumeration;
46 import org.opendaylight.yangtools.yang.common.Uint16;
47 import org.opendaylight.yangtools.yang.common.Uint32;
48 import org.opendaylight.yangtools.yang.common.Uint64;
49 import org.opendaylight.yangtools.yang.common.Uint8;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 public class SqlDBMapper {
54
55     private static final Logger LOG = LoggerFactory.getLogger(SqlDBMapper.class);
56
57     private static final Map<Class<?>, String> mariaDBTypeMap = initTypeMap();
58     private static final String ODLID_DBTYPE = "VARCHAR(40)";
59     private static final String STRING_DBTYPE = "VARCHAR(255)";
60     private static final String ENUM_DBTYPE = "VARCHAR(100)";
61     public static final String ODLID_DBCOL = "controller-id";
62     private static List<Class<?>> numericClasses = Arrays.asList(Byte.class, Integer.class, Long.class,
63             BigInteger.class, Uint8.class, Uint16.class, Uint32.class, Uint64.class);
64     private static final YangToolsMapper mapper = new YangToolsMapper();
65     public static final String TABLENAME_CONTROLLER = "controller";
66     private static final String DEFAULTID_DBTYPE = "int(11)";
67
68     public static String createTableOdl() {
69         return "CREATE TABLE IF NOT EXISTS " + TABLENAME_CONTROLLER + " (" + "`id` " + ODLID_DBTYPE + " "
70                 + getColumnOptions("id", ODLID_DBTYPE) + "," + "`desc` " + STRING_DBTYPE + " "
71                 + getColumnOptions("description", STRING_DBTYPE) + "," + "primary key(id))";
72     }
73
74     public static <T> String createTable(Class<T> clazz, Entity e) throws UnableToMapClassException {
75         return createTable(clazz, e, "", false);
76     }
77
78     public static <T> String createTable(Class<T> clazz, Entity e, String suffix) throws UnableToMapClassException {
79         return createTable(clazz, e, suffix, false);
80     }
81
82     public static <T> String createTable(Class<T> clazz, Entity e, boolean autoIndex) throws UnableToMapClassException {
83         return createTable(clazz, e, "", false);
84     }
85
86     public static <T> String createTable(Class<T> clazz, Entity e, String suffix, boolean autoIndex)
87             throws UnableToMapClassException {
88         StringBuilder sb = new StringBuilder();
89         sb.append("CREATE TABLE IF NOT EXISTS `" + e.getName() + suffix + "` (\n");
90         if (autoIndex) {
91             sb.append("`id` " + DEFAULTID_DBTYPE + " " + getColumnOptions("id", DEFAULTID_DBTYPE) + ",\n");
92         } else {
93             sb.append("`id` " + STRING_DBTYPE + " " + getColumnOptions("id", STRING_DBTYPE) + ",\n");
94         }
95         sb.append("`" + ODLID_DBCOL + "` " + ODLID_DBTYPE + " " + getColumnOptions(ODLID_DBCOL, ODLID_DBTYPE) + ",\n");
96         for (Method method : getFilteredMethods(clazz, true)) {
97             Class<?> valueType = method.getReturnType();
98             String colName = getColumnName(method);
99             if (colName.equals("id")) {
100                 continue;
101             }
102             String dbType = getDBType(valueType);
103             String options = getColumnOptions(colName, dbType);
104             sb.append("`" + colName + "` " + dbType + " " + options + ",\n");
105         }
106         sb.append("primary key(id),");
107         sb.append("foreign key(`" + ODLID_DBCOL + "`) references " + TABLENAME_CONTROLLER + "(id)");
108
109         sb.append(");");
110         return sb.toString();
111     }
112
113     private static String getColumnOptions(String colName, String dbType) {
114         StringBuilder options = new StringBuilder();
115         if (dbType.contains("VARCHAR")) {
116             options.append("CHARACTER SET utf8 ");
117         }
118         if (colName.equals("id") || colName.equals(ODLID_DBCOL)) {
119             if (dbType.equals(DEFAULTID_DBTYPE)) {
120                 options.append("NOT NULL AUTO_INCREMENT");
121             } else {
122                 options.append("NOT NULL");
123             }
124         }
125         return options.toString();
126     }
127
128     public static List<Method> getFilteredMethods(Class<?> clazz, boolean getterOrSetter) {
129         Method[] methods = clazz.getMethods();
130         List<Method> list = new ArrayList<>();
131         for (Method method : methods) {
132             if (getterOrSetter) {
133                 if (!isGetter(method)) {
134                     continue;
135                 }
136             } else {
137                 if (!isSetter(method)) {
138                     continue;
139                 }
140             }
141             if (ignoreMethod(method, methods, getterOrSetter)) {
142                 continue;
143             }
144             list.add(method);
145         }
146         return list;
147     }
148
149
150     private static Map<Class<?>, String> initTypeMap() {
151         Map<Class<?>, String> map = new HashMap<>();
152         map.put(String.class, STRING_DBTYPE);
153         map.put(Boolean.class, "BOOLEAN");
154         map.put(Byte.class, "TINYINT");
155         map.put(Integer.class, "INTEGER");
156         map.put(Long.class, "BIGINT");
157         map.put(BigInteger.class, "BIGINT");
158         map.put(Uint8.class, "SMALLINT");
159         map.put(Uint16.class, "INTEGER");
160         map.put(Uint32.class, "BIGINT");
161         map.put(Uint64.class, "BIGINT"); //????
162         map.put(DateAndTime.class, "DATETIME(3)");
163         return map;
164     }
165
166     private static boolean ignoreMethod(Method method, Method[] classMehtods, boolean getterOrSetter) {
167         final String name = method.getName();
168         if (name.equals("getAugmentations") || name.equals("getImplementedInterface")
169                 || name.equals("implementedInterface") || name.equals("getClass")) {
170             return true;
171         }
172         for (Method cm : classMehtods) {
173             if (!cm.equals(method) && cm.getName().equals(name)) {
174                 //resolve conflict
175                 return !resolveConflict(method, cm, getterOrSetter);
176             }
177             //silicon fix
178             if (method.getReturnType().equals(Boolean.class) && getterOrSetter) {
179                 if (name.startsWith("get") && cm.getName().startsWith("is")
180                         && cm.getName().endsWith(name.substring(3))) {
181                     return true;
182                 }
183             }
184         }
185         return false;
186     }
187
188     private static boolean resolveConflict(Method m1, Method m2, boolean getterOrSetter) {
189         Class<?> p1 = getterOrSetter ? m1.getReturnType() : m1.getParameterTypes()[0];
190         Class<?> p2 = getterOrSetter ? m2.getReturnType() : m2.getParameterTypes()[0];
191         if (YangToolsBuilderAnnotationIntrospector.isAssignable(p1, p2, Map.class, List.class)) {
192             return p1.isAssignableFrom(List.class); //prefer List setter
193         } else if (YangToolsBuilderAnnotationIntrospector.isAssignable(p1, p2, Uint64.class, BigInteger.class)) {
194             return p1.isAssignableFrom(Uint64.class);
195         } else if (YangToolsBuilderAnnotationIntrospector.isAssignable(p1, p2, Uint32.class, Long.class)) {
196             return p1.isAssignableFrom(Uint32.class);
197         } else if (YangToolsBuilderAnnotationIntrospector.isAssignable(p1, p2, Uint16.class, Integer.class)) {
198             return p1.isAssignableFrom(Uint16.class);
199         } else if (YangToolsBuilderAnnotationIntrospector.isAssignable(p1, p2, Uint8.class, Short.class)) {
200             return p1.isAssignableFrom(Uint8.class);
201         }
202         return false;
203     }
204
205     public static String getColumnName(Method method) {
206         String camelName = (method.getName().startsWith("get") || method.getName().startsWith("set"))
207                 ? method.getName().substring(3)
208                 : method.getName().substring(2);
209         return convertCamelToKebabCase(camelName);
210     }
211
212     private static String getDBType(Class<?> valueType) throws UnableToMapClassException {
213         String type = mariaDBTypeMap.getOrDefault(valueType, null);
214         if (type == null) {
215             if (implementsInterface(valueType, DataObject.class) || implementsInterface(valueType, List.class)
216                     || implementsInterface(valueType, Map.class)) {
217                 return "JSON";
218             }
219             if (implementsInterface(valueType, Enumeration.class)) {
220                 return ENUM_DBTYPE;
221             }
222             throw new UnableToMapClassException("no mapping for " + valueType.getName() + " found");
223         }
224         return type;
225     }
226
227     private static boolean implementsInterface(Class<?> valueType, Class<?> iftoImpl) {
228         return iftoImpl.isAssignableFrom(valueType);
229     }
230
231     private static boolean isGetter(Method method) {
232         return method.getName().startsWith("get") || method.getName().startsWith("is")
233                 || method.getName().startsWith("do");
234     }
235
236     private static boolean isSetter(Method method) {
237         return method.getName().startsWith("set");
238     }
239
240     /**
241      * @param input string in Camel Case
242      * @return String in Kebab case Inspiration from KebabCaseStrategy class of com.fasterxml.jackson.databind with an
243      *         additional condition to handle numbers as well Using QNAME would have been a more fool proof solution,
244      *         however it can lead to performance problems due to usage of Java reflection
245      */
246     private static String convertCamelToKebabCase(String input) {
247         if (input == null)
248             return input; // garbage in, garbage out
249         int length = input.length();
250         if (length == 0) {
251             return input;
252         }
253
254         StringBuilder result = new StringBuilder(length + (length >> 1));
255
256         int upperCount = 0;
257
258         for (int i = 0; i < length; ++i) {
259             char ch = input.charAt(i);
260             char lc = Character.toLowerCase(ch);
261
262             if (lc == ch) { // lower-case letter means we can get new word
263                 // but need to check for multi-letter upper-case (acronym), where assumption
264                 // is that the last upper-case char is start of a new word
265                 if ((upperCount > 1)) {
266                     // so insert hyphen before the last character now
267                     result.insert(result.length() - 1, '-');
268                 } else if ((upperCount == 1) && Character.isDigit(ch) && i != length - 1) {
269                     result.append('-');
270                 }
271                 upperCount = 0;
272             } else {
273                 // Otherwise starts new word, unless beginning of string
274                 if ((upperCount == 0) && (i > 0)) {
275                     result.append('-');
276                 }
277                 ++upperCount;
278             }
279             result.append(lc);
280         }
281         return result.toString();
282     }
283
284     public static class UnableToMapClassException extends Exception {
285
286         private static final long serialVersionUID = 1L;
287
288         public UnableToMapClassException(String message) {
289             super(message);
290         }
291
292     }
293
294     public static String escape(Object o) {
295         return escape(o.toString());
296     }
297
298     public static String escape(String o) {
299         return o.replace("'", "\'");
300     }
301
302     public static boolean isComplex(Class<?> valueType) {
303         return DataObject.class.isAssignableFrom(valueType) || List.class.isAssignableFrom(valueType);
304     }
305
306     public static Object getNumericValue(Object value, Class<?> valueType) {
307         if (valueType.equals(Byte.class) || valueType.equals(Integer.class) || valueType.equals(Long.class)) {
308             return value;
309         }
310         if (valueType.equals(Uint8.class) || valueType.equals(Uint16.class) || valueType.equals(Uint32.class)
311                 || valueType.equals(Uint64.class)) {
312             return ((Number) value).longValue();
313         }
314         return value;
315     }
316
317     public static Object bool2int(Object invoke) {
318         return Boolean.TRUE.equals(invoke) ? 1 : 0;
319     }
320
321     public static boolean isBoolean(Class<?> valueType) {
322         return valueType.equals(Boolean.class);
323     }
324
325     public static boolean isNumeric(Class<?> valueType) {
326         return numericClasses.contains(valueType);
327
328     }
329
330     private static boolean isDateTime(Class<?> valueType) {
331         return valueType.equals(DateAndTime.class);
332     }
333
334     private static boolean isYangEnum(Class<?> valueType) {
335         return YangToolsMapperHelper.implementsInterface(valueType, Enumeration.class);
336     }
337
338     public static <T extends DataObject> List<T> read(ResultSet data, Class<T> clazz)
339             throws JsonMappingException, IllegalAccessException, IllegalArgumentException, InvocationTargetException,
340             InstantiationException, SecurityException, NoSuchMethodException, JsonProcessingException, SQLException {
341         return read(data, clazz, null);
342     }
343
344     @SuppressWarnings("unchecked")
345     public static <T> List<T> read(ResultSet data, Class<T> clazz, String column) throws IllegalAccessException,
346             IllegalArgumentException, InvocationTargetException, SQLException, InstantiationException,
347             SecurityException, NoSuchMethodException, JsonMappingException, JsonProcessingException {
348
349         List<T> list = new ArrayList<>();
350         while (data.next()) {
351             if (column == null) {
352                 Builder<T> builder = findPOJOBuilder(clazz);
353                 Class<?> argType;
354                 String col;
355                 for (Method m : getFilteredMethods(builder.getClass(), false)) {
356                     argType = m.getParameterTypes()[0];
357                     col = getColumnName(m);
358                     m.setAccessible(true);
359                     m.invoke(builder, getValueOrDefault(data, col, argType, null));
360                 }
361                 list.add(builder.build());
362             } else {
363                 Object value = getValueOrDefault(data, column, clazz, null);
364                 if (value != null) {
365                     list.add((T) value);
366                 }
367             }
368         }
369         return list;
370     }
371
372     @SuppressWarnings("unchecked")
373     private static <T> Builder<T> findPOJOBuilder(Class<T> ac) throws InstantiationException, IllegalAccessException,
374             IllegalArgumentException, InvocationTargetException, SecurityException, NoSuchMethodException {
375         try {
376             String builder = null;
377
378             if (ac.isInterface()) {
379                 String clsName = ac.getName();
380                 if (clsName.endsWith("Entity")) {
381                     clsName = clsName.substring(0, clsName.length() - 6);
382                 }
383                 builder = clsName + "Builder";
384             }
385             if (builder != null) {
386                 Class<?> innerBuilder = YangToolsMapperHelper.findClass(builder);
387                 Class<Builder<T>> builderClass = (Class<Builder<T>>) innerBuilder;
388                 return builderClass.getDeclaredConstructor().newInstance();
389             }
390         } catch (ClassNotFoundException e) {
391
392         }
393         return null;
394     }
395
396     private static Object getValueOrDefault(ResultSet data, String col, Class<?> dstType, Object defaultValue)
397             throws SQLException, JsonMappingException, JsonProcessingException {
398         if (isBoolean(dstType)) {
399             return data.getBoolean(col);
400         } else if (isNumeric(dstType)) {
401             return getNumeric(dstType, data.getLong(col));
402         } else if (String.class.equals(dstType)) {
403             return data.getString(col);
404         } else if (isYangEnum(dstType)) {
405             return getYangEnum(data.getString(col), dstType);
406         } else if (isDateTime(dstType)) {
407             String v = data.getString(col);
408             return v == null || v.equals("null") ? null : DateAndTime.getDefaultInstance(v.replace(" ", "T") + "Z");
409         } else if (isComplex(dstType)) {
410             String v = data.getString(col);
411
412             return (v == null || v.toLowerCase().equals("null")) ? null : mapper.readValue(v, dstType);
413         }
414         return defaultValue;
415     }
416
417
418
419     private static Object getYangEnum(String value, Class<?> dstType) {
420         if (value == null || value.equals("null")) {
421             return null;
422         }
423         try {
424             return YangToolsDeserializerModifier.parseEnum(value, dstType);
425         } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException
426                 | SecurityException e) {
427             LOG.warn("unable to parse enum value '{}' to class {}: ", value, dstType, e);
428         }
429         return null;
430     }
431
432     private static Object getNumeric(Class<?> dstType, long value) {
433         if (dstType.equals(Uint64.class)) {
434             return Uint64.valueOf(value);
435         } else if (dstType.equals(Uint32.class)) {
436             return Uint32.valueOf(value);
437         } else if (dstType.equals(Uint16.class)) {
438             return Uint16.valueOf(value);
439         } else if (dstType.equals(Uint16.class)) {
440             return Uint8.valueOf(value);
441         } else if (dstType.equals(Long.class)) {
442             return Long.valueOf(value);
443         } else if (dstType.equals(Integer.class)) {
444             return Long.valueOf(value).intValue();
445         } else if (dstType.equals(Byte.class)) {
446             return Long.valueOf(value).byteValue();
447         }
448         return null;
449     }
450
451     public static DBKeyValuePair<String> getEscapedKeyValue(Method m, String col, Object value)
452             throws JsonProcessingException {
453         Class<?> valueType = m.getReturnType();
454         String svalue = null;
455         if (isBoolean(valueType)) {
456             svalue = String.valueOf(bool2int(value));
457         } else if (isNumeric(valueType)) {
458             svalue = String.valueOf(getNumericValue(value, valueType));
459         } else if (isDateTime(valueType)) {
460             svalue = "'" + getDateTimeValue((DateAndTime) value) + "'";
461         } else if (isComplex(valueType)) {
462             svalue = "'" + escape(mapper.writeValueAsString(value)) + "'";
463         } else {
464             svalue = "'" + escape(value) + "'";
465         }
466         return new DBKeyValuePair<String>("`" + col + "`", svalue);
467     }
468
469     private static String getDateTimeValue(DateAndTime value) {
470         String s = value.getValue();
471         if (s.endsWith("Z")) {
472             s = s.substring(0, s.length() - 1).replace("T", " ");
473         } else if (s.contains("+")) {
474             s = s.substring(0, s.indexOf("+")).replace("T", " ");
475         }
476         return s;
477     }
478
479
480 }