1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
40
41
42
43
44
45 final class FKEnforcer
46 {
47
48
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
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
74
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
87
88 void reset() {
89
90 initColumns();
91
92
93 _primaryJoinersChkUp = null;
94 _primaryJoinersChkDel = null;
95 _primaryJoinersDoUp = null;
96 _primaryJoinersDoDel = null;
97 _primaryJoinersDoNull = null;
98 _secondaryJoiners = null;
99 }
100
101
102
103
104 private void initialize() throws IOException {
105 if(_secondaryJoiners != null) {
106
107 return;
108 }
109
110
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
145
146
147
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
162
163
164
165
166
167
168 public void updateRow(Object[] oldRow, Object[] newRow) throws IOException {
169 if(!enforcing()) {
170 return;
171 }
172
173 if(!anyUpdates(oldRow, newRow)) {
174
175 return;
176 }
177
178 initialize();
179
180 SharedState ss = _table.getDatabase().getFKEnforcerSharedState();
181
182 if(ss.isUpdating()) {
183
184
185
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
197
198 for(Joiner joiner : _primaryJoinersChkUp) {
199 if(anyUpdates(joiner, oldRow, newRow)) {
200 requireNoSecondaryValues(joiner, oldRow);
201 }
202 }
203
204
205
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
219
220
221
222
223 public void deleteRow(Object[] row) throws IOException {
224 if(!enforcing()) {
225 return;
226 }
227 initialize();
228
229
230
231 for(Joiner joiner : _primaryJoinersChkDel) {
232 requireNoSecondaryValues(joiner, row);
233 }
234
235
236
237 for(Joiner joiner : _primaryJoinersDoDel) {
238 joiner.deleteRows(row);
239 }
240
241
242
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
252
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
265
266
267
268
269
270
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
295
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
305
306
307
308
309
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
320
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
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
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
425
426 static final class SharedState
427 {
428
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 }