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>();
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<Joiner>(1);
112     _primaryJoinersChkDel = new ArrayList<Joiner>(1);
113     _primaryJoinersDoUp = new ArrayList<Joiner>(1);
114     _primaryJoinersDoDel = new ArrayList<Joiner>(1);
115     _primaryJoinersDoNull = new ArrayList<Joiner>(1);
116     _secondaryJoiners = new ArrayList<Joiner>(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) && !joiner.hasRows(row)) {
254       throw new ConstraintViolationException(
255           "Adding new row " + Arrays.asList(row) + " violates constraint " +
256           joiner.toFKString());
257     }
258   }
259 
260   private static void requireNoSecondaryValues(Joiner joiner, Object[] row) 
261     throws IOException 
262   {
263     // ensure that no rows exist in the secondary table for which this table is
264     // the primary table.
265     if(joiner.hasRows(row)) {
266       throw new ConstraintViolationException(
267           "Removing old row " + Arrays.asList(row) + " violates constraint " +
268           joiner.toFKString());
269     }
270   }
271 
272   private static void updateSecondaryValues(Joiner joiner, Object[] oldFromRow,
273                                             Object[] newFromRow)
274     throws IOException
275   {
276     IndexCursor toCursor = joiner.getToCursor();
277     List<? extends Index.Column> fromCols = joiner.getColumns();
278     List<? extends Index.Column> toCols = joiner.getToIndex().getColumns();
279     Object[] toRow = new Object[joiner.getToTable().getColumnCount()];
280 
281     for(Iterator<Row> iter = joiner.findRows(oldFromRow)
282           .setColumnNames(Collections.<String>emptySet())
283           .iterator(); iter.hasNext(); ) {
284       iter.next();
285 
286       // create update row for "to" table
287       Arrays.fill(toRow, Column.KEEP_VALUE);
288       for(int i = 0; i < fromCols.size(); ++i) {
289         Object val = fromCols.get(i).getColumn().getRowValue(newFromRow);
290         toCols.get(i).getColumn().setRowValue(toRow, val);
291       }
292 
293       toCursor.updateCurrentRow(toRow);
294     }
295   }
296 
297   private static void nullSecondaryValues(Joiner joiner, Object[] oldFromRow)
298     throws IOException
299   {
300     IndexCursor toCursor = joiner.getToCursor();
301     List<? extends Index.Column> fromCols = joiner.getColumns();
302     List<? extends Index.Column> toCols = joiner.getToIndex().getColumns();
303     Object[] toRow = new Object[joiner.getToTable().getColumnCount()];
304 
305     for(Iterator<Row> iter = joiner.findRows(oldFromRow)
306           .setColumnNames(Collections.<String>emptySet())
307           .iterator(); iter.hasNext(); ) {
308       iter.next();
309 
310       // create update row for "to" table
311       Arrays.fill(toRow, Column.KEEP_VALUE);
312       for(int i = 0; i < fromCols.size(); ++i) {
313         toCols.get(i).getColumn().setRowValue(toRow, null);
314       }
315 
316       toCursor.updateCurrentRow(toRow);
317     }
318   }
319 
320   private boolean anyUpdates(Object[] oldRow, Object[] newRow) {
321     for(ColumnImpl col : _cols) {
322       if(!MATCHER.matches(_table, col.getName(),
323                           col.getRowValue(oldRow), col.getRowValue(newRow))) {
324         return true;
325       }
326     }
327     return false;
328   }
329 
330   private static boolean anyUpdates(Joiner joiner,Object[] oldRow, 
331                                     Object[] newRow)
332   {
333     Table fromTable = joiner.getFromTable();
334     for(Index.Column iCol : joiner.getColumns()) {
335       Column col = iCol.getColumn();
336       if(!MATCHER.matches(fromTable, col.getName(),
337                           col.getRowValue(oldRow), col.getRowValue(newRow))) {
338         return true;
339       }
340     }
341     return false;
342   }
343 
344   private static boolean areNull(Joiner joiner, Object[] row) {
345     for(Index.Column col : joiner.getColumns()) {
346       if(col.getColumn().getRowValue(row) != null) {
347         return false;
348       }
349     } 
350     return true;
351   }
352 
353   private boolean enforcing() {
354     return _table.getDatabase().isEnforceForeignKeys();
355   }
356 
357   static SharedState initSharedState() {
358     return new SharedState();
359   }
360 
361   /**
362    * Shared state used by all FKEnforcers for a given Database.
363    */
364   static final class SharedState 
365   {
366     /** current depth of cascading update calls across one or more tables */
367     private int _updateDepth;
368 
369     private SharedState() {
370     }
371 
372     public boolean isUpdating() {
373       return (_updateDepth == 0);
374     }
375     
376     public void pushUpdate() {
377       ++_updateDepth;
378     }
379 
380     public void popUpdate() {
381       --_updateDepth;
382     }
383   }
384 }