2  * ============LICENSE_START=======================================================
 
   3  * ONAP : ccsdk features
 
   4  * ================================================================================
 
   5  * Copyright (C) 2021 highstreet technologies GmbH Intellectual Property.
 
   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
 
  12  *     http://www.apache.org/licenses/LICENSE-2.0
 
  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=========================================================
 
  22 package org.onap.ccsdk.features.sdnr.wt.dataprovider.database.sqldb.database;
 
  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;
 
  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;
 
  54 public class SqlDBMapper {
 
  56     private static final Logger LOG = LoggerFactory.getLogger(SqlDBMapper.class);
 
  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)";
 
  71     private SqlDBMapper() {
 
  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 + "))";
 
  81     public static <T> String createTable(Class<T> clazz, Entity e) throws UnableToMapClassException {
 
  82         return createTable(clazz, e, "", false, true);
 
  85     public static <T> String createTable(Class<T> clazz, Entity e, String suffix) throws UnableToMapClassException {
 
  86         return createTable(clazz, e, suffix, false, true);
 
  89     public static <T> String createTable(Class<T> clazz, Entity e, boolean autoIndex) throws UnableToMapClassException {
 
  90         return createTable(clazz, e, "", false, true);
 
  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");
 
  99             sb.append("`" + ID_DBCOL + "` " + DEFAULTID_DBTYPE + " " + getColumnOptions(ID_DBCOL, DEFAULTID_DBTYPE)
 
 102             sb.append("`" + ID_DBCOL + "` " + STRING_DBTYPE + " " + getColumnOptions(ID_DBCOL, STRING_DBTYPE) + ",\n");
 
 104         if(withControllerId) {
 
 105             sb.append("`" + ODLID_DBCOL + "` " + ODLID_DBTYPE + " " + getColumnOptions(ODLID_DBCOL, ODLID_DBTYPE) + ",\n");
 
 107         for (Method method : getFilteredMethods(clazz, true)) {
 
 108             Class<?> valueType = method.getReturnType();
 
 109             String colName = getColumnName(method);
 
 110             if (ID_DBCOL.equals(colName)) {
 
 113             String dbType = getDBType(valueType);
 
 114             String options = getColumnOptions(colName, dbType);
 
 115             sb.append("`" + colName + "` " + dbType + " " + options + ",\n");
 
 117         sb.append("primary key(" + ID_DBCOL + ")");
 
 118         if(withControllerId) {
 
 119             sb.append(",foreign key(`" + ODLID_DBCOL + "`) references " + TABLENAME_CONTROLLER + "(" + ID_DBCOL + ")");
 
 123         return sb.toString();
 
 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 ");
 
 131         if (ID_DBCOL.equals(colName) || ODLID_DBCOL.equals(colName)) {
 
 132             if (dbType.equals(DEFAULTID_DBTYPE)) {
 
 133                 options.append("NOT NULL AUTO_INCREMENT");
 
 135                 options.append("NOT NULL");
 
 138         return options.toString();
 
 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)) {
 
 150                 if (!isSetter(method)) {
 
 154             if (ignoreMethod(method, methods, getterOrSetter)) {
 
 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)");
 
 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")) {
 
 185         for (Method cm : classMehtods) {
 
 186             if (!cm.equals(method) && cm.getName().equals(name)) {
 
 188                 return !resolveConflict(method, cm, getterOrSetter);
 
 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))) {
 
 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);
 
 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);
 
 223     private static String getDBType(Class<?> valueType) throws UnableToMapClassException {
 
 224         String type = mariaDBTypeMap.getOrDefault(valueType, null);
 
 226             if (implementsInterface(valueType, DataObject.class) || implementsInterface(valueType, List.class)
 
 227                     || implementsInterface(valueType, Map.class) || implementsInterface(valueType, Set.class)) {
 
 230             if (implementsInterface(valueType, Enumeration.class)) {
 
 233             throw new UnableToMapClassException("no mapping for " + valueType.getName() + " found");
 
 238     private static boolean implementsInterface(Class<?> valueType, Class<?> iftoImpl) {
 
 239         return iftoImpl.isAssignableFrom(valueType);
 
 242     private static boolean isGetter(Method method) {
 
 243         return method.getName().startsWith("get") || method.getName().startsWith("is")
 
 244                 || method.getName().startsWith("do");
 
 247     private static boolean isSetter(Method method) {
 
 248         return method.getName().startsWith("set");
 
 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
 
 257     private static String convertCamelToKebabCase(String input) {
 
 259             return input; // garbage in, garbage out
 
 260         int length = input.length();
 
 265         StringBuilder result = new StringBuilder(length + (length >> 1));
 
 269         for (int i = 0; i < length; ++i) {
 
 270             char ch = input.charAt(i);
 
 271             char lc = Character.toLowerCase(ch);
 
 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) {
 
 284                 // Otherwise starts new word, unless beginning of string
 
 285                 if ((upperCount == 0) && (i > 0)) {
 
 292         return result.toString();
 
 295     public static class UnableToMapClassException extends Exception {
 
 297         private static final long serialVersionUID = 1L;
 
 299         public UnableToMapClassException(String message) {
 
 305     public static String escape(Object o) {
 
 306         return escape(o.toString());
 
 309     public static String escape(String o) {
 
 310         return o.replace("'", "\'");
 
 313     public static boolean isComplex(Class<?> valueType) {
 
 314         return DataObject.class.isAssignableFrom(valueType) || List.class.isAssignableFrom(valueType);
 
 317     public static Object getNumericValue(Object value, Class<?> valueType) {
 
 318         if (valueType.equals(Byte.class) || valueType.equals(Integer.class) || valueType.equals(Long.class)) {
 
 321         if (valueType.equals(Uint8.class) || valueType.equals(Uint16.class) || valueType.equals(Uint32.class)
 
 322                 || valueType.equals(Uint64.class)) {
 
 323             return ((Number) value).longValue();
 
 328     public static Object bool2int(Object invoke) {
 
 329         return Boolean.TRUE.equals(invoke) ? 1 : 0;
 
 332     public static boolean isBoolean(Class<?> valueType) {
 
 333         return valueType.equals(Boolean.class);
 
 336     public static boolean isNumeric(Class<?> valueType) {
 
 337         return numericClasses.contains(valueType);
 
 341     private static boolean isDateTime(Class<?> valueType) {
 
 342         return valueType.equals(DateAndTime.class);
 
 345     private static boolean isYangEnum(Class<?> valueType) {
 
 346         return YangToolsMapperHelper.implementsInterface(valueType, Enumeration.class);
 
 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);
 
 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 {
 
 360             return Arrays.asList();
 
 362         S builder = findPOJOBuilder(clazz);
 
 363         if(builder==null && column==null) {
 
 364             throw new InstantiationException("unable to find builder for class "+clazz.getName());
 
 367         List<T> list = new ArrayList<>();
 
 368         while (data.next()) {
 
 369             if (column == null) {
 
 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));
 
 378                 list.add(callBuild(builder));
 
 380                 Object value = getValueOrDefault(data, column, clazz, null);
 
 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);
 
 395         @SuppressWarnings("unchecked")
 
 396     private static <S,T> S findPOJOBuilder(Class<T> ac) throws InstantiationException, IllegalAccessException,
 
 397             IllegalArgumentException, InvocationTargetException, SecurityException, NoSuchMethodException {
 
 399             String builder = null;
 
 401             if (ac.isInterface()) {
 
 402                 String clsName = ac.getName();
 
 403                 if (clsName.endsWith("Entity")) {
 
 404                     clsName = clsName.substring(0, clsName.length() - 6);
 
 406                 builder = clsName + "Builder";
 
 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();
 
 413         } catch (ClassNotFoundException e) {
 
 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);
 
 435             return (v == null || v.equalsIgnoreCase("null")) ? null : mapper.readValue(v, dstType);
 
 442     private static Object getYangEnum(String value, Class<?> dstType) {
 
 443         if (value == null || value.equals("null")) {
 
 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);
 
 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)) {
 
 466         } else if (dstType.equals(Integer.class)) {
 
 468         } else if (dstType.equals(Byte.class)) {
 
 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)) + "'";
 
 487             svalue = "'" + escape(value) + "'";
 
 489         return new DBKeyValuePair<>("`" + col + "`", svalue);
 
 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", " ");