View Javadoc
1   /*
2   Copyright (c) 2012 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.Arrays;
22  import java.util.Collections;
23  import java.util.Iterator;
24  import java.util.List;
25  import java.util.Set;
26  import java.util.TreeSet;
27  
28  import com.healthmarketscience.jackcess.Column;
29  import com.healthmarketscience.jackcess.ConstraintViolationException;
30  import com.healthmarketscience.jackcess.Index;
31  import com.healthmarketscience.jackcess.IndexCursor;
32  import com.healthmarketscience.jackcess.Row;
33  import com.healthmarketscience.jackcess.Table;
34  import com.healthmarketscience.jackcess.util.CaseInsensitiveColumnMatcher;
35  import com.healthmarketscience.jackcess.util.ColumnMatcher;
36  import com.healthmarketscience.jackcess.util.Joiner;
37  
38  /**
39   * Utility class used by Table to enforce foreign-key relationships (if
40   * enabled).
41   *
42   * @author James Ahlborn
43   * @usage _advanced_class_
44   */
45  final class FKEnforcer
46  {
47    // fk constraints always work with indexes, which are always
48    // case-insensitive
49    private static final ColumnMatcher MATCHER =
50      CaseInsensitiveColumnMatcher.INSTANCE;
51  
52    private final TableImpl _table;
53    private List<ColumnImpl> _cols;
54    private List<Joiner> _primaryJoinersChkUp;
55    private List<Joiner> _primaryJoinersChkDel;
56    private List<Joiner> _primaryJoinersDoUp;
57    private List<Joiner> _primaryJoinersDoDel;
58    private List<Joiner> _primaryJoinersDoNull;
59    private List<Joiner> _secondaryJoiners;
60  
61    FKEnforcer(TableImpl table) {
62      _table = table;
63  
64      // at this point, only init the index columns
65      initColumns();
66    }
67  
68    private void initColumns() {
69      Set<ColumnImpl> cols = new TreeSet<>(ColumnImpl.DEFAULT_ORDER_COMPARATOR);
70      for(IndexImpl idx : _table.getIndexes()) {
71        IndexImpl.ForeignKeyReference ref = idx.getReference();
72        if(ref != null) {
73          // compile an ordered list of all columns in this table which are
74          // involved in foreign key relationships with other tables
75          for(IndexData.ColumnDescriptor iCol : idx.getColumns()) {
76            cols.add(iCol.getColumn());
77          }
78        }
79      }
80      _cols = !cols.isEmpty() ?
81        Collections.unmodifiableList(new ArrayList<ColumnImpl>(cols)) :
82        Collections.<ColumnImpl>emptyList();
83    }
84  
85    /**
86     * Resets the internals of this FKEnforcer (for post-table modification)
87     */
88    void reset() {
89      // columns to enforce may have changed
90      initColumns();
91  
92      // clear any existing joiners (will be re-created on next use)
93      _primaryJoinersChkUp = null;
94      _primaryJoinersChkDel = null;
95      _primaryJoinersDoUp = null;
96      _primaryJoinersDoDel = null;
97      _primaryJoinersDoNull = null;
98      _secondaryJoiners = null;
99    }
100 
101   /**
102    * Does secondary initialization, if necessary.
103    */
104   private void initialize() throws IOException {
105     if(_secondaryJoiners != null) {
106       // already initialized
107       return;
108     }
109 
110     // initialize all the joiners
111     _primaryJoinersChkUp = new ArrayList<>(1);
112     _primaryJoinersChkDel = new ArrayList<>(1);
113     _primaryJoinersDoUp = new ArrayList<>(1);
114     _primaryJoinersDoDel = new ArrayList<>(1);
115     _primaryJoinersDoNull = new ArrayList<>(1);
116     _secondaryJoiners = new ArrayList<>(1);
117 
118     for(IndexImpl idx : _table.getIndexes()) {
119       IndexImpl.ForeignKeyReference ref = idx.getReference();
120       if(ref != null) {
121 
122         Joiner joiner = Joiner.create(idx);
123         if(ref.isPrimaryTable()) {
124           if(ref.isCascadeUpdates()) {
125             _primaryJoinersDoUp.add(joiner);
126           } else {
127             _primaryJoinersChkUp.add(joiner);
128           }
129           if(ref.isCascadeDeletes()) {
130             _primaryJoinersDoDel.add(joiner);
131           } else if(ref.isCascadeNullOnDelete()) {
132             _primaryJoinersDoNull.add(joiner);
133           } else {
134             _primaryJoinersChkDel.add(joiner);
135           }
136         } else {
137           _secondaryJoiners.add(joiner);
138         }
139       }
140     }
141   }
142 
143   /**
144    * Handles foregn-key constraints when adding a row.
145    *
146    * @param row new row in the Table's row format, including all values used
147    *            in any foreign-key relationships
148    */
149   public void addRow(Object[] row) throws IOException {
150     if(!enforcing()) {
151       return;
152     }
153     initialize();
154 
155     for(Joiner joiner : _secondaryJoiners) {
156       requirePrimaryValues(joiner, row);
157     }
158   }
159 
160   /**
161    * Handles foregn-key constraints when updating a row.
162    *
163    * @param oldRow old row in the Table's row format, including all values
164    *               used in any foreign-key relationships
165    * @param newRow new row in the Table's row format, including all values
166    *               used in any foreign-key relationships
167    */
168   public void updateRow(Object[] oldRow, Object[] newRow) throws IOException {
169     if(!enforcing()) {
170       return;
171     }
172 
173     if(!anyUpdates(oldRow, newRow)) {
174       // no changes were made to any relevant columns
175       return;
176     }
177 
178     initialize();
179 
180     SharedState ss = _table.getDatabase().getFKEnforcerSharedState();
181 
182     if(ss.isUpdating()) {
183       // we only check the primary relationships for the "top-level" of an
184       // update operation.  in nested levels we are only ever changing the fk
185       // values themselves, so we always know the new values are valid.
186       for(Joiner joiner : _secondaryJoiners) {
187         if(anyUpdates(joiner, oldRow, newRow)) {
188           requirePrimaryValues(joiner, newRow);
189         }
190       }
191     }
192 
193     ss.pushUpdate();
194     try {
195 
196       // now, check the tables for which we are the primary table in the
197       // relationship (but not cascading)
198       for(Joiner joiner : _primaryJoinersChkUp) {
199         if(anyUpdates(joiner, oldRow, newRow)) {
200           requireNoSecondaryValues(joiner, oldRow);
201         }
202       }
203 
204       // lastly, update the tables for which we are the primary table in the
205       // relationship
206       for(Joiner joiner : _primaryJoinersDoUp) {
207         if(anyUpdates(joiner, oldRow, newRow)) {
208           updateSecondaryValues(joiner, oldRow, newRow);
209         }
210       }
211 
212     } finally {
213       ss.popUpdate();
214     }
215   }
216 
217   /**
218    * Handles foregn-key constraints when deleting a row.
219    *
220    * @param row old row in the Table's row format, including all values used
221    *            in any foreign-key relationships
222    */
223   public void deleteRow(Object[] row) throws IOException {
224     if(!enforcing()) {
225       return;
226     }
227     initialize();
228 
229     // first, check the tables for which we are the primary table in the
230     // relationship (but not cascading)
231     for(Joiner joiner : _primaryJoinersChkDel) {
232       requireNoSecondaryValues(joiner, row);
233     }
234 
235     // next, delete from the tables for which we are the primary table in
236     // the relationship
237     for(Joiner joiner : _primaryJoinersDoDel) {
238       joiner.deleteRows(row);
239     }
240 
241     // lastly, null the tables for which we are the primary table in
242     // the relationship
243     for(Joiner joiner : _primaryJoinersDoNull) {
244       nullSecondaryValues(joiner, row);
245     }
246   }
247 
248   private static void requirePrimaryValues(Joiner joiner, Object[] row)
249     throws IOException
250   {
251     // ensure that the relevant rows exist in the primary tables for which
252     // this table is a secondary table.  however, null values are allowed
253     if(areNull(joiner, row) || isOwnPrimaryRow(joiner, row) ||
254        joiner.hasRows(row)) {
255       return;
256     }
257 
258     throw new ConstraintViolationException(
259         "Adding new row " + Arrays.asList(row) + " violates constraint " +
260         joiner.toFKString());
261   }
262 
263   /**
264    * A table related to itself holds a tree, a row carrying the key of its
265    * parent row, and ms access lets a row of one be its own parent.  The row
266    * is not written when the constraint is checked, so the lookup cannot find
267    * the match the row supplies itself.
268    *
269    * @return whether the given row already holds the primary values its own
270    *         foreign key names
271    */
272   private static boolean isOwnPrimaryRow(Joiner joiner, Object[] row) {
273     Table table = joiner.getFromTable();
274     if(table != joiner.getToTable()) {
275       return false;
276     }
277 
278     List<? extends Index.Column> fkCols = joiner.getColumns();
279     List<? extends Index.Column> pkCols = joiner.getToIndex().getColumns();
280     for(int i = 0; i < fkCols.size(); ++i) {
281       Column pkCol = pkCols.get(i).getColumn();
282       if(!MATCHER.matches(table, pkCol.getName(),
283                           fkCols.get(i).getColumn().getRowValue(row),
284                           pkCol.getRowValue(row))) {
285         return false;
286       }
287     }
288     return true;
289   }
290 
291   private static void requireNoSecondaryValues(Joiner joiner, Object[] row)
292     throws IOException
293   {
294     // ensure that no rows exist in the secondary table for which this table is
295     // the primary table.
296     if(hasOtherSecondaryRows(joiner, row)) {
297       throw new ConstraintViolationException(
298           "Removing old row " + Arrays.asList(row) + " violates constraint " +
299           joiner.toFKString());
300     }
301   }
302 
303   /**
304    * A row of a table related to itself can be its own parent, and ms access
305    * removes such a row.  It is the only row naming it which goes away with
306    * it, so it does not count among the rows which would be left behind
307    * naming a row which is gone.
308    *
309    * @return whether a row other than the given one names it
310    */
311   private static boolean hasOtherSecondaryRows(Joiner joiner, Object[] row)
312     throws IOException
313   {
314     Table table = joiner.getFromTable();
315     if(table != joiner.getToTable()) {
316       return joiner.hasRows(row);
317     }
318 
319     // the "from" columns are the primary ones, so two rows of this table are
320     // the same row exactly when they agree on all of them
321     List<? extends Index.Column> pkCols = joiner.getColumns();
322     for(Row other : joiner.findRows(row)) {
323       for(Index.Column iCol : pkCols) {
324         Column pkCol = iCol.getColumn();
325         if(!MATCHER.matches(table, pkCol.getName(), pkCol.getRowValue(row),
326                             other.get(pkCol.getName()))) {
327           return true;
328         }
329       }
330     }
331     return false;
332   }
333 
334   private static void updateSecondaryValues(Joiner joiner, Object[] oldFromRow,
335                                             Object[] newFromRow)
336     throws IOException
337   {
338     IndexCursor toCursor = joiner.getToCursor();
339     List<? extends Index.Column> fromCols = joiner.getColumns();
340     List<? extends Index.Column> toCols = joiner.getToIndex().getColumns();
341     Object[] toRow = new Object[joiner.getToTable().getColumnCount()];
342 
343     for(Iterator<Row> iter = joiner.findRows(oldFromRow)
344           .setColumnNames(Collections.<String>emptySet())
345           .iterator(); iter.hasNext(); ) {
346       iter.next();
347 
348       // create update row for "to" table
349       Arrays.fill(toRow, Column.KEEP_VALUE);
350       for(int i = 0; i < fromCols.size(); ++i) {
351         Object val = fromCols.get(i).getColumn().getRowValue(newFromRow);
352         toCols.get(i).getColumn().setRowValue(toRow, val);
353       }
354 
355       toCursor.updateCurrentRow(toRow);
356     }
357   }
358 
359   private static void nullSecondaryValues(Joiner joiner, Object[] oldFromRow)
360     throws IOException
361   {
362     IndexCursor toCursor = joiner.getToCursor();
363     List<? extends Index.Column> fromCols = joiner.getColumns();
364     List<? extends Index.Column> toCols = joiner.getToIndex().getColumns();
365     Object[] toRow = new Object[joiner.getToTable().getColumnCount()];
366 
367     for(Iterator<Row> iter = joiner.findRows(oldFromRow)
368           .setColumnNames(Collections.<String>emptySet())
369           .iterator(); iter.hasNext(); ) {
370       iter.next();
371 
372       // create update row for "to" table
373       Arrays.fill(toRow, Column.KEEP_VALUE);
374       for(int i = 0; i < fromCols.size(); ++i) {
375         toCols.get(i).getColumn().setRowValue(toRow, null);
376       }
377 
378       toCursor.updateCurrentRow(toRow);
379     }
380   }
381 
382   private boolean anyUpdates(Object[] oldRow, Object[] newRow) {
383     for(ColumnImpl col : _cols) {
384       if(!MATCHER.matches(_table, col.getName(),
385                           col.getRowValue(oldRow), col.getRowValue(newRow))) {
386         return true;
387       }
388     }
389     return false;
390   }
391 
392   private static boolean anyUpdates(Joiner joiner,Object[] oldRow,
393                                     Object[] newRow)
394   {
395     Table fromTable = joiner.getFromTable();
396     for(Index.Column iCol : joiner.getColumns()) {
397       Column col = iCol.getColumn();
398       if(!MATCHER.matches(fromTable, col.getName(),
399                           col.getRowValue(oldRow), col.getRowValue(newRow))) {
400         return true;
401       }
402     }
403     return false;
404   }
405 
406   private static boolean areNull(Joiner joiner, Object[] row) {
407     for(Index.Column col : joiner.getColumns()) {
408       if(col.getColumn().getRowValue(row) != null) {
409         return false;
410       }
411     }
412     return true;
413   }
414 
415   private boolean enforcing() {
416     return _table.getDatabase().isEnforceForeignKeys();
417   }
418 
419   static SharedState initSharedState() {
420     return new SharedState();
421   }
422 
423   /**
424    * Shared state used by all FKEnforcers for a given Database.
425    */
426   static final class SharedState
427   {
428     /** current depth of cascading update calls across one or more tables */
429     private int _updateDepth;
430 
431     private SharedState() {
432     }
433 
434     public boolean isUpdating() {
435       return (_updateDepth == 0);
436     }
437 
438     public void pushUpdate() {
439       ++_updateDepth;
440     }
441 
442     public void popUpdate() {
443       --_updateDepth;
444     }
445   }
446 }