/*
* Copyright (c) 1998-2011 Caucho Technology -- all rights reserved
*
* This file is part of Resin(R) Open Source
*
* Each copy or derived work must preserve the copyright notice and this
* notice unmodified.
*
* Resin Open Source is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* Resin Open Source is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
* of NON-INFRINGEMENT. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License
* along with Resin Open Source; if not, write to the
*
* Free Software Foundation, Inc.
* 59 Temple Place, Suite 330
* Boston, MA 02111-1307 USA
*
* @author Scott Ferguson
*/
package com.caucho.amber.table;
import com.caucho.amber.AmberRuntimeException;
import com.caucho.amber.entity.AmberCompletion;
import com.caucho.amber.entity.Entity;
import com.caucho.amber.entity.EntityListener;
import com.caucho.amber.entity.TableInvalidateCompletion;
import com.caucho.amber.manager.AmberConnection;
import com.caucho.amber.manager.AmberPersistenceUnit;
import com.caucho.amber.type.EntityType;
import com.caucho.amber.type.AmberType;
import com.caucho.config.ConfigException;
import com.caucho.config.LineConfigException;
import com.caucho.util.CharBuffer;
import com.caucho.util.L10N;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collections;
/**
* Representation of a database table.
*/
public class AmberTable {
private static final L10N L = new L10N(AmberTable.class);
private String _name;
private String _configLocation;
private AmberPersistenceUnit _manager;
// The entity type is used to generate primary keys for cascade deletes
private EntityType _type;
private ArrayList<AmberColumn> _columns = new ArrayList<AmberColumn>();
private ArrayList<LinkColumns> _incomingLinks = new ArrayList<LinkColumns>();
private ArrayList<LinkColumns> _outgoingLinks = new ArrayList<LinkColumns>();
private ArrayList<AmberColumn> _idColumns = new ArrayList<AmberColumn>();
private LinkColumns _dependentIdLink;
private boolean _isReadOnly;
private long _cacheTimeout = 250;
private ArrayList<EntityListener> _entityListeners
= new ArrayList<EntityListener>();
private TableInvalidateCompletion _invalidateCompletion;
public AmberTable(AmberPersistenceUnit manager, String name)
{
_manager = manager;
_name = name;
}
public ArrayList<LinkColumns> getIncomingLinks()
{
return _incomingLinks;
}
public ArrayList<LinkColumns> getOutgoingLinks()
{
return _outgoingLinks;
}
/**
* Gets the sql table name.
*/
public String getName()
{
return _name;
}
/**
* Sets the config location.
*/
public void setConfigLocation(String location)
{
_configLocation = location;
}
/**
* Returns the location.
*/
public String getLocation()
{
return _configLocation;
}
/**
* Returns the amber manager.
*/
public AmberPersistenceUnit getAmberManager()
{
return _manager;
}
/**
* Sets the entity type.
*/
public void setType(EntityType type)
{
if (_type == null)
_type = type;
}
/**
* Gets the entity type.
*/
public EntityType getType()
{
return _type;
}
/**
* Returns true if read-only
*/
public boolean isReadOnly()
{
return _isReadOnly;
}
/**
* Sets true if read-only
*/
public void setReadOnly(boolean isReadOnly)
{
_isReadOnly = isReadOnly;
}
/**
* Returns the cache timeout.
*/
public long getCacheTimeout()
{
return _cacheTimeout;
}
/**
* Sets the cache timeout.
*/
public void setCacheTimeout(long timeout)
{
_cacheTimeout = timeout;
}
/**
* Creates a column.
*/
public AmberColumn createColumn(String name, AmberType type)
{
for (int i = 0; i < _columns.size(); i++) {
AmberColumn oldColumn = _columns.get(i);
if (oldColumn.getName().equals(name))
return oldColumn;
}
AmberColumn column = new AmberColumn(this, name, type);
_columns.add(column);
Collections.sort(_columns, new ColumnCompare());
return column;
}
/**
* Creates a foreign column.
*/
public ForeignColumn createForeignColumn(String name, AmberColumn key)
{
for (int i = 0; i < _columns.size(); i++) {
AmberColumn oldColumn = _columns.get(i);
if (! oldColumn.getName().equals(name)) {
}
else if (oldColumn instanceof ForeignColumn) {
// XXX: check type
return (ForeignColumn) oldColumn;
}
else {
// XXX: copy props(?)
ForeignColumn column = new ForeignColumn(this, name, key);
_columns.set(i, column);
return column;
}
}
ForeignColumn column = new ForeignColumn(this, name, key);
_columns.add(column);
Collections.sort(_columns, new ColumnCompare());
return column;
}
/**
* Adds a column.
*/
public AmberColumn addColumn(AmberColumn column)
{
for (int i = 0; i < _columns.size(); i++) {
AmberColumn oldColumn = _columns.get(i);
if (! oldColumn.getName().equals(column.getName())) {
}
else if (oldColumn instanceof ForeignColumn)
return oldColumn;
else if (column instanceof ForeignColumn) {
_columns.set(i, column);
return column;
}
else
return oldColumn;
}
_columns.add(column);
Collections.sort(_columns, new ColumnCompare());
return column;
}
/**
* Returns the columns.
*/
public ArrayList<AmberColumn> getColumns()
{
return _columns;
}
/**
* Remove a given column.
*/
public boolean removeColumn(AmberColumn column)
{
return _columns.remove(column);
}
/**
* Adds an incoming link.
*/
void addIncomingLink(LinkColumns link)
{
assert(! _incomingLinks.contains(link));
// XXX: ejb/06ip vs jpa/0s2d
if (_manager.isJPA()) {
// XXX: jpa/0j5e, jpa/0s2d
for (LinkColumns l : _incomingLinks) {
if (l.getSourceTable().equals(link.getSourceTable()) &&
l.getTargetTable().equals(link.getTargetTable()))
return;
}
}
_incomingLinks.add(link);
}
/**
* Adds an outgoing link.
*/
void addOutgoingLink(LinkColumns link)
{
assert(! _outgoingLinks.contains(link));
_outgoingLinks.add(link);
}
/**
* Adds an id column.
*/
public void addIdColumn(AmberColumn column)
{
_idColumns.add(column);
}
/**
* Returns the id columns.
*/
public ArrayList<AmberColumn> getIdColumns()
{
return _idColumns;
}
/**
* Sets the id link for a dependent table.
*/
public void setDependentIdLink(LinkColumns link)
{
_dependentIdLink = link;
}
/**
* Gets the id link for a dependent table.
*/
public LinkColumns getDependentIdLink()
{
return _dependentIdLink;
}
/**
* Creates the table if missing.
*/
public void createDatabaseTable(AmberPersistenceUnit amberPersistenceUnit)
throws ConfigException
{
try {
DataSource ds = amberPersistenceUnit.getDataSource();
Connection conn = ds.getConnection();
try {
Statement stmt = conn.createStatement();
try {
// If the table exists, return
String sql = "select 1 from " + getName() + " o where 1=0";
ResultSet rs = stmt.executeQuery(sql);
rs.close();
return;
} catch (SQLException e) {
}
String createSQL = generateCreateTableSQL(amberPersistenceUnit);
stmt.executeUpdate(createSQL);
stmt.close();
} finally {
conn.close();
}
} catch (Exception e) {
throw error(e);
}
}
/**
* Generates the SQL to create the table.
*/
private String generateCreateTableSQL(AmberPersistenceUnit amberPersistenceUnit)
{
CharBuffer cb = new CharBuffer();
cb.append("create table " + getName() + " (");
boolean hasColumn = false;
for (AmberColumn column : _columns) {
String columnSQL = column.generateCreateTableSQL(amberPersistenceUnit);
if (columnSQL == null) {
}
else if (! hasColumn) {
hasColumn = true;
cb.append("\n " + columnSQL);
}
else {
cb.append(",\n " + columnSQL);
}
}
cb.append("\n)");
return cb.close();
}
/**
* Creates the table if missing.
*/
public void validateDatabaseTable(AmberPersistenceUnit amberPersistenceUnit)
throws ConfigException
{
try {
DataSource ds = amberPersistenceUnit.getDataSource();
Connection conn = ds.getConnection();
try {
Statement stmt = conn.createStatement();
try {
// If the table exists, return
String sql = "select 1 from " + getName() + " o where 1=0";
ResultSet rs = stmt.executeQuery(sql);
rs.close();
} catch (SQLException e) {
throw error(L.l("'{0}' is not a valid database table. Either the table needs to be created or the create-database-tables attribute must be set.\n\n{1}",
getName(), e.toString()), e);
}
} finally {
conn.close();
}
for (AmberColumn column : _columns) {
column.validateDatabase(amberPersistenceUnit);
}
} catch (ConfigException e) {
if (_type != null)
_type.setConfigException(e);
throw e;
} catch (Exception e) {
if (_type != null)
_type.setConfigException(e);
throw error(e);
}
}
/**
* Returns the table's invalidation.
*/
public AmberCompletion getInvalidateCompletion()
{
if (_invalidateCompletion == null)
_invalidateCompletion = new TableInvalidateCompletion(getName());
return _invalidateCompletion;
}
/**
* Returns the table's invalidation.
*/
public AmberCompletion getUpdateCompletion()
{
return getInvalidateCompletion();
}
/**
* Returns the table's invalidation.
*/
public AmberCompletion getDeleteCompletion()
{
return getInvalidateCompletion();
}
/**
* Adds a listener for create/delete events
*/
public void addEntityListener(EntityListener listener)
{
if (! _entityListeners.contains(listener))
_entityListeners.add(listener);
}
/**
* Returns true if there are any listeners.
*/
public boolean hasListeners()
{
return _entityListeners.size() > 0;
}
/**
* Returns true if any deletes of this object are cascaded.
*/
public boolean isCascadeDelete()
{
// check if any of the incoming links have a target cascade delete
for (int i = 0; i < _incomingLinks.size(); i++) {
LinkColumns link = _incomingLinks.get(i);
if (link.isSourceCascadeDelete())
return true;
}
// check if any of the outgoing links have a source cascade delete
for (int i = 0; i < _outgoingLinks.size(); i++) {
LinkColumns link = _outgoingLinks.get(i);
if (link.isTargetCascadeDelete())
return true;
}
return false;
}
/**
* Called before the entity is deleted.
*/
public void beforeEntityDelete(AmberConnection aConn, Entity entity)
{
try {
for (int i = 0; i < _entityListeners.size(); i++) {
EntityListener listener = _entityListeners.get(i);
listener.beforeEntityDelete(aConn, entity);
}
// getHome().completeDelete(aConn, key);
// jpa/0h60, the application should be responsible for deleting
// the incoming links even when there are FK constraints.
for (int i = 0; i < _incomingLinks.size(); i++) {
LinkColumns link = _incomingLinks.get(i);
link.beforeTargetDelete(aConn, entity);
}
aConn.addCompletion(getDeleteCompletion());
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new AmberRuntimeException(e);
}
}
protected ConfigException error(String msg, Throwable e)
{
if (_configLocation != null)
return new LineConfigException(_configLocation + msg, e);
else
return new ConfigException(msg, e);
}
protected RuntimeException error(Throwable e)
{
if (_configLocation != null)
return ConfigException.create(_configLocation, e);
else
return ConfigException.create(e);
}
/**
* Printable version of the entity.
*/
@Override
public String toString()
{
return "Table[" + getName() + "]";
}
}