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.util.ArrayList;
21  import java.util.Collection;
22  import java.util.HashSet;
23  import java.util.List;
24  import java.util.Set;
25  
26  import com.healthmarketscience.jackcess.ConstraintViolationException;
27  import com.healthmarketscience.jackcess.IndexBuilder;
28  import com.healthmarketscience.jackcess.IndexCursor;
29  import com.healthmarketscience.jackcess.RelationshipBuilder;
30  import com.healthmarketscience.jackcess.Row;
31  
32  /**
33   * Helper class used to maintain state during relationship creation.
34   *
35   * @author James Ahlborn
36   */
37  public class RelationshipCreator extends DBMutator
38  {
39    private final static int CASCADE_FLAGS =
40      RelationshipImpl.CASCADE_DELETES_FLAG |
41      RelationshipImpl.CASCADE_UPDATES_FLAG |
42      RelationshipImpl.CASCADE_NULL_FLAG;
43  
44    // for the purposes of choosing a backing index for a foreign key, there are
45    // certain index flags that can be ignored (we don't care how they are set)
46    private final static byte IGNORED_PRIMARY_INDEX_FLAGS =
47      IndexData.IGNORE_NULLS_INDEX_FLAG | IndexData.REQUIRED_INDEX_FLAG;
48    private final static byte IGNORED_SECONDARY_INDEX_FLAGS =
49      IGNORED_PRIMARY_INDEX_FLAGS | IndexData.UNIQUE_INDEX_FLAG;
50  
51    private TableImpl _primaryTable;
52    private TableImpl _secondaryTable;
53    private RelationshipBuilder _relationship;
54    private List<ColumnImpl> _primaryCols;
55    private List<ColumnImpl> _secondaryCols;
56    private int _flags;
57    private String _name;
58  
59    public RelationshipCreator(DatabaseImpl database)
60    {
61      super(database);
62    }
63  
64    public String getName() {
65      return _name;
66    }
67  
68    public TableImpl getPrimaryTable() {
69      return _primaryTable;
70    }
71  
72    public TableImpl getSecondaryTable() {
73      return _secondaryTable;
74    }
75  
76    public boolean hasReferentialIntegrity() {
77      return _relationship.hasReferentialIntegrity();
78    }
79  
80    public RelationshipImpl createRelationshipImpl(String name) {
81      _name = name;
82      return new RelationshipImpl(
83          name, _primaryTable, _secondaryTable, _flags,
84          _primaryCols, _secondaryCols);
85    }
86  
87    /**
88     * Creates the relationship in the database.
89     * @usage _advanced_method_
90     */
91    public RelationshipImpl createRelationship(RelationshipBuilder relationship)
92      throws IOException
93    {
94      _relationship = relationship;
95      _name = relationship.getName();
96  
97      validate();
98  
99      _flags = _relationship.getFlags();
100     // need to determine the one-to-one flag on our own
101     if(isOneToOne()) {
102       _flags |= RelationshipImpl.ONE_TO_ONE_FLAG;
103     }
104 
105     getPageChannel().startExclusiveWrite();
106     try {
107 
108       RelationshipImpl newRel = getDatabase().writeRelationship(this);
109 
110       if(hasReferentialIntegrity()) {
111         // each index carries the number of the other one, so both numbers
112         // are worked out before either index is added.  a table related to
113         // itself takes both of them, one slot apart, and ms access writes
114         // the secondary one first
115         int secondaryIdxNum = _secondaryTable.getLogicalIndexCount();
116         int primaryIdxNum = _primaryTable.getLogicalIndexCount() +
117           (isSameTable() ? 1 : 0);
118         addSecondaryIndex(primaryIdxNum);
119         addPrimaryIndex(secondaryIdxNum);
120       }
121 
122       return newRel;
123 
124     } finally {
125       getPageChannel().finishWrite();
126     }
127   }
128 
129   private boolean isSameTable() {
130     return (_primaryTable.getTableDefPageNumber() ==
131             _secondaryTable.getTableDefPageNumber());
132   }
133 
134   private void addPrimaryIndex(int secondaryIdxNum) throws IOException {
135     TableUpdaterpl/TableUpdater.html#TableUpdater">TableUpdater updater = new TableUpdater(_primaryTable);
136     updater.setForeignKey(createFKReference(true, secondaryIdxNum));
137     updater.addIndex(createPrimaryIndex(), true,
138                      IGNORED_PRIMARY_INDEX_FLAGS, (byte)0);
139   }
140 
141   private void addSecondaryIndex(int primaryIdxNum) throws IOException {
142     TableUpdaterpl/TableUpdater.html#TableUpdater">TableUpdater updater = new TableUpdater(_secondaryTable);
143     updater.setForeignKey(createFKReference(false, primaryIdxNum));
144     updater.addIndex(createSecondaryIndex(), true,
145                      IGNORED_SECONDARY_INDEX_FLAGS, (byte)0);
146   }
147 
148   private IndexImpl.ForeignKeyReference createFKReference(
149       boolean isPrimary, int otherIdxNum) {
150     byte tableType = 0;
151     int otherTableNum = 0;
152     if(isPrimary) {
153       tableType = IndexImpl.FK_PRIMARY_TABLE_TYPE;
154       otherTableNum = _secondaryTable.getTableDefPageNumber();
155     } else {
156       tableType = IndexImpl.FK_SECONDARY_TABLE_TYPE;
157       otherTableNum = _primaryTable.getTableDefPageNumber();
158     }
159     boolean cascadeUpdates = ((_flags & RelationshipImpl.CASCADE_UPDATES_FLAG) != 0);
160     boolean cascadeDeletes = ((_flags & RelationshipImpl.CASCADE_DELETES_FLAG) != 0);
161     boolean cascadeNull = ((_flags & RelationshipImpl.CASCADE_NULL_FLAG) != 0);
162 
163     return new IndexImpl.ForeignKeyReference(
164         tableType, otherIdxNum, otherTableNum, cascadeUpdates, cascadeDeletes,
165         cascadeNull);
166   }
167 
168   private void validate() throws IOException {
169 
170     _primaryTable = getDatabase().getTable(_relationship.getFromTable());
171     _secondaryTable = getDatabase().getTable(_relationship.getToTable());
172 
173     if((_primaryTable == null) || (_secondaryTable == null)) {
174       throw new IllegalArgumentException(withErrorContext(
175           "Two valid tables are required in relationship"));
176     }
177 
178     if(_name != null) {
179       DatabaseImpl.validateIdentifierName(
180           _name, _primaryTable.getFormat().MAX_INDEX_NAME_LENGTH, "relationship");
181     }
182 
183     _primaryCols = getColumns(_primaryTable, _relationship.getFromColumns());
184     _secondaryCols = getColumns(_secondaryTable, _relationship.getToColumns());
185 
186     if((_primaryCols == null) || (_primaryCols.isEmpty()) ||
187        (_secondaryCols == null) || (_secondaryCols.isEmpty())) {
188       throw new IllegalArgumentException(withErrorContext(
189           "Missing columns in relationship"));
190     }
191 
192     if(_primaryCols.size() != _secondaryCols.size()) {
193       throw new IllegalArgumentException(withErrorContext(
194           "Must have same number of columns on each side of relationship"));
195     }
196 
197     for(int i = 0; i < _primaryCols.size(); ++i) {
198       ColumnImpl pcol = _primaryCols.get(i);
199       ColumnImpl scol = _secondaryCols.get(i);
200 
201       if(pcol.getType() != scol.getType()) {
202         throw new IllegalArgumentException(withErrorContext(
203             "Matched columns must have the same data type"));
204       }
205     }
206 
207     if(!hasReferentialIntegrity()) {
208 
209       if((_relationship.getFlags() & CASCADE_FLAGS) != 0) {
210         throw new IllegalArgumentException(withErrorContext(
211             "Cascade flags cannot be enabled if referential integrity is not enforced"));
212       }
213 
214       return;
215     }
216 
217     // for now, we will require the unique index on the primary table (just
218     // like access does).  we could just create it auto-magically...
219     IndexImpl primaryIdx = getUniqueIndex(_primaryTable, _primaryCols);
220     if(primaryIdx == null) {
221       throw new IllegalArgumentException(withErrorContext(
222           "Missing unique index on primary table required to enforce integrity"));
223     }
224 
225     // while relationships can have "dupe" columns, indexes (and therefore
226     // integrity enforced relationships) cannot
227     if((new HashSet<String>(getColumnNames(_primaryCols)).size() !=
228         _primaryCols.size()) ||
229        (new HashSet<String>(getColumnNames(_secondaryCols)).size() !=
230         _secondaryCols.size())) {
231       throw new IllegalArgumentException(withErrorContext(
232           "Cannot have duplicate columns in an integrity enforced relationship"));
233     }
234 
235     // a table can be related to itself, but access refuses to enforce
236     // integrity where a column is matched against itself, which every row
237     // satisfies by supplying its own match
238     if(isSameTable()) {
239       for(int i = 0; i < _primaryCols.size(); ++i) {
240         if(_primaryCols.get(i).getName().equalsIgnoreCase(
241                _secondaryCols.get(i).getName())) {
242           throw new IllegalArgumentException(withErrorContext(
243               "Cannot enforce integrity on column " +
244               _primaryCols.get(i).getName() + " matched against itself"));
245         }
246       }
247     }
248 
249     // TODO: future, check for enforce cycles?
250 
251     // check referential integrity
252     IndexCursor primaryCursor = primaryIdx.newCursor().toIndexCursor();
253     Object[] entryValues = new Object[_secondaryCols.size()];
254     for(Row row : _secondaryTable.newCursor().toCursor()
255           .newIterable().addColumns(_secondaryCols)) {
256       // grab the secondary table values
257       boolean hasValues = false;
258       for(int i = 0; i < _secondaryCols.size(); ++i) {
259         entryValues[i] = _secondaryCols.get(i).getRowValue(row);
260         hasValues = hasValues || (entryValues[i] != null);
261       }
262 
263       if(!hasValues) {
264         // we can ignore null entries
265         continue;
266       }
267 
268       // check that they exist in the primary table
269       if(!primaryCursor.findFirstRowByEntry(entryValues)) {
270         throw new ConstraintViolationException(withErrorContext(
271             "Integrity constraint violation found for relationship"));
272       }
273     }
274 
275   }
276 
277   private IndexBuilder createPrimaryIndex() {
278     String name = createPrimaryIndexName();
279     return createIndex(name, _primaryCols)
280       .setUnique()
281       .setType(IndexImpl.FOREIGN_KEY_INDEX_TYPE);
282   }
283 
284   private IndexBuilder createSecondaryIndex() {
285     // secondary index uses relationship name
286     return createIndex(_name, _secondaryCols)
287       .setType(IndexImpl.FOREIGN_KEY_INDEX_TYPE);
288   }
289 
290   private static IndexBuilder createIndex(String name, List<ColumnImpl> cols) {
291     IndexBuilders/IndexBuilder.html#IndexBuilder">IndexBuilder idx = new IndexBuilder(name);
292     for(ColumnImpl col : cols) {
293       idx.addColumns(col.getName());
294     }
295     return idx;
296   }
297 
298   private String createPrimaryIndexName() {
299     Set<String> idxNames = TableUpdater.getIndexNames(_primaryTable, null);
300 
301     // primary naming scheme: ".rB", .rC", ".rD", "rE" ...
302     String baseName = ".r";
303     String suffix = "B";
304 
305     while(true) {
306       String idxName = baseName + suffix;
307       if(!idxNames.contains(DatabaseImpl.toLookupName(idxName))) {
308         return idxName;
309       }
310 
311       char c = (char)(suffix.charAt(0) + 1);
312       if(c == '[') {
313         c = 'a';
314       }
315       suffix = "" + c;
316     }
317   }
318 
319   private static List<ColumnImpl> getColumns(TableImpl table,
320                                              List<String> colNames) {
321     List<ColumnImpl> cols = new ArrayList<>();
322     for(String colName : colNames) {
323       cols.add(table.getColumn(colName));
324     }
325     return cols;
326   }
327 
328   private static List<String> getColumnNames(List<ColumnImpl> cols) {
329     List<String> colNames = new ArrayList<>();
330     for(ColumnImpl col : cols) {
331       colNames.add(col.getName());
332     }
333     return colNames;
334   }
335 
336   private boolean isOneToOne() {
337     // a relationship is one to one if the two sides of the relationship have
338     // unique indexes on the relevant columns
339     if(getUniqueIndex(_primaryTable, _primaryCols) == null) {
340       return false;
341     }
342     IndexImpl idx = getUniqueIndex(_secondaryTable, _secondaryCols);
343     return (idx != null);
344   }
345 
346   private static IndexImpl getUniqueIndex(
347       TableImpl table, List<ColumnImpl> cols) {
348     return table.findIndexForColumns(getColumnNames(cols),
349                                      TableImpl.IndexFeature.EXACT_UNIQUE_ONLY);
350   }
351 
352   private static String getTableErrorContext(
353       TableImpl table, List<ColumnImpl> cols,
354       String tableName, Collection<String> colNames) {
355     if(table != null) {
356       tableName = table.getName();
357     }
358     if(cols != null) {
359       colNames = getColumnNames(cols);
360     }
361 
362     return ToStringBuilder.valueBuilder(tableName)
363       .append(null, colNames)
364       .toString();
365   }
366 
367   private String withErrorContext(String msg) {
368     return msg + "(Rel=" +
369       getTableErrorContext(_primaryTable, _primaryCols,
370                            _relationship.getFromTable(),
371                            _relationship.getFromColumns()) + " -> " +
372       getTableErrorContext(_secondaryTable, _secondaryCols,
373                            _relationship.getToTable(),
374                            _relationship.getToColumns()) + ")";
375   }
376 }