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<IndexDataState>();
48    private final Map<ColumnBuilder,ColumnState> _columnStates = 
49      new IdentityHashMap<ColumnBuilder,ColumnState>();
50    private final List<ColumnBuilder> _lvalCols = new ArrayList<ColumnBuilder>();
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   public List<ColumnBuilder> getLongValueColumns() {
121     return _lvalCols;
122   }
123 
124   @Override
125   short getColumnNumber(String colName) {
126     for(ColumnBuilder col : _columns) {
127       if(col.getName().equalsIgnoreCase(colName)) {
128         return col.getColumnNumber();
129       }
130     }
131     return IndexData.COLUMN_UNUSED;
132   }  
133 
134   /**
135    * @return The number of variable length columns which are not long values
136    *         found in the list
137    * @usage _advanced_method_
138    */
139   public short countNonLongVariableLength() {
140     short rtn = 0;
141     for (ColumnBuilder col : _columns) {
142       if (col.isVariableLength() && !col.getType().isLongValue()) {
143         rtn++;
144       }
145     }
146     return rtn;
147   }
148   
149 
150   /**
151    * Creates the table in the database.
152    * @usage _advanced_method_
153    */
154   public TableImpl createTable(TableBuilder table) throws IOException {
155 
156     _name = table.getName();
157     _columns = table.getColumns();
158     _indexes = table.getIndexes();
159     if(_indexes == null) {
160       _indexes = Collections.<IndexBuilder>emptyList();
161     }
162 
163     validate();
164 
165     // assign column numbers and do some assorted column bookkeeping
166     short columnNumber = (short) 0;
167     for(ColumnBuilder col : _columns) {
168       col.setColumnNumber(columnNumber++);
169       if(col.getType().isLongValue()) {
170         _lvalCols.add(col);
171         // only lval columns need extra state
172         _columnStates.put(col, new ColumnState());
173       }
174     }
175 
176     if(hasIndexes()) {
177       // sort out index numbers (and backing index data).  
178       for(IndexBuilder idx : _indexes) {
179         idx.setIndexNumber(_logicalIndexCount++);
180         findIndexDataState(idx);
181       }
182     }
183 
184     getPageChannel().startExclusiveWrite();
185     try {
186       
187       // reserve some pages
188       _tdefPageNumber = reservePageNumber();
189       _umapPageNumber = reservePageNumber();
190     
191       //Write the tdef page to disk.
192       TableImpl.writeTableDefinition(this);
193 
194       // update the database with the new table info
195       getDatabase().addNewTable(_name, _tdefPageNumber, DatabaseImpl.TYPE_TABLE, 
196                                 null, null);
197 
198       TableImpl newTable = getDatabase().getTable(_name);
199 
200       // add any table properties
201       boolean addedProps = false;
202       Map<String,PropertyMap.Property> props = table.getProperties();
203       if(props != null) {
204         newTable.getProperties().putAll(props.values());
205         addedProps = true;
206       }
207       for(ColumnBuilder cb : _columns) {
208         Map<String,PropertyMap.Property> colProps = cb.getProperties();
209         if(colProps != null) {
210           newTable.getColumn(cb.getName()).getProperties()
211             .putAll(colProps.values());
212           addedProps = true;
213         }
214       }
215 
216       // all table and column props are saved together
217       if(addedProps) {
218         newTable.getProperties().save();
219       }
220 
221       return newTable;
222 
223     } finally {
224       getPageChannel().finishWrite();
225     }
226   }
227 
228   private IndexDataState findIndexDataState(IndexBuilder idx) {
229 
230     // search for an index which matches the given index (in terms of the
231     // backing data)
232     for(IndexDataState idxDataState : _indexDataStates) {
233       if(sameIndexData(idxDataState.getFirstIndex(), idx)) {
234         idxDataState.addIndex(idx);
235         return idxDataState;
236       }
237     }
238 
239     // no matches found, need new index data state
240     IndexDataState idxDataState = new IndexDataState();
241     idxDataState.setIndexDataNumber(_indexCount++);
242     idxDataState.addIndex(idx);
243     _indexDataStates.add(idxDataState);
244     return idxDataState;
245   }
246 
247   /**
248    * Validates the new table information before attempting creation.
249    */
250   private void validate() throws IOException {
251 
252     getDatabase().validateNewTableName(_name);
253     
254     if((_columns == null) || _columns.isEmpty()) {
255       throw new IllegalArgumentException(withErrorContext(
256           "Cannot create table with no columns"));
257     }
258     if(_columns.size() > getFormat().MAX_COLUMNS_PER_TABLE) {
259       throw new IllegalArgumentException(withErrorContext(
260           "Cannot create table with more than " +
261           getFormat().MAX_COLUMNS_PER_TABLE + " columns"));
262     }
263     
264     Set<String> colNames = new HashSet<String>();
265     // next, validate the column definitions
266     for(ColumnBuilder column : _columns) {
267       validateColumn(colNames, column);
268     }
269 
270     List<ColumnBuilder> autoCols = getAutoNumberColumns();
271     if(autoCols.size() > 1) {
272       // for most autonumber types, we can only have one of each type
273       Set<DataType> autoTypes = EnumSet.noneOf(DataType.class);
274       for(ColumnBuilder c : autoCols) {
275         validateAutoNumberColumn(autoTypes, c);
276       }
277     }
278 
279     if(hasIndexes()) {
280 
281       if(_indexes.size() > getFormat().MAX_INDEXES_PER_TABLE) {
282         throw new IllegalArgumentException(withErrorContext(
283             "Cannot create table with more than " +
284             getFormat().MAX_INDEXES_PER_TABLE + " indexes"));
285       }
286 
287       // now, validate the indexes
288       Set<String> idxNames = new HashSet<String>();
289       boolean foundPk[] = new boolean[1];
290       for(IndexBuilder index : _indexes) {
291         validateIndex(colNames, idxNames, foundPk, index);
292       }
293     }
294   }
295 
296   private List<ColumnBuilder> getAutoNumberColumns() 
297   {
298     List<ColumnBuilder> autoCols = new ArrayList<ColumnBuilder>(1);
299     for(ColumnBuilder c : _columns) {
300       if(c.isAutoNumber()) {
301         autoCols.add(c);
302       }
303     }
304     return autoCols;
305   }
306 
307   private static boolean sameIndexData(IndexBuilder idx1, IndexBuilder idx2) {
308     // index data can be combined if flags match and columns (and col flags)
309     // match
310     if(idx1.getFlags() != idx2.getFlags()) {
311       return false;
312     }
313 
314     if(idx1.getColumns().size() != idx2.getColumns().size()) {
315       return false;
316     }
317     
318     for(int i = 0; i < idx1.getColumns().size(); ++i) {
319       IndexBuilder.Column col1 = idx1.getColumns().get(i);
320       IndexBuilder.Column col2 = idx2.getColumns().get(i);
321 
322       if(!sameIndexData(col1, col2)) {
323         return false;
324       }
325     }
326 
327     return true;
328   }
329 
330   private static boolean sameIndexData(
331       IndexBuilder.Column col1, IndexBuilder.Column col2) {
332     return (col1.getName().equals(col2.getName()) && 
333             (col1.getFlags() == col2.getFlags()));
334   }
335 
336   @Override
337   protected String withErrorContext(String msg) {
338     return msg + "(Table=" + getName() + ")";
339   }
340 }