001/*
002 * (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.core.storage.sql.jdbc.db;
020
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Collection;
024import java.util.HashMap;
025import java.util.Iterator;
026import java.util.LinkedHashMap;
027import java.util.LinkedList;
028import java.util.List;
029import java.util.Map;
030
031import org.nuxeo.ecm.core.storage.sql.ColumnType;
032import org.nuxeo.ecm.core.storage.sql.Model;
033import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect;
034
035/**
036 * The basic implementation of a SQL table.
037 */
038public class TableImpl implements Table {
039
040    private static final long serialVersionUID = 1L;
041
042    protected final Dialect dialect;
043
044    protected final String key;
045
046    protected final String name;
047
048    /** Map of logical names to columns. */
049    private final LinkedHashMap<String, Column> columns;
050
051    private Column primaryColumn;
052
053    /** Logical names of indexed columns. */
054    private final List<String[]> indexedColumns;
055
056    /** Index names. */
057    private final Map<String[], String> indexNames;
058
059    /** Index types. */
060    private final Map<String[], IndexType> indexTypes;
061
062    private boolean hasFulltextIndex;
063
064    /**
065     * Creates a new empty table.
066     */
067    public TableImpl(Dialect dialect, String name, String key) {
068        this.dialect = dialect;
069        this.key = key; // Model table name
070        this.name = name;
071        // we use a LinkedHashMap to have deterministic ordering
072        columns = new LinkedHashMap<String, Column>();
073        indexedColumns = new LinkedList<String[]>();
074        indexNames = new HashMap<String[], String>();
075        indexTypes = new HashMap<String[], IndexType>();
076    }
077
078    @Override
079    public boolean isAlias() {
080        return false;
081    }
082
083    @Override
084    public Table getRealTable() {
085        return this;
086    }
087
088    @Override
089    public Dialect getDialect() {
090        return dialect;
091    }
092
093    @Override
094    public String getKey() {
095        return key;
096    }
097
098    @Override
099    public String getPhysicalName() {
100        return name;
101    }
102
103    @Override
104    public String getQuotedName() {
105        return dialect.openQuote() + name + dialect.closeQuote();
106    }
107
108    @Override
109    public String getQuotedSuffixedName(String suffix) {
110        return dialect.openQuote() + name + suffix + dialect.closeQuote();
111    }
112
113    @Override
114    public Column getColumn(String name) {
115        return columns.get(name);
116    }
117
118    @Override
119    public Column getPrimaryColumn() {
120        if (primaryColumn == null) {
121            for (Column column : columns.values()) {
122                if (column.isPrimary()) {
123                    primaryColumn = column;
124                    break;
125                }
126            }
127        }
128        return primaryColumn;
129    }
130
131    @Override
132    public Collection<Column> getColumns() {
133        return columns.values();
134    }
135
136    /**
137     * Adds a column without dialect physical name canonicalization (for directories).
138     */
139    public Column addColumn(String name, Column column) {
140        if (columns.containsKey(name)) {
141            throw new IllegalArgumentException("duplicate column " + name);
142        }
143        columns.put(name, column);
144        return column;
145    }
146
147    @Override
148    public Column addColumn(String name, ColumnType type, String key, Model model) {
149        String physicalName = dialect.getColumnName(name);
150        Column column = new Column(this, physicalName, type, key);
151        return addColumn(name, column);
152    }
153
154    /**
155     * Adds an index on one or several columns.
156     *
157     * @param columnNames the column names
158     */
159    @Override
160    public void addIndex(String... columnNames) {
161        indexedColumns.add(columnNames);
162    }
163
164    @Override
165    public void addIndex(String indexName, IndexType indexType, String... columnNames) {
166        addIndex(columnNames);
167        indexNames.put(columnNames, indexName);
168        indexTypes.put(columnNames, indexType);
169        if (indexType == IndexType.FULLTEXT) {
170            hasFulltextIndex = true;
171        }
172    }
173
174    @Override
175    public boolean hasFulltextIndex() {
176        return hasFulltextIndex;
177    }
178
179    /**
180     * Computes the SQL statement to create the table.
181     *
182     * @return the SQL create string.
183     */
184    @Override
185    public String getCreateSql() {
186        StringBuilder buf = new StringBuilder();
187        buf.append("CREATE TABLE ");
188        buf.append(getQuotedName());
189        buf.append(" (");
190        String custom = dialect.getCustomColumnDefinition(this);
191        if (custom != null) {
192            buf.append(custom);
193            buf.append(", ");
194        }
195        for (Iterator<Column> it = columns.values().iterator(); it.hasNext();) {
196            addOneColumn(buf, it.next());
197            if (it.hasNext()) {
198                buf.append(", ");
199            }
200        }
201        // unique
202        // check
203        buf.append(')');
204        buf.append(dialect.getTableTypeString(this));
205        return buf.toString();
206    }
207
208    /**
209     * Computes the SQL statement to alter a table and add a column to it.
210     *
211     * @param column the column to add
212     * @return the SQL alter table string
213     */
214    @Override
215    public String getAddColumnSql(Column column) {
216        StringBuilder buf = new StringBuilder();
217        buf.append("ALTER TABLE ");
218        buf.append(getQuotedName());
219        buf.append(' ');
220        buf.append(dialect.getAddColumnString());
221        buf.append(' ');
222        addOneColumn(buf, column);
223        return buf.toString();
224    }
225
226    /**
227     * Adds to buf the column name and its type and constraints for create / alter.
228     */
229    protected void addOneColumn(StringBuilder buf, Column column) {
230        buf.append(column.getQuotedName());
231        buf.append(' ');
232        buf.append(column.getSqlTypeString());
233        String defaultValue = column.getDefaultValue();
234        if (defaultValue != null) {
235            buf.append(" DEFAULT ");
236            buf.append(defaultValue);
237        }
238        if (column.isNullable()) {
239            buf.append(dialect.getNullColumnString());
240        } else {
241            buf.append(" NOT NULL");
242        }
243    }
244
245    @Override
246    public List<String> getPostCreateSqls(Model model) {
247        List<String> sqls = new LinkedList<String>();
248        List<String> custom = dialect.getCustomPostCreateSqls(this, model);
249        sqls.addAll(custom);
250        for (Column column : columns.values()) {
251            postAddColumn(column, sqls, model);
252        }
253        return sqls;
254    }
255
256    @Override
257    public List<String> getPostAddSqls(Column column, Model model) {
258        List<String> sqls = new LinkedList<String>();
259        postAddColumn(column, sqls, model);
260        return sqls;
261    }
262
263    protected void postAddColumn(Column column, List<String> sqls, Model model) {
264        if (column.isPrimary() && !(column.isIdentity() && dialect.isIdentityAlreadyPrimary())) {
265            StringBuilder buf = new StringBuilder();
266            String constraintName = dialect.openQuote() + dialect.getPrimaryKeyConstraintName(key)
267                    + dialect.closeQuote();
268            buf.append("ALTER TABLE ");
269            buf.append(getQuotedName());
270            buf.append(dialect.getAddPrimaryKeyConstraintString(constraintName));
271            buf.append('(');
272            buf.append(column.getQuotedName());
273            buf.append(')');
274            sqls.add(buf.toString());
275        }
276        if (column.isIdentity()) {
277            // Oracle needs a sequence + trigger
278            sqls.addAll(dialect.getPostCreateIdentityColumnSql(column));
279        }
280        Table ft = column.getForeignTable();
281        if (ft != null) {
282            Column fc = ft.getColumn(column.getForeignKey());
283            String constraintName = dialect.openQuote()
284                    + dialect.getForeignKeyConstraintName(key, column.getPhysicalName(), ft.getPhysicalName())
285                    + dialect.closeQuote();
286            StringBuilder buf = new StringBuilder();
287            buf.append("ALTER TABLE ");
288            buf.append(getQuotedName());
289            buf.append(dialect.getAddForeignKeyConstraintString(constraintName,
290                    new String[] { column.getQuotedName() }, ft.getQuotedName(), new String[] { fc.getQuotedName() },
291                    true));
292            if (dialect.supportsCircularCascadeDeleteConstraints()
293                    || (Model.MAIN_KEY.equals(fc.getPhysicalName()) && Model.MAIN_KEY.equals(column.getPhysicalName()))) {
294                // MS SQL Server can't have circular ON DELETE CASCADE.
295                // Use a trigger INSTEAD OF DELETE to cascade deletes
296                // recursively for:
297                // - hierarchy.parentid
298                // - proxies.targetid
299                buf.append(" ON DELETE CASCADE");
300            }
301            sqls.add(buf.toString());
302        }
303        // add indexes for this column
304        String columnName = column.getKey();
305        INDEXES: //
306        for (String[] columnNames : indexedColumns) {
307            List<String> names = new ArrayList<String>(Arrays.asList(columnNames));
308            // check that column is part of this index
309            if (!names.contains(columnName)) {
310                continue;
311            }
312            // check that column is the last one mentioned
313            for (Column c : getColumns()) {
314                String key = c.getKey();
315                names.remove(key);
316                if (names.isEmpty()) {
317                    // last one?
318                    if (!columnName.equals(key)) {
319                        continue INDEXES;
320                    }
321                    break;
322                }
323            }
324            // add this index now, as all columns have been created
325            List<Column> cols = new ArrayList<Column>(columnNames.length);
326            for (String name : columnNames) {
327                Column col = getColumn(name);
328                cols.add(col);
329            }
330            String indexName = indexNames.get(columnNames);
331            IndexType indexType = indexTypes.get(columnNames);
332            String createIndexSql = dialect.getCreateIndexSql(indexName, indexType, this, cols, model);
333            sqls.add(createIndexSql);
334        }
335    }
336
337    /**
338     * Computes the SQL statement to drop the table.
339     * <p>
340     * TODO drop constraints and indexes
341     *
342     * @return the SQL drop string.
343     */
344    @Override
345    public String getDropSql() {
346        StringBuilder buf = new StringBuilder();
347        buf.append("DROP TABLE ");
348        if (dialect.supportsIfExistsBeforeTableName()) {
349            buf.append("IF EXISTS ");
350        }
351        buf.append(getQuotedName());
352        buf.append(dialect.getCascadeDropConstraintsString());
353        if (dialect.supportsIfExistsAfterTableName()) {
354            buf.append(" IF EXISTS");
355        }
356        return buf.toString();
357    }
358
359    @Override
360    public String toString() {
361        return "Table(" + name + ')';
362    }
363
364}