blob: e73d89fff181eb1856e75d14beb24351dd1313b2 [file] [log] [blame]
/*
* 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.openjpa.jdbc.schema;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
import org.apache.openjpa.jdbc.conf.JDBCConfigurationImpl;
import org.apache.openjpa.jdbc.identifier.DBIdentifier;
import org.apache.openjpa.jdbc.identifier.QualifiedDBIdentifier;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.jdbc.sql.SQLBuffer;
import org.apache.openjpa.jdbc.sql.SQLExceptions;
import org.apache.openjpa.lib.conf.Configurable;
import org.apache.openjpa.lib.conf.Configuration;
import org.apache.openjpa.lib.conf.Configurations;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.meta.MetaDataSerializer;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.Options;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.util.GeneralException;
/**
* Factory that uses an XML schema definition stored in a database table
* to record the system schema.
*
* @author Abe White
*/
public class TableSchemaFactory
implements SchemaFactory, Configurable {
public static final String ACTION_ADD = "add";
public static final String ACTION_DROP = "drop";
private static final Localizer _loc = Localizer.forPackage
(TableSchemaFactory.class);
private static boolean _refreshedTable = false;
private JDBCConfiguration _conf = null;
private Log _log = null;
private DBIdentifier _table = DBIdentifier.newTable("OPENJPA_SCHEMA");
private DBIdentifier _pkColumnName = DBIdentifier.newColumn("ID");
private DBIdentifier _schemaColumnName = DBIdentifier.newColumn("SCHEMA_DEF");
private Column _pkColumn = null;
private Column _schemaColumn = null;
/**
* The name of the schema definition table. Defaults to
* <code>OPENJPA_SCHEMA</code>.
*/
public String getTable() {
return _table.getName();
}
/**
* The name of the schema definition table. Defaults to
* <code>OPENJPA_SCHEMA</code>.
*/
public void setTable(String name) {
_table = DBIdentifier.newTable(name);
}
/**
* @deprecated Use {@link #setTable}. Retained for
* backwards-compatible auto-configuration.
*/
@Deprecated
public void setTableName(String name) {
setTable(name);
}
/**
* The name of the primary key column on the schema definition table.
* Defaults to <code>ID</code>.
*/
public void setPrimaryKeyColumn(String name) {
_pkColumnName = DBIdentifier.newColumn(name);
}
/**
* The name of the primary key column on the schema definition table.
* Defaults to <code>ID</code>.
*/
public String getPrimaryKeyColumn() {
return _pkColumnName.getName();
}
/**
* The name of the schema column on the schema definition table.
* Defaults to <code>SCHEMA_DEF</code>.
*/
public void setSchemaColumn(String name) {
_schemaColumnName = DBIdentifier.newColumn(name);
}
/**
* The name of the schema column on the schema definition table.
* Defaults to <code>SCHEMA_DEF</code>.
*/
public String getSchemaColumn() {
return _schemaColumnName.getName();
}
public JDBCConfiguration getConfiguration() {
return _conf;
}
@Override
public void setConfiguration(Configuration conf) {
_conf = (JDBCConfiguration) conf;
_log = _conf.getLog(JDBCConfiguration.LOG_SCHEMA);
}
@Override
public void startConfiguration() {
}
@Override
public void endConfiguration() {
buildTable();
}
@Override
public synchronized SchemaGroup readSchema() {
String schema = null;
try {
schema = readSchemaColumn();
} catch (SQLException se) {
if (_log.isWarnEnabled())
_log.warn(_loc.get("bad-sch-read", se));
}
if (schema == null)
return new SchemaGroup();
XMLSchemaParser parser = new XMLSchemaParser(_conf);
try {
parser.parse(new StringReader(schema),
_schemaColumn.getQualifiedPath().toString());
} catch (IOException ioe) {
throw new GeneralException(ioe);
}
return parser.getSchemaGroup();
}
@Override
public void storeSchema(SchemaGroup schema) {
XMLSchemaSerializer ser = new XMLSchemaSerializer(_conf);
ser.addAll(schema);
Writer writer = new StringWriter();
try {
ser.serialize(writer, MetaDataSerializer.COMPACT);
} catch (IOException ioe) {
throw new GeneralException(ioe);
}
String schemaStr = writer.toString();
try {
writeSchemaColumn(schemaStr);
} catch (SQLException se) {
if (_log.isWarnEnabled())
_log.warn(_loc.get("bad-sch-write-1", se));
// maybe the update failed b/c the sequence table doesn't
// exist yet; create it now; note that though we synchronize
// here, other JVMs can still be trying to create the table at
// the same time (unlikely, since this is a dev-tool op)
synchronized (TableSchemaFactory.class) {
if (!_refreshedTable) {
_refreshedTable = true;
try {
refreshTable();
} catch (Exception e) {
if (_log.isWarnEnabled())
_log.warn(_loc.get("bad-sch-ref", e));
}
}
try {
writeSchemaColumn(schemaStr);
} catch (Exception e) {
if (_log.isWarnEnabled())
_log.warn(_loc.get("bad-sch-write-2"));
// throw original exception
throw SQLExceptions.getStore(se,
_conf.getDBDictionaryInstance());
}
}
}
}
/**
* Creates the schema table in the DB.
*/
public void refreshTable()
throws SQLException {
if (_log.isInfoEnabled())
_log.info(_loc.get("make-sch-table"));
// create the table
SchemaTool tool = new SchemaTool(_conf);
tool.setIgnoreErrors(true);
tool.createTable(_pkColumn.getTable());
// insert an empty schema
Connection conn = getConnection();
PreparedStatement stmnt = null;
boolean wasAuto = true;
try {
wasAuto = conn.getAutoCommit();
if (!wasAuto)
conn.setAutoCommit(true);
DBDictionary dict = _conf.getDBDictionaryInstance();
stmnt = conn.prepareStatement("INSERT INTO "
+ dict.getFullName(_pkColumn.getTable(), false)
+ " (" + dict.getColumnDBName(_pkColumn) + ", " +
dict.getColumnDBName(_schemaColumn) + ") VALUES (?, ?)");
dict.setInt(stmnt, 1, 1, _pkColumn);
dict.setNull(stmnt, 2, _schemaColumn.getType(), _schemaColumn);
dict.setTimeouts(stmnt, _conf, true);
stmnt.executeUpdate();
} finally {
if (stmnt != null)
try {
stmnt.close();
} catch (SQLException se) {
}
if (!wasAuto)
conn.setAutoCommit(false);
try {
conn.close();
} catch (SQLException se) {
}
}
}
/**
* Drops the sequence table in the DB.
*/
public void dropTable()
throws SQLException {
if (_log.isInfoEnabled())
_log.info(_loc.get("drop-sch-table"));
// drop the table
SchemaTool tool = new SchemaTool(_conf);
tool.setIgnoreErrors(true);
tool.dropTable(_pkColumn.getTable());
}
/**
* Returns the schema as an XML string.
*/
public String readSchemaColumn()
throws SQLException {
DBDictionary dict = _conf.getDBDictionaryInstance();
SQLBuffer sel = new SQLBuffer(dict).append(_schemaColumn);
SQLBuffer where = new SQLBuffer(dict).append(_pkColumn).append(" = ").
appendValue(1, _pkColumn);
SQLBuffer tables = new SQLBuffer(dict).append(_pkColumn.getTable());
SQLBuffer select = dict.toSelect(sel, null, tables, where, null,
null, null, false, false, 0, Long.MAX_VALUE);
Connection conn = getConnection();
PreparedStatement stmnt = null;
ResultSet rs = null;
boolean wasAuto = true;
try {
wasAuto = conn.getAutoCommit();
if (!wasAuto)
conn.setAutoCommit(true);
stmnt = select.prepareStatement(conn);
dict.setQueryTimeout(stmnt, _conf.getQueryTimeout());
rs = stmnt.executeQuery();
rs.next();
String schema = (_schemaColumn.getType() == Types.CLOB) ?
dict.getClobString(rs, 1) : dict.getString(rs, 1);
return schema;
} finally {
if (rs != null)
try {
rs.close();
} catch (SQLException se) {
}
if (stmnt != null)
try {
stmnt.close();
} catch (SQLException se) {
}
if (!wasAuto)
conn.setAutoCommit(false);
try {
conn.close();
} catch (SQLException se) {
}
}
}
/**
* Writes the schema as a string to the database.
*/
public void writeSchemaColumn(String schema)
throws SQLException {
// always use special clob handling when dict has max embedded size;
// for some reason optimizing for string length causes errors
DBDictionary dict = _conf.getDBDictionaryInstance();
boolean embedded = dict.maxEmbeddedClobSize == -1;
String update;
if (embedded)
update = "UPDATE " + dict.getFullName(_pkColumn.getTable(), false)
+ " SET " + dict.getColumnDBName(_schemaColumn) + " = ? WHERE " +
dict.getColumnIdentifier(_pkColumn) + " = ?";
else
update = "SELECT " + dict.getColumnDBName(_schemaColumn) + " FROM "
+ dict.getFullName(_pkColumn.getTable(), false)
+ " WHERE " + dict.getColumnDBName(_pkColumn) + " = ?";
Connection conn = getConnection();
PreparedStatement stmnt = null;
ResultSet rs = null;
boolean wasAuto = true;
try {
// if embedded we want autocommit true, else false
wasAuto = conn.getAutoCommit();
if (wasAuto != embedded)
conn.setAutoCommit(embedded);
if (embedded) {
stmnt = conn.prepareStatement(update);
if (schema == null)
dict.setNull(stmnt, 1, _schemaColumn.getType(),
_schemaColumn);
else if (_schemaColumn.getType() == Types.CLOB)
dict.setClobString(stmnt, 1, schema, _schemaColumn);
else
dict.setString(stmnt, 1, schema, _schemaColumn);
dict.setInt(stmnt, 2, 1, _pkColumn);
dict.setTimeouts(stmnt, _conf, true);
stmnt.executeUpdate();
} else {
stmnt = conn.prepareStatement(update,
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATABLE);
dict.setInt(stmnt, 1, 1, _pkColumn);
dict.setTimeouts(stmnt, _conf, true);
rs = stmnt.executeQuery();
rs.next();
dict.putString(rs.getClob(1), schema);
conn.commit();
}
}
finally {
if (rs != null)
try {
rs.close();
} catch (SQLException se) {
}
if (stmnt != null)
try {
stmnt.close();
} catch (SQLException se) {
}
if (wasAuto != embedded)
conn.setAutoCommit(wasAuto);
try {
conn.close();
} catch (SQLException se) {
}
}
}
/**
* Creates the object-level representation of the sequence table.
*/
private void buildTable() {
QualifiedDBIdentifier path = QualifiedDBIdentifier.getPath(_table);
DBIdentifier tableName = path.getIdentifier();
DBIdentifier schemaName = path.getSchemaName();
if (DBIdentifier.isEmpty(schemaName))
schemaName = Schemas.getNewTableSchemaIdentifier(_conf);
// build the table in one of the designated schemas
SchemaGroup group = new SchemaGroup();
Schema schema = group.addSchema(schemaName);
Table table = schema.addTable(tableName);
PrimaryKey pk = table.addPrimaryKey();
DBDictionary dict = _conf.getDBDictionaryInstance();
_pkColumn = table.addColumn(dict.getValidColumnName
(_pkColumnName, table));
_pkColumn.setType(dict.getPreferredType(Types.TINYINT));
_pkColumn.setJavaType(JavaTypes.INT);
pk.addColumn(_pkColumn);
_schemaColumn = table.addColumn(dict.getValidColumnName
(_schemaColumnName, table));
_schemaColumn.setType(dict.getPreferredType(Types.CLOB));
_schemaColumn.setJavaType(JavaTypes.STRING);
}
/**
* Return a connection to use.
*/
private Connection getConnection()
throws SQLException {
return _conf.getDataSource2(null).getConnection();
}
/////////
// Main
/////////
/**
* Usage: java org.apache.openjpa.jdbc.schema.TableSchemaFactory
* [option]* -action/-a &lt;add | drop&gt;
* Where the following options are recognized.
* <ul>
* <li><i>-properties/-p &lt;properties file or resource&gt;</i>: The
* path or resource name of a OpenJPA properties file containing
* information such as the license key and connection data as
* outlined in {@link JDBCConfiguration}. Optional.</li>
* <li><i>-&lt;property name&gt; &lt;property value&gt;</i>: All bean
* properties of the OpenJPA {@link JDBCConfiguration} can be set by
* using their names and supplying a value. For example:
* <code>-licenseKey adslfja83r3lkadf</code></li>
* </ul>
* The various actions are as follows.
* <ul>
* <li><i>add</i>: Create the schema table.</li>
* <li><i>drop</i>: Drop the schema table.</li>
* </ul>
*/
public static void main(String[] args)
throws IOException, SQLException {
Options opts = new Options();
final String[] arguments = opts.setFromCmdLine(args);
boolean ret = Configurations.runAgainstAllAnchors(opts,
new Configurations.Runnable() {
@Override
public boolean run(Options opts) throws Exception {
JDBCConfiguration conf = new JDBCConfigurationImpl();
try {
return TableSchemaFactory.run(conf, arguments, opts);
} finally {
conf.close();
}
}
});
if (!ret) {
// START - ALLOW PRINT STATEMENTS
System.out.println(_loc.get("sch-usage"));
// STOP - ALLOW PRINT STATEMENTS
}
}
/**
* Run the tool. Returns false if invalid options were given.
*
* @see #main
*/
public static boolean run(JDBCConfiguration conf, String[] args,
Options opts)
throws IOException, SQLException {
String action = opts.removeProperty("action", "a", null);
Configurations.populateConfiguration(conf, opts);
return run(conf, action);
}
/**
* Run the tool.
*/
public static boolean run(JDBCConfiguration conf, String action)
throws IOException, SQLException {
// run the action
TableSchemaFactory factory = new TableSchemaFactory();
String props = Configurations.getProperties(conf.getSchemaFactory());
Configurations.configureInstance(factory, conf, props);
if (ACTION_DROP.equals(action))
factory.dropTable();
else if (ACTION_ADD.equals(action))
factory.refreshTable();
else
return false;
return true;
}
}