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