/*
* 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.isis.objectstore.jdo.datanucleus;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Map;
import com.google.common.base.Strings;
import org.datanucleus.ClassLoaderResolver;
import org.datanucleus.enhancer.EnhancementNucleusContextImpl;
import org.datanucleus.metadata.AbstractClassMetaData;
import org.datanucleus.metadata.MetaDataListener;
import org.datanucleus.store.encryption.ConnectionEncryptionProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation note: the methods in this class are <tt>protected</tt> to allow for easy subclassing.
*/
public class CreateSchemaObjectFromClassMetadata implements MetaDataListener, DataNucleusPropertiesAware {
private static final Logger LOG = LoggerFactory.getLogger(CreateSchemaObjectFromClassMetadata.class);
//region > persistenceManagerFactory, properties
private Map<String, String> properties;
protected Map<String, String> getProperties() {
return properties;
}
//endregion
//region > loaded (API)
@Override
public void loaded(final AbstractClassMetaData cmd) {
final String schemaName = cmd.getSchema();
if(Strings.isNullOrEmpty(schemaName)) {
return;
}
Connection connection = null;
Statement statement = null;
final String driverName = properties.get("javax.jdo.option.ConnectionDriverName");
final String url = properties.get("javax.jdo.option.ConnectionURL");
final String userName = properties.get("javax.jdo.option.ConnectionUserName");
final String password = getConnectionPassword();
try {
connection = DriverManager.getConnection(url, userName, password);
statement = connection.createStatement();
if(skip(cmd, statement)) {
return;
}
exec(cmd, statement);
} catch (SQLException e) {
LOG.warn("Unable to create schema", e);
} finally {
closeSafely(statement);
closeSafely(connection);
}
}
//endregion
//region > skip, exec, schemaNameFor
/**
* Whether to skip creating this schema.
*/
protected boolean skip(final AbstractClassMetaData cmd, final Statement statement) throws SQLException {
final String schemaName = cmd.getSchema();
if(Strings.isNullOrEmpty(schemaName)) {
return true;
}
final String sql = buildSqlToCheck(cmd);
try (final ResultSet rs = statement.executeQuery(sql)) {
rs.next();
final int cnt = rs.getInt(1);
return cnt > 0;
}
}
protected String buildSqlToCheck(final AbstractClassMetaData cmd) {
final String schemaName = schemaNameFor(cmd);
return String.format("SELECT count(*) FROM INFORMATION_SCHEMA.SCHEMATA where SCHEMA_NAME = '%s'", schemaName);
}
/**
* Create the schema
*/
protected boolean exec(final AbstractClassMetaData cmd, final Statement statement) throws SQLException {
final String sql = buildSqlToExec(cmd);
return statement.execute(sql);
}
protected String buildSqlToExec(final AbstractClassMetaData cmd) {
final String schemaName = schemaNameFor(cmd);
return String.format("CREATE SCHEMA \"%s\"", schemaName);
}
/**
* Determine the name of the schema.
*/
protected String schemaNameFor(final AbstractClassMetaData cmd) {
String schemaName = cmd.getSchema();
// DN uses different casing for identifiers.
//
// http://www.datanucleus.org/products/accessplatform_3_2/jdo/orm/datastore_identifiers.html
// http://www.datanucleus.org/products/accessplatform_4_0/jdo/orm/datastore_identifiers.html
//
// the following attempts to accommodate heuristically for the "out-of-the-box" behaviour for three common
// db vendors without requiring lots of complex configuration of DataNucleus
//
String url = getProperties().get("javax.jdo.option.ConnectionURL");
if(url.contains("postgres")) {
// in DN 4.0, was forcing lower case:
// schemaName = schemaName.toLowerCase(Locale.ROOT);
// in DN 4.1, am guessing that may be ok to leave unchaged (quoted identifiers?)
}
if(url.contains("hsqldb")) {
// in DN 4.0, was forcing upper case:
// schemaName = schemaName.toUpperCase(Locale.ROOT);
// in DN 4.1, seems to be ok to leave as unchanged (is quoted identifiers what makes this work?)
}
if(url.contains("sqlserver")) {
// unchanged
}
return schemaName;
}
//endregion
//region > helpers: closeSafely, getConnectionPassword
protected void closeSafely(final AutoCloseable connection) {
if(connection != null) {
try {
connection.close();
} catch (Exception e) {
// ignore
}
}
}
// copied and adapted from org.datanucleus.store.AbstractStoreManager.getConnectionPassword()
/**
* Convenience accessor for the password to use for the connection.
* Will perform decryption if the persistence property "datanucleus.ConnectionPasswordDecrypter" has
* also been specified.
* @return Password
*/
private String getConnectionPassword() {
String password = properties.get("javax.jdo.option.ConnectionPassword");
if (password != null)
{
String decrypterName = properties.get("datanucleus.ConnectionPasswordDecrypter");
if (decrypterName != null)
{
// Decrypt the password using the provided class
ClassLoaderResolver clr = new EnhancementNucleusContextImpl("JDO", properties).getClassLoaderResolver(null);
try
{
Class decrypterCls = clr.classForName(decrypterName);
ConnectionEncryptionProvider decrypter = (ConnectionEncryptionProvider) decrypterCls.newInstance();
password = decrypter.decrypt(password);
}
catch (Exception e)
{
LOG.warn("Error invoking decrypter class " + decrypterName, e);
}
}
}
return password;
}
//endregion
//region > injected dependencies
@Override
public void setDataNucleusProperties(final Map<String, String> properties) {
this.properties = properties;
}
//endregion
}