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