View Javadoc
1   /*
2   Copyright (c) 2016 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.nio.ByteBuffer;
21  import java.util.ArrayList;
22  import java.util.EnumSet;
23  import java.util.HashSet;
24  import java.util.List;
25  import java.util.Set;
26  
27  import com.healthmarketscience.jackcess.ColumnBuilder;
28  import com.healthmarketscience.jackcess.DataType;
29  import com.healthmarketscience.jackcess.IndexBuilder;
30  
31  /**
32   * Helper class used to maintain state during table mutation.
33   *
34   * @author James Ahlborn
35   * @usage _advanced_class_
36   */
37  public class TableUpdater extends TableMutator
38  {
39    private final TableImpl _table;
40  
41    private ColumnBuilder _column;
42    private IndexBuilder _index;
43    private int _origTdefLen;
44    private int _addedTdefLen;
45    private List<Integer> _nextPages = new ArrayList<Integer>(1);
46    private ColumnState _colState;
47    private IndexDataState _idxDataState;
48    private IndexImpl.ForeignKeyReference _fkReference;
49  
50    public TableUpdater(TableImpl table) {
51      super(table.getDatabase());
52      _table = table;
53    }
54  
55    public ColumnBuilder getColumn() {
56      return _column;
57    }
58  
59    public IndexBuilder getIndex() {
60      return _index;
61    }
62  
63    @Override
64    String getTableName() {
65      return _table.getName();
66    }
67    
68    @Override
69    public int getTdefPageNumber() {
70      return _table.getTableDefPageNumber();
71    }
72  
73    @Override
74    short getColumnNumber(String colName) {
75      for(ColumnImpl col : _table.getColumns()) {
76        if(col.getName().equalsIgnoreCase(colName)) {
77          return col.getColumnNumber();
78        }
79      }
80      return IndexData.COLUMN_UNUSED;
81    }
82  
83    @Override
84    public ColumnState getColumnState(ColumnBuilder col) {
85      return ((col == _column) ? _colState : null);
86    }
87  
88    @Override
89    public IndexDataState getIndexDataState(IndexBuilder idx) {
90      return ((idx == _index) ? _idxDataState : null);
91    }
92  
93    void setForeignKey(IndexImpl.ForeignKeyReference fkReference) {
94      _fkReference = fkReference;
95    }
96  
97    @Override
98    public IndexImpl.ForeignKeyReference getForeignKey(IndexBuilder idx) {
99      return ((idx == _index) ? _fkReference : null);
100   }  
101 
102   int getAddedTdefLen() {
103     return _addedTdefLen;
104   }
105 
106   void addTdefLen(int add) {
107     _addedTdefLen += add;
108   }
109 
110   void setOrigTdefLen(int len) {
111     _origTdefLen = len;
112   }
113 
114   List<Integer> getNextPages() {
115     return _nextPages;
116   }
117 
118   void resetTdefInfo() {
119     _addedTdefLen = 0;
120     _origTdefLen = 0;
121     _nextPages.clear();
122   }
123 
124   public ColumnImpl addColumn(ColumnBuilder column) throws IOException {
125 
126     _column = column;
127 
128     validateAddColumn();
129     
130     // assign column number and do some assorted column bookkeeping
131     short columnNumber = (short)_table.getMaxColumnCount();
132     _column.setColumnNumber(columnNumber);
133     if(_column.getType().isLongValue()) {
134       _colState = new ColumnState();
135     }
136 
137     getPageChannel().startExclusiveWrite();
138     try {
139 
140       return _table.mutateAddColumn(this);
141 
142     } finally {
143       getPageChannel().finishWrite();
144     }
145   }
146 
147   public IndexImpl addIndex(IndexBuilder index) throws IOException {
148     return addIndex(index, false, (byte)0, (byte)0);
149   }
150 
151   IndexImpl addIndex(IndexBuilder index, boolean isInternal, byte ignoreIdxFlags,
152                      byte ignoreColFlags) 
153     throws IOException 
154   {
155     _index = index;
156 
157     if(!isInternal) {
158       validateAddIndex();
159     }
160 
161     // assign index number and do some assorted index bookkeeping
162     int indexNumber = _table.getLogicalIndexCount();
163     _index.setIndexNumber(indexNumber);
164 
165     // initialize backing index state
166     initIndexDataState(ignoreIdxFlags, ignoreColFlags);
167 
168     if(!isInternal) {
169       getPageChannel().startExclusiveWrite();
170     } else {
171       // if "internal" update, this is part of a larger operation which
172       // already holds an exclusive write lock
173       getPageChannel().startWrite();      
174     }
175     try {
176 
177       if(_idxDataState.getIndexDataNumber() == _table.getIndexCount()) {
178         // we need a new backing index data
179         _table.mutateAddIndexData(this);
180 
181         // we need to modify the table def again when adding the Index, so reset
182         resetTdefInfo();
183       }
184 
185       return _table.mutateAddIndex(this);
186 
187     } finally {
188       getPageChannel().finishWrite();
189     }
190   }
191 
192   boolean validateUpdatedTdef(ByteBuffer tableBuffer) {
193     // sanity check the updates
194     return((_origTdefLen + _addedTdefLen) == tableBuffer.limit());
195   }
196 
197   private void validateAddColumn() {
198 
199     if(_column == null) {
200       throw new IllegalArgumentException(withErrorContext(
201           "Cannot add column with no column"));
202     }
203     if((_table.getColumnCount() + 1) > getFormat().MAX_COLUMNS_PER_TABLE) {
204       throw new IllegalArgumentException(withErrorContext(
205           "Cannot add column to table with " +
206           getFormat().MAX_COLUMNS_PER_TABLE + " columns"));
207     }
208     
209     Set<String> colNames = getColumnNames();
210     // next, validate the column definition
211     validateColumn(colNames, _column);
212     
213     if(_column.isAutoNumber()) {
214       // for most autonumber types, we can only have one of each type
215       Set<DataType> autoTypes = EnumSet.noneOf(DataType.class);
216       for(ColumnImpl column : _table.getAutoNumberColumns()) {
217         autoTypes.add(column.getType());
218       }
219 
220       validateAutoNumberColumn(autoTypes, _column);
221     }
222   }
223 
224   private void validateAddIndex() {
225     
226     if(_index == null) {
227       throw new IllegalArgumentException(withErrorContext(
228           "Cannot add index with no index"));
229     }
230     if((_table.getLogicalIndexCount() + 1) > getFormat().MAX_INDEXES_PER_TABLE) {
231       throw new IllegalArgumentException(withErrorContext(
232           "Cannot add index to table with " +
233           getFormat().MAX_INDEXES_PER_TABLE + " indexes"));
234     }
235     
236     boolean foundPk[] = new boolean[1];
237     Set<String> idxNames = getIndexNames(_table, foundPk);
238     // next, validate the index definition
239     validateIndex(getColumnNames(), idxNames, foundPk, _index);    
240   }
241 
242   private Set<String> getColumnNames() {
243     Set<String> colNames = new HashSet<String>();
244     for(ColumnImpl column : _table.getColumns()) {
245       colNames.add(DatabaseImpl.toLookupName(column.getName()));
246     }
247     return colNames;
248   }
249 
250   static Set<String> getIndexNames(TableImpl table, boolean[] foundPk) {
251     Set<String> idxNames = new HashSet<String>();
252     for(IndexImpl index : table.getIndexes()) {
253       idxNames.add(DatabaseImpl.toLookupName(index.getName()));
254       if(index.isPrimaryKey() && (foundPk != null)) {
255         foundPk[0] = true;
256       }
257     }
258     return idxNames;
259   }
260 
261   private void initIndexDataState(byte ignoreIdxFlags, byte ignoreColFlags) {
262 
263     _idxDataState = new IndexDataState();
264     _idxDataState.addIndex(_index);
265     
266     // search for an existing index which matches the given index (in terms of
267     // the backing data)
268     IndexData idxData = findIndexData(
269         _index, _table, ignoreIdxFlags, ignoreColFlags);
270 
271     int idxDataNumber = ((idxData != null) ?
272                          idxData.getIndexDataNumber() :
273                          _table.getIndexCount());
274 
275     _idxDataState.setIndexDataNumber(idxDataNumber);
276   }
277 
278   static IndexData findIndexData(IndexBuilder idx, TableImpl table,
279                                  byte ignoreIdxFlags, byte ignoreColFlags)
280   {
281     for(IndexData idxData : table.getIndexDatas()) {
282       if(sameIndexData(idx, idxData, ignoreIdxFlags, ignoreColFlags)) {
283         return idxData;
284       }
285     }
286     return null;
287   }
288   
289   private static boolean sameIndexData(IndexBuilder idx1, IndexData idx2,
290                                        byte ignoreIdxFlags, byte ignoreColFlags) {
291     // index data can be combined if flags match and columns (and col flags)
292     // match
293     if((idx1.getFlags() | ignoreIdxFlags) !=
294        (idx2.getIndexFlags() | ignoreIdxFlags)) {
295       return false;
296     }
297 
298     if(idx1.getColumns().size() != idx2.getColumnCount()) {
299       return false;
300     }
301     
302     for(int i = 0; i < idx1.getColumns().size(); ++i) {
303       IndexBuilder.Column col1 = idx1.getColumns().get(i);
304       IndexData.ColumnDescriptor col2 = idx2.getColumns().get(i);
305 
306       if(!sameIndexData(col1, col2, ignoreColFlags)) {
307         return false;
308       }
309     }
310 
311     return true;
312   }
313 
314   private static boolean sameIndexData(
315       IndexBuilder.Column col1, IndexData.ColumnDescriptor col2,
316       int ignoreColFlags) {
317     return (col1.getName().equals(col2.getName()) && 
318             ((col1.getFlags() | ignoreColFlags) ==
319              (col2.getFlags() | ignoreColFlags)));
320   }
321 
322   @Override
323   protected String withErrorContext(String msg) {
324     String objStr = "";
325     if(_column != null) {
326       objStr = ";Column=" + _column.getName();
327     } else if(_index != null) {
328       objStr = ";Index=" + _index.getName();
329     }
330     return msg + "(Table=" + _table.getName() + objStr + ")";
331   }
332 }