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.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
34
35
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
45
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
89
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
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
112
113
114
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
218
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
226
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
236
237
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
250
251
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
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
265 continue;
266 }
267
268
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
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
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
338
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 }