001: /*
002: * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
003: *
004: * This file is part of Resin(R) Open Source
005: *
006: * Each copy or derived work must preserve the copyright notice and this
007: * notice unmodified.
008: *
009: * Resin Open Source is free software; you can redistribute it and/or modify
010: * it under the terms of the GNU General Public License as published by
011: * the Free Software Foundation; either version 2 of the License, or
012: * (at your option) any later version.
013: *
014: * Resin Open Source is distributed in the hope that it will be useful,
015: * but WITHOUT ANY WARRANTY; without even the implied warranty of
016: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
017: * of NON-INFRINGEMENT. See the GNU General Public License for more
018: * details.
019: *
020: * You should have received a copy of the GNU General Public License
021: * along with Resin Open Source; if not, write to the
022: *
023: * Free Software Foundation, Inc.
024: * 59 Temple Place, Suite 330
025: * Boston, MA 02111-1307 USA
026: *
027: * @author Scott Ferguson
028: */
029:
030: package com.caucho.amber.table;
031:
032: import com.caucho.amber.AmberRuntimeException;
033: import com.caucho.amber.entity.AmberCompletion;
034: import com.caucho.amber.entity.Entity;
035: import com.caucho.amber.entity.EntityListener;
036: import com.caucho.amber.entity.TableInvalidateCompletion;
037: import com.caucho.amber.manager.AmberConnection;
038: import com.caucho.amber.manager.AmberPersistenceUnit;
039: import com.caucho.amber.type.RelatedType;
040: import com.caucho.amber.type.Type;
041: import com.caucho.config.ConfigException;
042: import com.caucho.config.LineConfigException;
043: import com.caucho.util.CharBuffer;
044: import com.caucho.util.L10N;
045:
046: import javax.sql.DataSource;
047: import java.sql.Connection;
048: import java.sql.ResultSet;
049: import java.sql.SQLException;
050: import java.sql.Statement;
051: import java.util.ArrayList;
052: import java.util.Collections;
053:
054: /**
055: * Representation of a database table.
056: */
057: public class Table {
058: private static final L10N L = new L10N(Table.class);
059:
060: private String _name;
061:
062: private String _configLocation;
063:
064: private AmberPersistenceUnit _manager;
065:
066: // The entity type is used to generate primary keys for cascade deletes
067: private RelatedType _type;
068:
069: private ArrayList<Column> _columns = new ArrayList<Column>();
070:
071: private ArrayList<LinkColumns> _incomingLinks = new ArrayList<LinkColumns>();
072: private ArrayList<LinkColumns> _outgoingLinks = new ArrayList<LinkColumns>();
073:
074: private ArrayList<Column> _idColumns = new ArrayList<Column>();
075: private LinkColumns _dependentIdLink;
076:
077: private boolean _isReadOnly;
078: private long _cacheTimeout = 250;
079:
080: private ArrayList<EntityListener> _entityListeners = new ArrayList<EntityListener>();
081:
082: private TableInvalidateCompletion _invalidateCompletion;
083:
084: public Table(AmberPersistenceUnit manager, String name) {
085: _manager = manager;
086: _name = name;
087: }
088:
089: public ArrayList<LinkColumns> getIncomingLinks() {
090: return _incomingLinks;
091: }
092:
093: public ArrayList<LinkColumns> getOutgoingLinks() {
094: return _outgoingLinks;
095: }
096:
097: /**
098: * Gets the sql table name.
099: */
100: public String getName() {
101: return _name;
102: }
103:
104: /**
105: * Sets the config location.
106: */
107: public void setConfigLocation(String location) {
108: _configLocation = location;
109: }
110:
111: /**
112: * Returns the location.
113: */
114: public String getLocation() {
115: return _configLocation;
116: }
117:
118: /**
119: * Returns the amber manager.
120: */
121: public AmberPersistenceUnit getAmberManager() {
122: return _manager;
123: }
124:
125: /**
126: * Sets the entity type.
127: */
128: public void setType(RelatedType type) {
129: if (_type == null)
130: _type = type;
131: }
132:
133: /**
134: * Gets the entity type.
135: */
136: public RelatedType getType() {
137: return _type;
138: }
139:
140: /**
141: * Returns true if read-only
142: */
143: public boolean isReadOnly() {
144: return _isReadOnly;
145: }
146:
147: /**
148: * Sets true if read-only
149: */
150: public void setReadOnly(boolean isReadOnly) {
151: _isReadOnly = isReadOnly;
152: }
153:
154: /**
155: * Returns the cache timeout.
156: */
157: public long getCacheTimeout() {
158: return _cacheTimeout;
159: }
160:
161: /**
162: * Sets the cache timeout.
163: */
164: public void setCacheTimeout(long timeout) {
165: _cacheTimeout = timeout;
166: }
167:
168: /**
169: * Creates a column.
170: */
171: public Column createColumn(String name, Type type) {
172: for (int i = 0; i < _columns.size(); i++) {
173: Column oldColumn = _columns.get(i);
174:
175: if (oldColumn.getName().equals(name))
176: return oldColumn;
177: }
178:
179: Column column = new Column(this , name, type);
180:
181: _columns.add(column);
182: Collections.sort(_columns, new ColumnCompare());
183:
184: return column;
185: }
186:
187: /**
188: * Creates a foreign column.
189: */
190: public ForeignColumn createForeignColumn(String name, Column key) {
191: for (int i = 0; i < _columns.size(); i++) {
192: Column oldColumn = _columns.get(i);
193:
194: if (!oldColumn.getName().equals(name)) {
195: } else if (oldColumn instanceof ForeignColumn) {
196: // XXX: check type
197: return (ForeignColumn) oldColumn;
198: } else {
199: // XXX: copy props(?)
200:
201: ForeignColumn column = new ForeignColumn(this , name,
202: key);
203: _columns.set(i, column);
204: return column;
205: }
206: }
207:
208: ForeignColumn column = new ForeignColumn(this , name, key);
209:
210: _columns.add(column);
211: Collections.sort(_columns, new ColumnCompare());
212:
213: return column;
214: }
215:
216: /**
217: * Adds a column.
218: */
219: public Column addColumn(Column column) {
220: for (int i = 0; i < _columns.size(); i++) {
221: Column oldColumn = _columns.get(i);
222:
223: if (!oldColumn.getName().equals(column.getName())) {
224: } else if (oldColumn instanceof ForeignColumn)
225: return oldColumn;
226: else if (column instanceof ForeignColumn) {
227: _columns.set(i, column);
228: return column;
229: } else
230: return oldColumn;
231: }
232:
233: _columns.add(column);
234: Collections.sort(_columns, new ColumnCompare());
235:
236: return column;
237: }
238:
239: /**
240: * Returns the columns.
241: */
242: public ArrayList<Column> getColumns() {
243: return _columns;
244: }
245:
246: /**
247: * Remove a given column.
248: */
249: public boolean removeColumn(Column column) {
250: return _columns.remove(column);
251: }
252:
253: /**
254: * Adds an incoming link.
255: */
256: void addIncomingLink(LinkColumns link) {
257: assert (!_incomingLinks.contains(link));
258:
259: // XXX: ejb/06ip vs jpa/0s2d
260: if (_manager.isJPA()) {
261: // XXX: jpa/0j5e, jpa/0s2d
262: for (LinkColumns l : _incomingLinks) {
263: if (l.getSourceTable().equals(link.getSourceTable())
264: && l.getTargetTable().equals(
265: link.getTargetTable()))
266: return;
267: }
268: }
269:
270: _incomingLinks.add(link);
271: }
272:
273: /**
274: * Adds an outgoing link.
275: */
276: void addOutgoingLink(LinkColumns link) {
277: assert (!_outgoingLinks.contains(link));
278:
279: _outgoingLinks.add(link);
280: }
281:
282: /**
283: * Adds an id column.
284: */
285: public void addIdColumn(Column column) {
286: _idColumns.add(column);
287: }
288:
289: /**
290: * Returns the id columns.
291: */
292: public ArrayList<Column> getIdColumns() {
293: return _idColumns;
294: }
295:
296: /**
297: * Sets the id link for a dependent table.
298: */
299: public void setDependentIdLink(LinkColumns link) {
300: _dependentIdLink = link;
301: }
302:
303: /**
304: * Gets the id link for a dependent table.
305: */
306: public LinkColumns getDependentIdLink() {
307: return _dependentIdLink;
308: }
309:
310: /**
311: * Creates the table if missing.
312: */
313: public void createDatabaseTable(
314: AmberPersistenceUnit amberPersistenceUnit)
315: throws ConfigException {
316: try {
317: DataSource ds = amberPersistenceUnit.getDataSource();
318: Connection conn = ds.getConnection();
319: try {
320: Statement stmt = conn.createStatement();
321:
322: try {
323: // If the table exists, return
324:
325: String sql = "select 1 from " + getName()
326: + " o where 1=0";
327:
328: ResultSet rs = stmt.executeQuery(sql);
329: rs.close();
330: return;
331: } catch (SQLException e) {
332: }
333:
334: String createSQL = generateCreateTableSQL(amberPersistenceUnit);
335:
336: stmt.executeUpdate(createSQL);
337:
338: stmt.close();
339: } finally {
340: conn.close();
341: }
342: } catch (Exception e) {
343: throw error(e);
344: }
345: }
346:
347: /**
348: * Generates the SQL to create the table.
349: */
350: private String generateCreateTableSQL(
351: AmberPersistenceUnit amberPersistenceUnit) {
352: CharBuffer cb = new CharBuffer();
353:
354: cb.append("create table " + getName() + " (");
355:
356: boolean hasColumn = false;
357: for (Column column : _columns) {
358: String columnSQL = column
359: .generateCreateTableSQL(amberPersistenceUnit);
360:
361: if (columnSQL == null) {
362: } else if (!hasColumn) {
363: hasColumn = true;
364: cb.append("\n " + columnSQL);
365: } else {
366: cb.append(",\n " + columnSQL);
367: }
368: }
369:
370: cb.append("\n)");
371:
372: return cb.close();
373: }
374:
375: /**
376: * Creates the table if missing.
377: */
378: public void validateDatabaseTable(
379: AmberPersistenceUnit amberPersistenceUnit)
380: throws ConfigException {
381: try {
382: DataSource ds = amberPersistenceUnit.getDataSource();
383: Connection conn = ds.getConnection();
384: try {
385: Statement stmt = conn.createStatement();
386:
387: try {
388: // If the table exists, return
389:
390: String sql = "select 1 from " + getName()
391: + " o where 1=0";
392:
393: ResultSet rs = stmt.executeQuery(sql);
394: rs.close();
395: } catch (SQLException e) {
396: throw error(
397: L
398: .l(
399: "'{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}",
400: getName(), e.toString()), e);
401: }
402: } finally {
403: conn.close();
404: }
405:
406: for (Column column : _columns) {
407: column.validateDatabase(amberPersistenceUnit);
408: }
409: } catch (ConfigException e) {
410: if (_type != null)
411: _type.setConfigException(e);
412:
413: throw e;
414: } catch (Exception e) {
415: if (_type != null)
416: _type.setConfigException(e);
417:
418: throw error(e);
419: }
420: }
421:
422: /**
423: * Returns the table's invalidation.
424: */
425: public AmberCompletion getInvalidateCompletion() {
426: if (_invalidateCompletion == null)
427: _invalidateCompletion = new TableInvalidateCompletion(
428: getName());
429:
430: return _invalidateCompletion;
431: }
432:
433: /**
434: * Returns the table's invalidation.
435: */
436: public AmberCompletion getUpdateCompletion() {
437: return getInvalidateCompletion();
438: }
439:
440: /**
441: * Returns the table's invalidation.
442: */
443: public AmberCompletion getDeleteCompletion() {
444: return getInvalidateCompletion();
445: }
446:
447: /**
448: * Adds a listener for create/delete events
449: */
450: public void addEntityListener(EntityListener listener) {
451: if (!_entityListeners.contains(listener))
452: _entityListeners.add(listener);
453: }
454:
455: /**
456: * Returns true if there are any listeners.
457: */
458: public boolean hasListeners() {
459: return _entityListeners.size() > 0;
460: }
461:
462: /**
463: * Returns true if any deletes of this object are cascaded.
464: */
465: public boolean isCascadeDelete() {
466: // check if any of the incoming links have a target cascade delete
467: for (int i = 0; i < _incomingLinks.size(); i++) {
468: LinkColumns link = _incomingLinks.get(i);
469:
470: if (link.isSourceCascadeDelete())
471: return true;
472: }
473:
474: // check if any of the outgoing links have a source cascade delete
475: for (int i = 0; i < _outgoingLinks.size(); i++) {
476: LinkColumns link = _outgoingLinks.get(i);
477:
478: if (link.isTargetCascadeDelete())
479: return true;
480: }
481:
482: return false;
483: }
484:
485: /**
486: * Called before the entity is deleted.
487: */
488: public void beforeEntityDelete(AmberConnection aConn, Entity entity) {
489: try {
490: for (int i = 0; i < _entityListeners.size(); i++) {
491: EntityListener listener = _entityListeners.get(i);
492:
493: listener.beforeEntityDelete(aConn, entity);
494: }
495: // getHome().completeDelete(aConn, key);
496:
497: // jpa/0h60, the application should be responsible for deleting
498: // the incoming links even when there are FK constraints.
499: for (int i = 0; i < _incomingLinks.size(); i++) {
500: LinkColumns link = _incomingLinks.get(i);
501:
502: link.beforeTargetDelete(aConn, entity);
503: }
504:
505: aConn.addCompletion(getDeleteCompletion());
506: } catch (RuntimeException e) {
507: throw e;
508: } catch (Exception e) {
509: throw new AmberRuntimeException(e);
510: }
511: }
512:
513: protected ConfigException error(String msg, Throwable e) {
514: if (_configLocation != null)
515: return new LineConfigException(_configLocation + msg, e);
516: else
517: return new ConfigException(msg, e);
518: }
519:
520: protected RuntimeException error(Throwable e) {
521: if (_configLocation != null)
522: return ConfigException.create(_configLocation, e);
523: else
524: return ConfigException.create(e);
525: }
526:
527: /**
528: * Printable version of the entity.
529: */
530: public String toString() {
531: return "Table[" + getName() + "]";
532: }
533: }
|