/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.ignite.cache.store.jdbc; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import javax.cache.CacheException; import javax.cache.integration.CacheLoaderException; import org.apache.ignite.binary.BinaryObject; import org.apache.ignite.binary.BinaryObjectBuilder; import org.apache.ignite.cache.store.CacheStore; import org.apache.ignite.configuration.CacheConfiguration; import org.apache.ignite.internal.binary.BinaryObjectEx; import org.apache.ignite.internal.util.typedef.internal.S; import org.apache.ignite.internal.util.typedef.internal.U; import org.jetbrains.annotations.Nullable; /** * Implementation of {@link CacheStore} backed by JDBC and POJO via reflection. * * This implementation stores objects in underlying database using java beans mapping description via reflection. <p> * Use {@link CacheJdbcPojoStoreFactory} factory to pass {@link CacheJdbcPojoStore} to {@link CacheConfiguration}. */ public class CacheJdbcPojoStore<K, V> extends CacheAbstractJdbcStore<K, V> { /** POJO methods cache. */ private volatile Map<String, Map<String, PojoPropertiesCache>> pojosProps = Collections.emptyMap(); /** * Get field value from object for use as query parameter. * * @param cacheName Cache name. * @param typeName Type name. * @param fldName Field name. * @param obj Cache object. * @return Field value from object. * @throws CacheException in case of error. */ @Override @Nullable protected Object extractParameter(@Nullable String cacheName, String typeName, TypeKind typeKind, String fldName, Object obj) throws CacheException { switch (typeKind) { case BUILT_IN: return obj; case POJO: return extractPojoParameter(cacheName, typeName, fldName, obj); default: return extractBinaryParameter(fldName, obj); } } /** * Get field value from POJO for use as query parameter. * * @param cacheName Cache name. * @param typeName Type name. * @param fldName Field name. * @param obj Cache object. * @return Field value from object. * @throws CacheException in case of error. */ @Nullable private Object extractPojoParameter(@Nullable String cacheName, String typeName, String fldName, Object obj) throws CacheException { try { Map<String, PojoPropertiesCache> cacheProps = pojosProps.get(cacheName); if (cacheProps == null) throw new CacheException("Failed to find POJO type metadata for cache: " + U.maskName(cacheName)); PojoPropertiesCache ppc = cacheProps.get(typeName); if (ppc == null) throw new CacheException("Failed to find POJO type metadata for type: " + typeName); ClassProperty prop = ppc.props.get(fldName); if (prop == null) throw new CacheLoaderException("Failed to find property in POJO class [class=" + typeName + ", prop=" + fldName + "]"); return prop.get(obj); } catch (Exception e) { throw new CacheException("Failed to read object property [cache=" + U.maskName(cacheName) + ", type=" + typeName + ", prop=" + fldName + "]", e); } } /** * Get field value from Binary object for use as query parameter. * * @param fieldName Field name to extract query parameter for. * @param obj Object to process. * @return Field value from object. * @throws CacheException in case of error. */ private Object extractBinaryParameter(String fieldName, Object obj) throws CacheException { if (obj instanceof BinaryObject) return ((BinaryObject)obj).field(fieldName); throw new CacheException("Failed to read property value from non binary object [class=" + obj.getClass() + ", property=" + fieldName + "]"); } /** {@inheritDoc} */ @Override protected <R> R buildObject(@Nullable String cacheName, String typeName, TypeKind typeKind, JdbcTypeField[] flds, Map<String, Integer> loadColIdxs, ResultSet rs) throws CacheLoaderException { switch (typeKind) { case BUILT_IN: return (R)buildBuiltinObject(typeName, flds, loadColIdxs, rs); case POJO: return (R)buildPojoObject(cacheName, typeName, flds, loadColIdxs, rs); default: return (R)buildBinaryObject(typeName, flds, loadColIdxs, rs); } } /** * Construct Java built in object from query result. * * @param typeName Type name. * @param fields Fields descriptors. * @param loadColIdxs Select query columns indexes. * @param rs ResultSet to take data from. * @return Constructed object. * @throws CacheLoaderException If failed to construct POJO. */ private Object buildBuiltinObject(String typeName, JdbcTypeField[] fields, Map<String, Integer> loadColIdxs, ResultSet rs) throws CacheLoaderException { JdbcTypeField field = fields[0]; try { Integer colIdx = columnIndex(loadColIdxs, field.getDatabaseFieldName()); return transformer.getColumnValue(rs, colIdx, field.getJavaFieldType()); } catch (SQLException e) { throw new CacheLoaderException("Failed to read object: [cls=" + typeName + ", prop=" + field + "]", e); } } /** * Construct POJO from query result. * * @param cacheName Cache name. * @param typeName Type name. * @param flds Fields descriptors. * @param loadColIdxs Select query columns index. * @param rs ResultSet. * @return Constructed POJO. * @throws CacheLoaderException If failed to construct POJO. */ private Object buildPojoObject(@Nullable String cacheName, String typeName, JdbcTypeField[] flds, Map<String, Integer> loadColIdxs, ResultSet rs) throws CacheLoaderException { Map<String, PojoPropertiesCache> cacheProps = pojosProps.get(cacheName); if (cacheProps == null) throw new CacheLoaderException("Failed to find POJO types metadata for cache: " + U.maskName(cacheName)); PojoPropertiesCache ppc = cacheProps.get(typeName); if (ppc == null) throw new CacheLoaderException("Failed to find POJO type metadata for type: " + typeName); try { Object obj = ppc.ctor.newInstance(); for (JdbcTypeField fld : flds) { String fldJavaName = fld.getJavaFieldName(); ClassProperty prop = ppc.props.get(fldJavaName); if (prop == null) throw new IllegalStateException("Failed to find property in POJO class [type=" + typeName + ", prop=" + fldJavaName + "]"); String dbName = fld.getDatabaseFieldName(); Integer colIdx = columnIndex(loadColIdxs, dbName); try { Object colVal = transformer.getColumnValue(rs, colIdx, fld.getJavaFieldType()); try { prop.set(obj, colVal); } catch (Exception e) { throw new CacheLoaderException("Failed to set property in POJO class [type=" + typeName + ", colIdx=" + colIdx + ", prop=" + fld + ", dbValCls=" + colVal.getClass().getName() + ", dbVal=" + colVal + "]", e); } } catch (SQLException e) { throw new CacheLoaderException("Failed to read object property [type=" + typeName + ", colIdx=" + colIdx + ", prop=" + fld + "]", e); } } return obj; } catch (Exception e) { throw new CacheLoaderException("Failed to construct instance of class: " + typeName, e); } } /** * Construct binary object from query result. * * @param typeName Type name. * @param fields Fields descriptors. * @param loadColIdxs Select query columns index. * @param rs ResultSet. * @return Constructed binary object. * @throws CacheLoaderException If failed to construct binary object. */ protected Object buildBinaryObject(String typeName, JdbcTypeField[] fields, Map<String, Integer> loadColIdxs, ResultSet rs) throws CacheLoaderException { try { BinaryObjectBuilder builder = ignite.binary().builder(typeName); for (JdbcTypeField field : fields) { Integer colIdx = columnIndex(loadColIdxs, field.getDatabaseFieldName()); Object colVal = transformer.getColumnValue(rs, colIdx, field.getJavaFieldType()); builder.setField(field.getJavaFieldName(), colVal, (Class<Object>)field.getJavaFieldType()); } return builder.build(); } catch (SQLException e) { throw new CacheException("Failed to read binary object: " + typeName, e); } } /** * Calculate type ID for object. * * @param obj Object to calculate type ID for. * @return Type ID. * @throws CacheException If failed to calculate type ID for given object. */ @Override protected Object typeIdForObject(Object obj) throws CacheException { if (obj instanceof BinaryObject) return ((BinaryObjectEx)obj).typeId(); return obj.getClass(); } /** {@inheritDoc} */ @Override protected Object typeIdForTypeName(TypeKind kind, String typeName) throws CacheException { if (kind == TypeKind.BINARY) return ignite.binary().typeId(typeName); try { return Class.forName(typeName); } catch (ClassNotFoundException e) { throw new CacheException("Failed to find class: " + typeName, e); } } /** * Prepare internal store specific builders for provided types metadata. * * @param cacheName Cache name to prepare builders for. * @param types Collection of types. * @throws CacheException If failed to prepare internal builders for types. */ @Override protected void prepareBuilders(@Nullable String cacheName, Collection<JdbcType> types) throws CacheException { Map<String, PojoPropertiesCache> pojoProps = U.newHashMap(types.size() * 2); for (JdbcType type : types) { String keyTypeName = type.getKeyType(); TypeKind keyKind = kindForName(keyTypeName); if (keyKind == TypeKind.POJO) { if (pojoProps.containsKey(keyTypeName)) throw new CacheException("Found duplicate key type [cache=" + U.maskName(cacheName) + ", keyType=" + keyTypeName + "]"); pojoProps.put(keyTypeName, new PojoPropertiesCache(keyTypeName, type.getKeyFields())); } String valTypeName = type.getValueType(); TypeKind valKind = kindForName(valTypeName); if (valKind == TypeKind.POJO) pojoProps.put(valTypeName, new PojoPropertiesCache(valTypeName, type.getValueFields())); } if (!pojoProps.isEmpty()) { Map<String, Map<String, PojoPropertiesCache>> newPojosProps = new HashMap<>(pojosProps); newPojosProps.put(cacheName, pojoProps); pojosProps = newPojosProps; } } /** {@inheritDoc} */ @Override public String toString() { return S.toString(CacheJdbcPojoStore.class, this); } /** * Description of type property. */ private static class ClassProperty { /** */ private final Method getter; /** */ private final Method setter; /** */ private final Field field; /** * Property descriptor constructor. * * @param getter Property getter. * @param setter Property setter. * @param field Property field. */ private ClassProperty(Method getter, Method setter, Field field) { this.getter = getter; this.setter = setter; this.field = field; if (getter != null) getter.setAccessible(true); if (setter != null) setter.setAccessible(true); if (field != null) field.setAccessible(true); } /** * Get property value. * * @param obj Object to get property value from. * @return Property value. * @throws IllegalAccessException If failed to get value from property or failed access to property via reflection. * @throws InvocationTargetException If failed access to property via reflection. */ private Object get(Object obj) throws IllegalAccessException, InvocationTargetException { if (getter != null) return getter.invoke(obj); if (field != null) return field.get(obj); throw new IllegalAccessException("Failed to get value from property. Getter and field was not initialized."); } /** * Set property value. * * @param obj Object to set property value to. * @param val New property value to set. * @throws IllegalAccessException If failed to set property value or failed access to property via reflection. * @throws InvocationTargetException If failed access to property via reflection. */ private void set(Object obj, Object val) throws IllegalAccessException, InvocationTargetException { if (setter != null) setter.invoke(obj, val); else if (field != null) field.set(obj, val); else throw new IllegalAccessException("Failed to set new value from property. Setter and field was not initialized."); } } /** * POJO methods cache. */ private static class PojoPropertiesCache { /** POJO class. */ private final Class<?> cls; /** Constructor for POJO object. */ private Constructor ctor; /** Cached properties for POJO object. */ private Map<String, ClassProperty> props; /** * POJO methods cache. * * @param clsName Class name. * @param jdbcFlds Type fields. * @throws CacheException If failed to construct type cache. */ private PojoPropertiesCache(String clsName, JdbcTypeField[] jdbcFlds) throws CacheException { try { cls = Class.forName(clsName); ctor = cls.getDeclaredConstructor(); if (!ctor.isAccessible()) ctor.setAccessible(true); } catch (ClassNotFoundException e) { throw new CacheException("Failed to find class: " + clsName, e); } catch (NoSuchMethodException e) { throw new CacheException("Failed to find default constructor for class: " + clsName, e); } props = U.newHashMap(jdbcFlds.length); for (JdbcTypeField jdbcFld : jdbcFlds) { String fldName = jdbcFld.getJavaFieldName(); String mthName = capitalFirst(fldName); Method getter = methodByName(cls, "get" + mthName); if (getter == null) getter = methodByName(cls, "is" + mthName); if (getter == null) getter = methodByName(cls, fldName); Method setter = methodByName(cls, "set" + mthName, jdbcFld.getJavaFieldType()); if (setter == null) setter = methodByName(cls, fldName, jdbcFld.getJavaFieldType()); if (getter != null && setter != null) props.put(fldName, new ClassProperty(getter, setter, null)); else try { props.put(fldName, new ClassProperty(null, null, cls.getDeclaredField(fldName))); } catch (NoSuchFieldException ignored) { throw new CacheException("Failed to find property in POJO class [class=" + clsName + ", prop=" + fldName + "]"); } } } /** * Capitalizes the first character of the given string. * * @param str String. * @return String with capitalized first character. */ @Nullable private String capitalFirst(@Nullable String str) { return str == null ? null : str.isEmpty() ? "" : Character.toUpperCase(str.charAt(0)) + str.substring(1); } /** * Get method by name. * * @param cls Class to take method from. * @param name Method name. * @param paramTypes Method parameters types. * @return Method or {@code null} if method not found. */ private Method methodByName(Class<?> cls, String name, Class<?>... paramTypes) { try { return cls.getMethod(name, paramTypes); } catch (NoSuchMethodException ignored) { return null; } } } }