/* * 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() + "]"; } }