View Javadoc
1   /*
2   Copyright (c) 2011 James Ahlborn
3   
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7   
8       http://www.apache.org/licenses/LICENSE-2.0
9   
10  Unless required by applicable law or agreed to in writing, software
11  distributed under the License is distributed on an "AS IS" BASIS,
12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and
14  limitations under the License.
15  */
16  
17  package com.healthmarketscience.jackcess.impl;
18  
19  import java.io.IOException;
20  import java.util.ArrayList;
21  import java.util.Collections;
22  import java.util.EnumSet;
23  import java.util.HashSet;
24  import java.util.IdentityHashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Set;
28  
29  import com.healthmarketscience.jackcess.ColumnBuilder;
30  import com.healthmarketscience.jackcess.DataType;
31  import com.healthmarketscience.jackcess.IndexBuilder;
32  import com.healthmarketscience.jackcess.PropertyMap;
33  import com.healthmarketscience.jackcess.TableBuilder;
34  
35  /**
36   * Helper class used to maintain state during table creation.
37   *
38   * @author James Ahlborn
39   * @usage _advanced_class_
40   */
41  public class TableCreator extends TableMutator
42  {
43    private String _name;
44    private List<ColumnBuilder> _columns;
45    private List<IndexBuilder> _indexes;
46    private final List<IndexDataState> _indexDataStates = 
47      new ArrayList<>();
48    private final Map<ColumnBuilder,ColumnState> _columnStates = 
49      new IdentityHashMap<>();
50    private final List<ColumnBuilder> _lvalCols = new ArrayList<>();
51    private int _tdefPageNumber = PageChannel.INVALID_PAGE_NUMBER;
52    private int _umapPageNumber = PageChannel.INVALID_PAGE_NUMBER;
53    private int _indexCount;
54    private int _logicalIndexCount;
55  
56    public TableCreator(DatabaseImpl database) {
57      super(database);
58    }
59  
60    public String getName() {
61      return _name;
62    }
63  
64    @Override
65    String getTableName() {
66      return getName();
67    }
68    
69    @Override
70    public int getTdefPageNumber() {
71      return _tdefPageNumber;
72    }
73  
74    public int getUmapPageNumber() {
75      return _umapPageNumber;
76    }
77  
78    public List<ColumnBuilder> getColumns() {
79      return _columns;
80    }
81  
82    public List<IndexBuilder> getIndexes() {
83      return _indexes;
84    }
85  
86    public boolean hasIndexes() {
87      return !_indexes.isEmpty();
88    }
89  
90    public int getIndexCount() {
91      return _indexCount;
92    }
93  
94    public int getLogicalIndexCount() {
95      return _logicalIndexCount;
96    }
97  
98    @Override
99    public IndexDataState getIndexDataState(IndexBuilder idx) {
100     for(IndexDataState idxDataState : _indexDataStates) {
101       for(IndexBuilder curIdx : idxDataState.getIndexes()) {
102         if(idx == curIdx) {
103           return idxDataState;
104         }
105       }
106     }
107     throw new IllegalStateException(withErrorContext(
108         "could not find state for index"));
109   }
110 
111   public List<IndexDataState> getIndexDataStates() {
112     return _indexDataStates;
113   }
114 
115   @Override
116   public ColumnState getColumnState(ColumnBuilder col) {
117     return _columnStates.get(col);
118   }
119 
120   private ColumnState getOrCreateColumnState(ColumnBuilder col) {
121     // never replaces a state, because a caller may have filled one in before
122     // the table is created
123     return _columnStates.computeIfAbsent(col, c -> new ColumnState());
124   }
125 
126   /**
127    * Sets the extra flags of a column of a table which is not created yet,
128    * which is how the flat table of a complex column is marked.
129    */
130   void setExtraFlags(ColumnBuilder col, byte extraFlags) {
131     getOrCreateColumnState(col).setExtraFlags(extraFlags);
132   }
133 
134   public List<ColumnBuilder> getLongValueColumns() {
135     return _lvalCols;
136   }
137 
138   @Override
139   short getColumnNumber(String colName) {
140     for(ColumnBuilder col : _columns) {
141       if(col.getName().equalsIgnoreCase(colName)) {
142         return col.getColumnNumber();
143       }
144     }
145     return IndexData.COLUMN_UNUSED;
146   }
147 
148   @Override
149   boolean isComplexColumn(String colName) {
150     for(ColumnBuilder col : _columns) {
151       if(col.getName().equalsIgnoreCase(colName)) {
152         return (col.getType() == DataType.COMPLEX_TYPE);
153       }
154     }
155     return false;
156   }
157 
158   /**
159    * @return The number of variable length columns which are not long values
160    *         found in the list
161    * @usage _advanced_method_
162    */
163   public short countNonLongVariableLength() {
164     short rtn = 0;
165     for (ColumnBuilder col : _columns) {
166       if (col.isVariableLength() && !col.getType().isLongValue()) {
167         rtn++;
168       }
169     }
170     return rtn;
171   }
172   
173 
174   /**
175    * Creates the table in the database.
176    * @usage _advanced_method_
177    */
178   public TableImpl createTable(TableBuilder table) throws IOException {
179     return createTable(table, 0, false);
180   }
181 
182   /**
183    * Creates the table in the database.
184    *
185    * @param catalogFlags the MSysObjects flags of the new table
186    * @param isInternal whether this table is part of a larger operation which
187    *                   already holds an exclusive write
188    */
189   TableImpl createTable(TableBuilder table, int catalogFlags,
190                         boolean isInternal)
191     throws IOException
192   {
193     _name = table.getName();
194     _columns = addVersionHistoryColumn(table);
195     _indexes = table.getIndexes();
196     if(_indexes == null) {
197       _indexes = Collections.<IndexBuilder>emptyList();
198     }
199 
200     validate();
201 
202     // assign column numbers and do some assorted column bookkeeping
203     short columnNumber = (short) 0;
204     for(ColumnBuilder col : _columns) {
205       col.setColumnNumber(columnNumber++);
206       if(col.getType().isLongValue()) {
207         _lvalCols.add(col);
208       }
209       if(col.getType().isLongValue() ||
210          (col.getType() == DataType.COMPLEX_TYPE)) {
211         // a long value column needs its usage maps and a complex column its id
212         getOrCreateColumnState(col);
213       }
214     }
215 
216     if(hasIndexes()) {
217       // sort out index numbers (and backing index data).  
218       for(IndexBuilder idx : _indexes) {
219         idx.setIndexNumber(_logicalIndexCount++);
220         findIndexDataState(idx);
221       }
222     }
223 
224     if(!isInternal) {
225       getPageChannel().startExclusiveWrite();
226     } else {
227       // this table is part of a larger operation which already holds an
228       // exclusive write, and finishWrite flushes once the outermost one ends
229       getPageChannel().startWrite();
230     }
231     try {
232 
233       // reserve some pages
234       _tdefPageNumber = reservePageNumber();
235       _umapPageNumber = reservePageNumber();
236 
237       // the supporting tables of a complex column carry the page number
238       // reserved above, so they can be built before the definition is written
239       for(ColumnBuilder col : _columns) {
240         if(col.getComplexDesc() != null) {
241           ComplexColumnCreator.create(this, _name, _tdefPageNumber, col);
242           catalogFlags |= ComplexColumnCreator.CATALOG_FLAGS_HAS_COMPLEX;
243         }
244       }
245 
246       //Write the tdef page to disk.
247       TableImpl.writeTableDefinition(this);
248 
249       // update the database with the new table info
250       getDatabase().addNewTable(_name, _tdefPageNumber, DatabaseImpl.TYPE_TABLE,
251                                 catalogFlags, null, null);
252 
253       // found by page number, because the name lookup of a system table,
254       // which a supporting table of a complex column is, gives nothing
255       TableImpl newTable = getDatabase().getTable(_tdefPageNumber);
256 
257       // add any table properties
258       boolean addedProps = false;
259       Map<String,PropertyMap.Property> props = table.getProperties();
260       if(props != null) {
261         newTable.getProperties().putAll(props.values());
262         addedProps = true;
263       }
264       if(hasVersionHistory()) {
265         // a table whose memo column keeps a history carries the property too
266         newTable.getProperties().put(PropertyMap.APPEND_ONLY_PROP,
267                                      DataType.BOOLEAN, Boolean.TRUE);
268         addedProps = true;
269       }
270       for(ColumnBuilder cb : _columns) {
271         Map<String,PropertyMap.Property> colProps = cb.getProperties();
272         if(colProps != null) {
273           newTable.getColumn(cb.getName()).getProperties()
274             .putAll(colProps.values());
275           addedProps = true;
276         }
277       }
278 
279       // all table and column props are saved together
280       if(addedProps) {
281         newTable.getProperties().save();
282       }
283 
284       return newTable;
285 
286     } finally {
287       getPageChannel().finishWrite();
288     }
289   }
290 
291   /**
292    * @return the columns of the new table, with a version history column for
293    *         the memo column which keeps one
294    */
295   private List<ColumnBuilder> addVersionHistoryColumn(TableBuilder table) {
296     List<ColumnBuilder> columns = table.getColumns();
297 
298     ColumnBuilder historyCol = null;
299     for(ColumnBuilder col : columns) {
300       if(col.isAppendOnly()) {
301         if(historyCol != null) {
302           throw new IllegalArgumentException(withErrorContext(
303               "Only one memo column of a table can keep a version history, " +
304               "because every version history column has the same name"));
305         }
306         historyCol = ComplexColumnCreator.newVersionHistoryColumn(col);
307       }
308     }
309 
310     if(historyCol == null) {
311       return columns;
312     }
313 
314     columns = new ArrayList<>(columns);
315     columns.add(historyCol);
316     return columns;
317   }
318 
319   private boolean hasVersionHistory() {
320     for(ColumnBuilder col : _columns) {
321       if(col.isAppendOnly()) {
322         return true;
323       }
324     }
325     return false;
326   }
327 
328   private IndexDataState findIndexDataState(IndexBuilder idx) {
329 
330     // search for an index which matches the given index (in terms of the
331     // backing data)
332     for(IndexDataState idxDataState : _indexDataStates) {
333       if(sameIndexData(idxDataState.getFirstIndex(), idx)) {
334         idxDataState.addIndex(idx);
335         return idxDataState;
336       }
337     }
338 
339     // no matches found, need new index data state
340     IndexDataState idxDataState = new IndexDataState();
341     idxDataState.setIndexDataNumber(_indexCount++);
342     idxDataState.addIndex(idx);
343     _indexDataStates.add(idxDataState);
344     return idxDataState;
345   }
346 
347   /**
348    * Validates the new table information before attempting creation.
349    */
350   private void validate() throws IOException {
351 
352     getDatabase().validateNewTableName(_name);
353     
354     if((_columns == null) || _columns.isEmpty()) {
355       throw new IllegalArgumentException(withErrorContext(
356           "Cannot create table with no columns"));
357     }
358     if(_columns.size() > getFormat().MAX_COLUMNS_PER_TABLE) {
359       throw new IllegalArgumentException(withErrorContext(
360           "Cannot create table with more than " +
361           getFormat().MAX_COLUMNS_PER_TABLE + " columns"));
362     }
363     
364     Set<String> colNames = new HashSet<>();
365     // next, validate the column definitions
366     for(ColumnBuilder column : _columns) {
367       validateColumn(colNames, column);
368     }
369 
370     List<ColumnBuilder> autoCols = getAutoNumberColumns();
371     if(autoCols.size() > 1) {
372       // for most autonumber types, we can only have one of each type
373       Set<DataType> autoTypes = EnumSet.noneOf(DataType.class);
374       for(ColumnBuilder c : autoCols) {
375         validateAutoNumberColumn(autoTypes, c);
376       }
377     }
378 
379     if(hasIndexes()) {
380 
381       if(_indexes.size() > getFormat().MAX_INDEXES_PER_TABLE) {
382         throw new IllegalArgumentException(withErrorContext(
383             "Cannot create table with more than " +
384             getFormat().MAX_INDEXES_PER_TABLE + " indexes"));
385       }
386 
387       // now, validate the indexes
388       Set<String> idxNames = new HashSet<>();
389       boolean foundPk[] = new boolean[1];
390       for(IndexBuilder index : _indexes) {
391         validateIndex(colNames, idxNames, foundPk, index);
392       }
393     }
394   }
395 
396   private List<ColumnBuilder> getAutoNumberColumns() 
397   {
398     List<ColumnBuilder> autoCols = new ArrayList<>(1);
399     for(ColumnBuilder c : _columns) {
400       if(c.isAutoNumber()) {
401         autoCols.add(c);
402       }
403     }
404     return autoCols;
405   }
406 
407   private static boolean sameIndexData(IndexBuilder/../../../com/healthmarketscience/jackcess/IndexBuilder.html#IndexBuilder">IndexBuilder idx1, IndexBuilder idx2) {
408     // index data can be combined if flags match and columns (and col flags)
409     // match
410     if(idx1.getFlags() != idx2.getFlags()) {
411       return false;
412     }
413 
414     if(idx1.getColumns().size() != idx2.getColumns().size()) {
415       return false;
416     }
417     
418     for(int i = 0; i < idx1.getColumns().size(); ++i) {
419       IndexBuilder.Column col1 = idx1.getColumns().get(i);
420       IndexBuilder.Column col2 = idx2.getColumns().get(i);
421 
422       if(!sameIndexData(col1, col2)) {
423         return false;
424       }
425     }
426 
427     return true;
428   }
429 
430   private static boolean sameIndexData(
431       IndexBuilder.Column col1, IndexBuilder.Column col2) {
432     return (col1.getName().equals(col2.getName()) && 
433             (col1.getFlags() == col2.getFlags()));
434   }
435 
436   @Override
437   protected String withErrorContext(String msg) {
438     return msg + "(Table=" + getName() + ")";
439   }
440 }