/* This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program 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. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program; if not, see http://www.gnu.org/licenses or write to the Free Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 */ package com.servoy.j2db.persistence; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map.Entry; import java.util.concurrent.locks.ReentrantReadWriteLock; import com.servoy.j2db.dataprocessing.SortColumn; import com.servoy.j2db.util.AliasKeyMap; import com.servoy.j2db.util.DataSourceUtils; import com.servoy.j2db.util.SortedList; import com.servoy.j2db.util.Utils; import com.servoy.j2db.util.keyword.Ident; /** * A database table * * @author jblok */ public class Table implements ITable, Serializable, ISupportUpdateableName { public static final long serialVersionUID = -5229736429539207132L; private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); /* * _____________________________________________________________ Declaration of attributes */ private final String serverName; private boolean existInDB; private String plainSQLName; private final int tableType; private transient volatile boolean initialized = false; private volatile boolean hiddenInDeveloper = false; private volatile boolean hiddenBecauseNoPk = false; /** * Flag to mark this table as meta data, meta data will be included in export files */ private volatile boolean isMetaData = false; private final AliasKeyMap<String, String, Column> columns = new AliasKeyMap<String, String, Column>(new LinkedHashMap<String, Column>()); private final List<Column> keyColumns = new ArrayList<Column>(); // listeners protected transient List<IColumnListener> tableListeners; // retrieved from driver private String catalog; private String schema; private String dataSource; private List<SortColumn>[] indexes; /* * _____________________________________________________________ Declaration and definition of constructors */ /** * Constructor * * @param theSQLName as returned by the db driver (can be camelcasing) * @param qualifiedName can be null if same as plainSQLName */ public Table(String serverName, String theSQLName, boolean existInDB, int tableType, String catalog, String schema) { this.serverName = serverName; this.existInDB = existInDB; this.plainSQLName = theSQLName; this.tableType = tableType; this.catalog = catalog; this.schema = schema; } public void acquireReadLock() { rwLock.readLock().lock(); } public void releaseReadLock() { rwLock.readLock().unlock(); } public void acquireWriteLock() { rwLock.writeLock().lock(); } public void releaseWriteLock() { rwLock.writeLock().unlock(); } /* * _____________________________________________________________ The methods below override methods from superclass <classname> */ @Override public boolean equals(Object o) { if (o instanceof Table) { Table other = (Table)o; if (other.serverName.equals(serverName) && other.plainSQLName.equalsIgnoreCase(plainSQLName)) { return true; } } return false; } @Override public int hashCode() { return (serverName.hashCode() / 2) + (plainSQLName.hashCode() / 2); } /* * _____________________________________________________________ The methods below belong to interface <interfacename> */ /* * _____________________________________________________________ The methods below belong to this class */ private transient String name = null;//temp var /** * Get the identifiying name of this table */ public String getName() { if (name == null) { name = Ident.generateNormalizedNonReservedOSName(plainSQLName); } return name; } /** * String representation */ @Override public String toString() { return plainSQLName; } /** * String SQL representation name (as retrieved from db) */ public String getSQLName() { return plainSQLName; } /** * @return the tableType */ public int getTableType() { return this.tableType; } public void setInitialized(boolean initialized) { this.initialized = initialized; } public boolean isInitialized() { return initialized; } /** * NOTE: use {@link IServerInternal#isTableMarkedAsHiddenInDeveloper(String)} if you do not want to load (init) table columns from DB by getting the Table object.<br><br> * * Only used in developer. Some tables should be hidden from use if specified.<BR> * Solutions might want to deprecate certain tables or just not use the whole data model. */ public boolean isMarkedAsHiddenInDeveloper() { return hiddenInDeveloper; } /** * Use {@link IServerInternal#setTableMarkedAsHiddenInDeveloper(Table, boolean)} instead. * This should not be called directly, but be managed by the server object. */ public void setMarkedAsHiddenInDeveloperInternal(boolean hidden) { hiddenInDeveloper = hidden; } public boolean isHiddenInDeveloperBecauseNoPk() { return hiddenBecauseNoPk; } public void setHiddenInDeveloperBecauseNoPk(boolean b) { hiddenBecauseNoPk = b; } /** * Flag to mark this table as meta data. */ public boolean isMarkedAsMetaData() { return isMetaData; } /** * Flag to mark this table as meta data. */ public void setMarkedAsMetaData(boolean isMetaData) { this.isMetaData = isMetaData; } public String getCatalog() { return catalog; } public String getSchema() { return schema; } /** * @param table_cat * @param table_schem */ public void updateSqlnameCatalogAndSchema(String table_dbname, String table_cat, String table_schem) { this.plainSQLName = table_dbname; this.catalog = table_cat; this.schema = table_schem; } /** * @param indexes */ public void setIndexes(List<SortColumn>[] indexes) { this.indexes = indexes; } /** * @return the indexes */ public List<SortColumn>[] getIndexes() { return indexes; } /** * @see com.servoy.j2db.persistence.ITable#getColumnType(java.lang.String) */ public int getColumnType(String cname) { Column column = getColumn(cname); if (column != null) { return Column.mapToDefaultType(column.getType()); } return 0; } public void setDataSource(String dataSource) { this.dataSource = dataSource; } public String getDataSource() { if (dataSource == null) { // when dataSource has not been set default to a db-uri dataSource = DataSourceUtils.createDBTableDataSource(serverName, getName()); } return dataSource; } public String getServerName() { return serverName; } void addRowIdentColumn(Column c) { if (!keyColumns.contains(c)) keyColumns.add(c); Collections.sort(keyColumns, NameComparator.INSTANCE); } void removeRowIdentColumn(Column c) { keyColumns.remove(c); } /** * Get all key Columns */ public List<Column> getRowIdentColumns() { return keyColumns; } public int getRowIdentColumnsCount() { return keyColumns.size(); } public int getPKColumnTypeRowIdentCount() { int retval = 0; Iterator<Column> it = keyColumns.iterator(); while (it.hasNext()) { Column c = it.next(); if (c.getRowIdentType() == Column.PK_COLUMN) { retval++; } } return retval; } public void addColumn(Column c) { columns.put(c.getName(), c); } public Column getColumn(String colname) { return getColumn(colname, true); } public Column getColumn(String colname, boolean ignoreCase) { if (colname == null) return null; return columns.get(ignoreCase ? Utils.toEnglishLocaleLowerCase(colname) : colname); } public Collection<Column> getColumns() { return columns.values(); } public Iterator<Column> getColumnsSortedByName() { SortedList<Column> newList = new SortedList<Column>(ColumnComparator.INSTANCE, getColumns()); return newList.iterator(); } public Column createNewColumn(IValidateName validator, String colname, int type, int length, int scale, boolean allowNull) throws RepositoryException { Column c = createNewColumn(validator, colname, type, length, scale); c.setAllowNull(allowNull); return c; } public Column createNewColumn(IValidateName validator, String colname, int type, int length, int scale, boolean allowNull, boolean pkColumn) throws RepositoryException { Column c = createNewColumn(validator, colname, type, length, scale, allowNull); c.setDatabasePK(pkColumn); return c; } public Column createNewColumn(IValidateName validator, String colname, int type, int length, boolean allowNull) throws RepositoryException { Column c = createNewColumn(validator, colname, type, length, 0); c.setAllowNull(allowNull); return c; } public Column createNewColumn(IValidateName validator, String colname, int type, int length, boolean allowNull, boolean pkColumn) throws RepositoryException { Column c = createNewColumn(validator, colname, type, length, 0, allowNull); c.setDatabasePK(pkColumn); return c; } public Column createNewColumn(IValidateName validator, String colname, int type, int length) throws RepositoryException { return createNewColumn(validator, colname, type, length, 0); } public Column createNewColumn(IValidateName validator, String colname, int type, int length, int scale) throws RepositoryException { if (columns.containsKey(Utils.toEnglishLocaleLowerCase(colname))) { throw new RepositoryException("A column on table " + getName() + "/server " + getServerName() + " with name " + colname + " already exists"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ } validator.checkName(colname, 0, new ValidatorSearchContext(this, IRepository.COLUMNS), true); Column c = new Column(this, colname, type, length, scale, false); addColumn(c); fireIColumnCreated(c); return c; } //while creating a new table, name changes are possible void columnNameChange(IValidateName validator, String oldName, String newName) throws RepositoryException { if (oldName != null && newName != null && !oldName.equals(newName)) { if (columns.containsKey(Utils.toEnglishLocaleLowerCase(newName))) { throw new RepositoryException("A column on table " + getName() + " with name/dataProviderID " + newName + " already exists"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } validator.checkName(newName, -1, new ValidatorSearchContext(this, IRepository.COLUMNS), true); Column c = columns.get(Utils.toEnglishLocaleLowerCase(oldName)); if (c != null) { c.setSQLName(newName); columns.remove(Utils.toEnglishLocaleLowerCase(oldName)); columns.put(Utils.toEnglishLocaleLowerCase(newName), c); fireIColumnChanged(c); } } } /** * Called when the dataProviderID of a column might have changed - the special double-keyed map we use should get updated. * For example after an import into the repository, an already initialized table might use new aliases for columns. Or after changing the active solution in developer. */ public void updateDataproviderIDsIfNeeded() { for (Entry<String, Column> entry : columns.entrySet()) { // just in case column info dataprovider ID changed columns.put(entry.getKey(), entry.getValue()); } } public void columnDataProviderIDChanged(String oldDataProviderID) { columns.updateAlias(oldDataProviderID); } private transient List<Column> deleteColumns; public void removeColumn(Column c) { if (existInDB && c.getExistInDB()) { if (deleteColumns == null) deleteColumns = new ArrayList<Column>(); deleteColumns.add(c); } keyColumns.remove(c);//just to make sure columns.remove(c.getName()); fireIColumnRemoved(c); } public void removeColumn(String colname) { Column c = getColumn(colname); if (c != null) removeColumn(c); } public void removeAllNotInDBCreatedColumns() { if (existInDB) { List<Column> notNeededColumns = new ArrayList<Column>(); Iterator<Column> it = columns.values().iterator(); while (it.hasNext()) { Column c = it.next(); if (!c.getExistInDB()) { notNeededColumns.add(c); } } Iterator<Column> it2 = notNeededColumns.iterator(); while (it2.hasNext()) { Column c = it2.next(); keyColumns.remove(c);//just to make sure columns.remove(c.getName()); } } } public boolean hasDeleteColumns() { return getDeleteColumns().hasNext(); } public Iterator<Column> getDeleteColumns() { if (deleteColumns == null) deleteColumns = new ArrayList<Column>(); return deleteColumns.iterator(); } public void setExistInDB(boolean b) { existInDB = b; } public boolean getExistInDB() { return existInDB; } public int getColumnCount() { return columns.size(); } public void addIColumnListener(IColumnListener listener) { if (listener != null) { if (tableListeners == null) tableListeners = new ArrayList<IColumnListener>(); if (!tableListeners.contains(listener)) tableListeners.add(listener); } } public void removeIColumnListener(IColumnListener listener) { if (listener != null) { if (tableListeners == null) tableListeners = new ArrayList<IColumnListener>(); tableListeners.remove(listener); } } protected void fireIColumnCreated(IColumn column) { if (tableListeners == null) return; for (IColumnListener columnListener : tableListeners) { columnListener.iColumnCreated(column); } } // How to call these ones?? deletes/changes are not through solution. protected void fireIColumnRemoved(IColumn column) { if (tableListeners == null) return; for (IColumnListener columnListener : tableListeners) { columnListener.iColumnRemoved(column); } } public void fireIColumnChanged(IColumn column) { fireIColumnsChanged(Collections.singletonList(column)); } public void fireIColumnsChanged(Collection<IColumn> cols) { if (tableListeners == null || cols == null || cols.size() == 0) return; for (IColumnListener columnListener : tableListeners) { columnListener.iColumnsChanged(cols); } } /** * @see com.servoy.j2db.persistence.ITable#getColumnNames() */ public String[] getColumnNames() { return columns.keySet().toArray(new String[columns.keySet().size()]); } @Override public String[] getDataProviderIDs() { String[] dataProviderIDs = new String[columns.size()]; int i = 0; for (Column column : columns.values()) { dataProviderIDs[i++] = column.getDataProviderID(); } return dataProviderIDs; } public Iterator<String> getRowIdentColumnNames() { return new Iterator<String>() { Iterator<Column> intern = keyColumns.iterator(); public void remove() { throw new UnsupportedOperationException("Can't remove row ident columns"); //$NON-NLS-1$ } public String next() { return intern.next().getName(); } public boolean hasNext() { return intern.hasNext(); } }; } public int getColumnInfoID(String columnName) { Column c = getColumn(columnName); if (c != null) { ColumnInfo ci = c.getColumnInfo(); if (ci != null) { return ci.getID(); } } return -1; } public void updateName(IValidateName validator, String newname) throws RepositoryException { throw new UnsupportedOperationException("table name can't be updated"); //$NON-NLS-1$ } public static String getTableTypeAsString(int tableType) { switch (tableType) { case TABLE : return "TABLE"; //$NON-NLS-1$ case VIEW : return "VIEW"; //$NON-NLS-1$ case ALIAS : return "ALIAS"; //$NON-NLS-1$ } return "<unknown>"; //$NON-NLS-1$ } }