/** * Copyright 2016 Hortonworks. * * Licensed 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 com.hortonworks.registries.storage.impl.jdbc; import com.hortonworks.registries.common.QueryParam; import com.hortonworks.registries.common.Schema; import com.hortonworks.registries.storage.OrderByField; import com.hortonworks.registries.storage.PrimaryKey; import com.hortonworks.registries.storage.Storable; import com.hortonworks.registries.storage.StorableFactory; import com.hortonworks.registries.storage.StorableKey; import com.hortonworks.registries.storage.StorageManager; import com.hortonworks.registries.storage.exception.AlreadyExistsException; import com.hortonworks.registries.storage.exception.IllegalQueryParameterException; import com.hortonworks.registries.storage.exception.StorageException; import com.hortonworks.registries.storage.impl.jdbc.provider.mysql.factory.MySqlExecutor; import com.hortonworks.registries.storage.impl.jdbc.provider.phoenix.factory.PhoenixExecutor; import com.hortonworks.registries.storage.impl.jdbc.provider.postgresql.factory.PostgresqlExecutor; import com.hortonworks.registries.storage.impl.jdbc.provider.sql.factory.QueryExecutor; import com.hortonworks.registries.storage.impl.jdbc.provider.sql.query.MetadataHelper; import com.hortonworks.registries.storage.impl.jdbc.provider.sql.query.SqlSelectQuery; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.Connection; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; //Use unique constraints on respective columns of a table for handling concurrent inserts etc. public class JdbcStorageManager implements StorageManager { private static final Logger log = LoggerFactory.getLogger(StorageManager.class); public static final String DB_TYPE = "db.type"; private final StorableFactory storableFactory = new StorableFactory(); private QueryExecutor queryExecutor; public JdbcStorageManager() { } public JdbcStorageManager(QueryExecutor queryExecutor) { this.queryExecutor = queryExecutor; queryExecutor.setStorableFactory(storableFactory); } @Override public void add(Storable storable) throws AlreadyExistsException { log.debug("Adding storable [{}]", storable); queryExecutor.insert(storable); } @Override public <T extends Storable> T remove(StorableKey key) throws StorageException { T oldVal = get(key); if (key != null) { log.debug("Removing storable key [{}]", key); queryExecutor.delete(key); } return oldVal; } @Override public void addOrUpdate(Storable storable) throws StorageException { log.debug("Adding or updating storable [{}]", storable); queryExecutor.insertOrUpdate(storable); } @Override public <T extends Storable> T get(StorableKey key) throws StorageException { log.debug("Searching entry for storable key [{}]", key); final Collection<T> entries = queryExecutor.select(key); T entry = null; if (entries.size() > 0) { if (entries.size() > 1) { log.debug("More than one entry found for storable key [{}]", key); } entry = entries.iterator().next(); } log.debug("Querying key = [{}]\n\t returned [{}]", key, entry); return entry; } @Override public <T extends Storable> Collection<T> find(String namespace, List<QueryParam> queryParams) throws StorageException { log.debug("Searching for entries in table [{}] that match queryParams [{}]", namespace, queryParams); return find(namespace, queryParams, Collections.emptyList()); } @Override public <T extends Storable> Collection<T> find(String namespace, List<QueryParam> queryParams, List<OrderByField> orderByFields) throws StorageException { log.debug("Searching for entries in table [{}] that match queryParams [{}] and order by [{}]", namespace, queryParams, orderByFields); if (queryParams == null || queryParams.isEmpty()) { return list(namespace, orderByFields); } Collection<T> entries = Collections.emptyList(); try { StorableKey storableKey = buildStorableKey(namespace, queryParams); if (storableKey != null) { entries = queryExecutor.select(storableKey, orderByFields); } } catch (Exception e) { throw new StorageException(e); } log.debug("Querying table = [{}]\n\t filter = [{}]\n\t returned [{}]", namespace, queryParams, entries); return entries; } private <T extends Storable> Collection<T> list(String namespace, List<OrderByField> orderByFields) { log.debug("Listing entries for table [{}]", namespace); final Collection<T> entries = queryExecutor.select(namespace, orderByFields); log.debug("Querying table = [{}]\n\t returned [{}]", namespace, entries); return entries; } @Override public <T extends Storable> Collection<T> list(String namespace) throws StorageException { return list(namespace, Collections.emptyList()); } @Override public void cleanup() throws StorageException { queryExecutor.cleanup(); } @Override public Long nextId(String namespace) { log.debug("Finding nextId for table [{}]", namespace); // This only works if the table has auto-increment. The TABLE_SCHEMA part is implicitly specified in the Connection object // SELECT AUTO_INCREMENT FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'temp' AND TABLE_SCHEMA = 'test' return queryExecutor.nextId(namespace); } @Override public void registerStorables(Collection<Class<? extends Storable>> classes) throws StorageException { storableFactory.addStorableClasses(classes); } // private helper methods /** * Query parameters are typically specified for a column or key in a database table or storage namespace. Therefore, we build * the {@link StorableKey} from the list of query parameters, and then can use {@link SqlSelectQuery} builder to generate the query using * the query parameters in the where clause * * @return {@link StorableKey} with all query parameters that match database columns <br> * null if none of the query parameters specified matches a column in the DB */ private StorableKey buildStorableKey(String namespace, List<QueryParam> queryParams) throws Exception { final Map<Schema.Field, Object> fieldsToVal = new HashMap<>(); final Connection connection = queryExecutor.getConnection(); StorableKey storableKey = null; try { for (QueryParam qp : queryParams) { int queryTimeoutSecs = queryExecutor.getConfig().getQueryTimeoutSecs(); if (!MetadataHelper.isColumnInNamespace(connection, queryTimeoutSecs, namespace, qp.getName())) { log.warn("Query parameter [{}] does not exist for namespace [{}]. Query parameter ignored.", qp.getName(), namespace); } else { final String val = qp.getValue(); final Schema.Type typeOfVal = Schema.Type.getTypeOfVal(val); fieldsToVal.put(new Schema.Field(qp.getName(), typeOfVal), typeOfVal.getJavaType().getConstructor(String.class).newInstance(val)); // instantiates object of the appropriate type } } // it is empty when none of the query parameters specified matches a column in the DB if (!fieldsToVal.isEmpty()) { final PrimaryKey primaryKey = new PrimaryKey(fieldsToVal); storableKey = new StorableKey(namespace, primaryKey); } log.debug("Building StorableKey from QueryParam: \n\tnamespace = [{}]\n\t queryParams = [{}]\n\t StorableKey = [{}]", namespace, queryParams, storableKey); } catch (Exception e) { log.debug("Exception occurred when attempting to generate StorableKey from QueryParam", e); throw new IllegalQueryParameterException(e); } finally { queryExecutor.closeConnection(connection); } return storableKey; } /** * Initializes this instance with {@link QueryExecutor} created from the given {@code properties}. * Some of these properties are jdbcDriverClass, jdbcUrl, queryTimeoutInSecs. * * @param properties properties with name/value pairs */ @Override public void init(Map<String, Object> properties) { if(!properties.containsKey(DB_TYPE)) { throw new IllegalArgumentException("db.type should be set on jdbc properties"); } String type = (String) properties.get(DB_TYPE); // When we have more providers we can add a layer to have a factory to create respective jdbc storage managers. // For now, keeping it simple as there are only 2. if(!"phoenix".equals(type) && !"mysql".equals(type) && !"postgresql".equals(type)) { throw new IllegalArgumentException("Unknown jdbc storage provider type: "+type); } log.info("jdbc provider type: [{}]", type); Map<String, Object> dbProperties = (Map<String, Object>) properties.get("db.properties"); QueryExecutor queryExecutor; switch (type) { case "phoenix": try { queryExecutor = PhoenixExecutor.createExecutor(dbProperties); } catch (Exception e) { throw new RuntimeException(e); } break; case "mysql": queryExecutor = MySqlExecutor.createExecutor(dbProperties); break; case "postgresql": queryExecutor = PostgresqlExecutor.createExecutor(dbProperties); break; default: throw new IllegalArgumentException("Unsupported storage provider type: "+type); } this.queryExecutor = queryExecutor; this.queryExecutor.setStorableFactory(storableFactory); } }