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