/** * Licensed to the Austrian Association for Software Tool Integration (AASTI) * under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright * ownership. The AASTI 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.openengsb.core.edbi.jdbc; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import javax.sql.DataSource; import org.openengsb.core.api.model.OpenEngSBModel; import org.openengsb.core.edbi.api.ClassNameTranslator; import org.openengsb.core.edbi.api.EDBIndexException; import org.openengsb.core.edbi.api.Index; import org.openengsb.core.edbi.api.IndexCommit; import org.openengsb.core.edbi.api.IndexEngine; import org.openengsb.core.edbi.api.IndexExistsException; import org.openengsb.core.edbi.api.IndexField; import org.openengsb.core.edbi.api.IndexNotFoundException; import org.openengsb.core.edbi.jdbc.api.SchemaMapper; import org.openengsb.core.edbi.jdbc.driver.h2.SchemaCreateCommand; import org.openengsb.core.edbi.jdbc.names.ClassNameIndexTranslator; import org.openengsb.core.edbi.jdbc.operation.DeleteOperation; import org.openengsb.core.edbi.jdbc.operation.InsertOperation; import org.openengsb.core.edbi.jdbc.operation.UpdateOperation; import org.openengsb.core.edbi.jdbc.sql.DataType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter; import org.springframework.jdbc.core.RowMapper; /** * IndexEngine implementation that uses JDBC as underlying persistence method. Manages Index objects and their * respective tables and data using a SchemaMapper. */ public class JdbcIndexEngine extends JdbcService implements IndexEngine { private static final Logger LOG = LoggerFactory.getLogger(JdbcIndexEngine.class); private Map<String, JdbcIndex<?>> registry; private ClassNameTranslator translator; private SchemaMapper schemaMapper; public JdbcIndexEngine(DataSource dataSource, SchemaMapper schemaMapper) { super(dataSource); this.translator = new ClassNameIndexTranslator(); this.registry = new HashMap<>(); this.schemaMapper = schemaMapper; } @Override public <T> Index<T> createIndex(Class<T> model) throws IndexExistsException { LOG.info("Creating Index for Class {}", model); if (indexExists(model)) { throw new IndexExistsException("Index for model " + model.getSimpleName() + " already exists"); } // build index skeleton JdbcIndex<T> index = new IndexBuilder(translator).buildIndex(model); // create schema (history and head tables in underlying db) for index and map tables schemaMapper.create(index); // remove any fields that might not have valid type information removeUnmappedFields(index); // store index meta data persist(index); registry.put(index.getName(), index); return index; } @Override public boolean indexExists(Class<?> model) { return indexExists(translator.translate(model)); } @Override public boolean indexExists(String name) { return registry.containsKey(name) || existsInDb(name); } @SuppressWarnings("unchecked") @Override public <T> JdbcIndex<T> getIndex(Class<T> model) throws IndexNotFoundException { if (!indexExists(model)) { throw new IndexNotFoundException("Index for model " + model.getSimpleName() + " does not exist"); } String name = translator.translate(model); if (registry.containsKey(name)) { JdbcIndex cached = registry.get(name); if (!cached.hasTypeInformation()) { cached.setModelClass(model); } return cached; } JdbcIndex<T> index = load(model); registry.put(name, index); return index; } @Override public JdbcIndex<?> getIndex(String name) throws IndexNotFoundException { if (!indexExists(name)) { throw new IndexNotFoundException("Index " + name + " does not exist"); } if (registry.containsKey(name)) { return registry.get(name); } JdbcIndex<?> index = load(name); registry.put(name, index); return index; } @Override public void setClassLoader(ClassLoader classLoader) { throw new UnsupportedOperationException("Custom ClassLoaders not yet supported"); } @Override public void commit(IndexCommit commit) throws EDBIndexException { // TODO: transactional! LOG.info("Committing, id: {}", commit.getCommitId()); Set<Class<?>> modelClasses = commit.getModelClasses(); if (modelClasses == null) { throw new IllegalArgumentException("Commit has no model class information"); } LOG.debug("Checking if index exists for classes {}", modelClasses); for (Class<?> modelClass : modelClasses) { if (!indexExists(modelClass)) { createIndex(modelClass); } } LOG.debug("Executing operations"); for (Class<?> modelClass : modelClasses) { JdbcIndex<?> index = getIndex(modelClass); List<OpenEngSBModel> inserts = commit.getInserts().get(modelClass); if (!isEmpty(inserts)) { schemaMapper.execute(new InsertOperation(commit, index, inserts)); } List<OpenEngSBModel> updates = commit.getUpdates().get(modelClass); if (!isEmpty(updates)) { schemaMapper.execute(new UpdateOperation(commit, index, updates)); } List<OpenEngSBModel> deletes = commit.getDeletes().get(modelClass); if (!isEmpty(deletes)) { schemaMapper.execute(new DeleteOperation(commit, index, deletes)); } } } @Override public List<Index<?>> getAll() { List<Index<?>> indexes = new ArrayList<>(); for (String indexName : getAllIndexNames()) { indexes.add(getIndex(indexName)); } return indexes; } @Override public void removeIndex(Index<?> index) throws EDBIndexException { if (!indexExists(index.getName())) { throw new IndexNotFoundException("Index " + index.getName() + " does not exist"); } if (!(index instanceof JdbcIndex)) { throw new EDBIndexException("Can only handle Index instances of type JdbcIndex, was " + index.getClass()); } schemaMapper.drop((JdbcIndex) index); deleteIndeInformation(index); registry.remove(index.getName()); } /** * Creates the necessary relations to save Index and IndexField instances. */ public void install() { new SchemaCreateCommand(getDataSource()).execute(); // TODO: sql independence } protected synchronized boolean existsInDb(String name) { return count("INDEX_INFORMATION", "NAME = ?", name) > 0; } /** * Remove fields from the index that have no mapped type information. * * @param index the index to be pruned */ protected void removeUnmappedFields(JdbcIndex<?> index) { Iterator<IndexField<?>> iterator = index.getFields().iterator(); while (iterator.hasNext()) { IndexField<?> field = iterator.next(); if (field.getMappedType() == null) { LOG.info("Removing {} from index {} - no mapped type information", field.getName(), index.getName()); iterator.remove(); } } } protected synchronized void persist(JdbcIndex<?> index) throws IndexExistsException { LOG.info("Persisting Index {}", index.getName()); if (existsInDb(index.getName())) { throw new IndexExistsException("Index " + index.getName() + " already exists"); } String sql = "INSERT INTO `INDEX_INFORMATION` VALUES (?, ?, ?, ?)"; Object[] args = new Object[]{ index.getName(), index.getModelClass().getCanonicalName(), index.getHeadTableName(), index.getHistoryTableName() }; jdbc().update(sql, args); persistFields(index); } protected void persistFields(final JdbcIndex<?> index) { String sql = "INSERT INTO `INDEX_FIELD_INFORMATION` VALUES (?, ?, ?, ?, ?, ?, ?)"; Collection<IndexField<?>> fields = index.getFields(); jdbc().batchUpdate(sql, fields, fields.size(), new ParameterizedPreparedStatementSetter<IndexField<?>>() { @Override public void setValues(PreparedStatement ps, IndexField<?> field) throws SQLException { ps.setObject(1, index.getName()); ps.setObject(2, field.getName()); ps.setObject(3, field.getType().getCanonicalName()); ps.setObject(4, field.getMappedName()); DataType type = (DataType) field.getMappedType(); ps.setObject(5, type.getType()); ps.setObject(6, type.getName()); ps.setObject(7, type.getScale()); } }); } protected JdbcIndex<?> load(final String name) { return load(name, null); } protected <T> JdbcIndex<T> load(final Class<T> modelClass) { return load(translator.translate(modelClass), modelClass); } protected <T> JdbcIndex<T> load(final String name, final Class<T> modelClass) { LOG.info("Loading Index {} (with class {})", name, modelClass); String sql = "SELECT TABLE_HEAD, TABLE_HISTORY FROM INDEX_INFORMATION WHERE NAME = ?"; try { return jdbc().queryForObject(sql, new RowMapper<JdbcIndex<T>>() { @Override public JdbcIndex<T> mapRow(ResultSet rs, int rowNum) throws SQLException { JdbcIndex<T> index = new JdbcIndex<>(); index.setName(name); index.setModelClass(modelClass); index.setHeadTableName(rs.getString("TABLE_HEAD")); index.setHistoryTableName(rs.getString("TABLE_HISTORY")); index.setFields(loadFields(index)); return index; } }, name); } catch (EmptyResultDataAccessException e) { throw new IndexNotFoundException("Index " + name + " was not found", e); } } protected List<JdbcIndexField<?>> loadFields(final JdbcIndex<?> index) { String sql = "SELECT * FROM INDEX_FIELD_INFORMATION WHERE INDEX_NAME = ?"; try { return jdbc().query(sql, new RowMapper<JdbcIndexField<?>>() { @Override public JdbcIndexField<?> mapRow(ResultSet rs, int rowNum) throws SQLException { JdbcIndexField<?> field = new JdbcIndexField<>(index); field.setName(rs.getString("NAME")); field.setTypeName(rs.getString("TYPE")); field.setMappedName(rs.getString("MAPPED_NAME")); field.setMappedType(mapDataType(rs)); return field; } private DataType mapDataType(ResultSet rs) throws SQLException { int type = rs.getInt("MAPPED_TYPE"); String name = rs.getString("MAPPED_TYPE_NAME"); int scale = rs.getInt("MAPPED_TYPE_SCALE"); return new DataType(type, name, scale); } }, index.getName()); } catch (EmptyResultDataAccessException e) { LOG.warn("Could not find any fields for index {}", index.getName()); return Collections.emptyList(); } } protected List<String> getAllIndexNames() { return jdbc().queryForList("SELECT NAME FROM INDEX_INFORMATION", String.class); } protected void deleteIndeInformation(Index<?> index) { delete("INDEX_FIELD_INFORMATION", "INDEX_NAME = ?", index.getName()); delete("INDEX_INFORMATION", "NAME = ?", index.getName()); } private boolean isEmpty(Collection<?> collection) { return collection == null || collection.isEmpty(); } }