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 addPrimaryIndex();
112 addSecondaryIndex();
113 }
114
115 return newRel;
116
117 } finally {
118 getPageChannel().finishWrite();
119 }
120 }
121
122 private void addPrimaryIndex() throws IOException {
123 TableUpdaterpl/TableUpdater.html#TableUpdater">TableUpdater updater = new TableUpdater(_primaryTable);
124 updater.setForeignKey(createFKReference(true));
125 updater.addIndex(createPrimaryIndex(), true,
126 IGNORED_PRIMARY_INDEX_FLAGS, (byte)0);
127 }
128
129 private void addSecondaryIndex() throws IOException {
130 TableUpdaterpl/TableUpdater.html#TableUpdater">TableUpdater updater = new TableUpdater(_secondaryTable);
131 updater.setForeignKey(createFKReference(false));
132 updater.addIndex(createSecondaryIndex(), true,
133 IGNORED_SECONDARY_INDEX_FLAGS, (byte)0);
134 }
135
136 private IndexImpl.ForeignKeyReference createFKReference(boolean isPrimary) {
137 byte tableType = 0;
138 int otherTableNum = 0;
139 int otherIdxNum = 0;
140 if(isPrimary) {
141 tableType = IndexImpl.FK_PRIMARY_TABLE_TYPE;
142 otherTableNum = _secondaryTable.getTableDefPageNumber();
143
144
145 otherIdxNum = _secondaryTable.getLogicalIndexCount();
146 } else {
147 tableType = IndexImpl.FK_SECONDARY_TABLE_TYPE;
148 otherTableNum = _primaryTable.getTableDefPageNumber();
149
150
151 otherIdxNum = _primaryTable.getLogicalIndexCount() - 1;
152 }
153 boolean cascadeUpdates = ((_flags & RelationshipImpl.CASCADE_UPDATES_FLAG) != 0);
154 boolean cascadeDeletes = ((_flags & RelationshipImpl.CASCADE_DELETES_FLAG) != 0);
155 boolean cascadeNull = ((_flags & RelationshipImpl.CASCADE_NULL_FLAG) != 0);
156
157 return new IndexImpl.ForeignKeyReference(
158 tableType, otherIdxNum, otherTableNum, cascadeUpdates, cascadeDeletes,
159 cascadeNull);
160 }
161
162 private void validate() throws IOException {
163
164 _primaryTable = getDatabase().getTable(_relationship.getFromTable());
165 _secondaryTable = getDatabase().getTable(_relationship.getToTable());
166
167 if((_primaryTable == null) || (_secondaryTable == null)) {
168 throw new IllegalArgumentException(withErrorContext(
169 "Two valid tables are required in relationship"));
170 }
171
172 if(_name != null) {
173 DatabaseImpl.validateIdentifierName(
174 _name, _primaryTable.getFormat().MAX_INDEX_NAME_LENGTH, "relationship");
175 }
176
177 _primaryCols = getColumns(_primaryTable, _relationship.getFromColumns());
178 _secondaryCols = getColumns(_secondaryTable, _relationship.getToColumns());
179
180 if((_primaryCols == null) || (_primaryCols.isEmpty()) ||
181 (_secondaryCols == null) || (_secondaryCols.isEmpty())) {
182 throw new IllegalArgumentException(withErrorContext(
183 "Missing columns in relationship"));
184 }
185
186 if(_primaryCols.size() != _secondaryCols.size()) {
187 throw new IllegalArgumentException(withErrorContext(
188 "Must have same number of columns on each side of relationship"));
189 }
190
191 for(int i = 0; i < _primaryCols.size(); ++i) {
192 ColumnImpl pcol = _primaryCols.get(i);
193 ColumnImpl scol = _secondaryCols.get(i);
194
195 if(pcol.getType() != scol.getType()) {
196 throw new IllegalArgumentException(withErrorContext(
197 "Matched columns must have the same data type"));
198 }
199 }
200
201 if(!hasReferentialIntegrity()) {
202
203 if((_relationship.getFlags() & CASCADE_FLAGS) != 0) {
204 throw new IllegalArgumentException(withErrorContext(
205 "Cascade flags cannot be enabled if referential integrity is not enforced"));
206 }
207
208 return;
209 }
210
211
212
213 IndexImpl primaryIdx = getUniqueIndex(_primaryTable, _primaryCols);
214 if(primaryIdx == null) {
215 throw new IllegalArgumentException(withErrorContext(
216 "Missing unique index on primary table required to enforce integrity"));
217 }
218
219
220
221 if((new HashSet<String>(getColumnNames(_primaryCols)).size() !=
222 _primaryCols.size()) ||
223 (new HashSet<String>(getColumnNames(_secondaryCols)).size() !=
224 _secondaryCols.size())) {
225 throw new IllegalArgumentException(withErrorContext(
226 "Cannot have duplicate columns in an integrity enforced relationship"));
227 }
228
229
230
231
232 IndexCursor primaryCursor = primaryIdx.newCursor().toIndexCursor();
233 Object[] entryValues = new Object[_secondaryCols.size()];
234 for(Row row : _secondaryTable.newCursor().toCursor()
235 .newIterable().addColumns(_secondaryCols)) {
236
237 boolean hasValues = false;
238 for(int i = 0; i < _secondaryCols.size(); ++i) {
239 entryValues[i] = _secondaryCols.get(i).getRowValue(row);
240 hasValues = hasValues || (entryValues[i] != null);
241 }
242
243 if(!hasValues) {
244
245 continue;
246 }
247
248
249 if(!primaryCursor.findFirstRowByEntry(entryValues)) {
250 throw new ConstraintViolationException(withErrorContext(
251 "Integrity constraint violation found for relationship"));
252 }
253 }
254
255 }
256
257 private IndexBuilder createPrimaryIndex() {
258 String name = createPrimaryIndexName();
259 return createIndex(name, _primaryCols)
260 .setUnique()
261 .setType(IndexImpl.FOREIGN_KEY_INDEX_TYPE);
262 }
263
264 private IndexBuilder createSecondaryIndex() {
265
266 return createIndex(_name, _secondaryCols)
267 .setType(IndexImpl.FOREIGN_KEY_INDEX_TYPE);
268 }
269
270 private static IndexBuilder createIndex(String name, List<ColumnImpl> cols) {
271 IndexBuilders/IndexBuilder.html#IndexBuilder">IndexBuilder idx = new IndexBuilder(name);
272 for(ColumnImpl col : cols) {
273 idx.addColumns(col.getName());
274 }
275 return idx;
276 }
277
278 private String createPrimaryIndexName() {
279 Set<String> idxNames = TableUpdater.getIndexNames(_primaryTable, null);
280
281
282 String baseName = ".r";
283 String suffix = "B";
284
285 while(true) {
286 String idxName = baseName + suffix;
287 if(!idxNames.contains(DatabaseImpl.toLookupName(idxName))) {
288 return idxName;
289 }
290
291 char c = (char)(suffix.charAt(0) + 1);
292 if(c == '[') {
293 c = 'a';
294 }
295 suffix = "" + c;
296 }
297 }
298
299 private static List<ColumnImpl> getColumns(TableImpl table,
300 List<String> colNames) {
301 List<ColumnImpl> cols = new ArrayList<ColumnImpl>();
302 for(String colName : colNames) {
303 cols.add(table.getColumn(colName));
304 }
305 return cols;
306 }
307
308 private static List<String> getColumnNames(List<ColumnImpl> cols) {
309 List<String> colNames = new ArrayList<String>();
310 for(ColumnImpl col : cols) {
311 colNames.add(col.getName());
312 }
313 return colNames;
314 }
315
316 private boolean isOneToOne() {
317
318
319 if(getUniqueIndex(_primaryTable, _primaryCols) == null) {
320 return false;
321 }
322 IndexImpl idx = getUniqueIndex(_secondaryTable, _secondaryCols);
323 return (idx != null);
324 }
325
326 private static IndexImpl getUniqueIndex(
327 TableImpl table, List<ColumnImpl> cols) {
328 return table.findIndexForColumns(getColumnNames(cols),
329 TableImpl.IndexFeature.EXACT_UNIQUE_ONLY);
330 }
331
332 private static String getTableErrorContext(
333 TableImpl table, List<ColumnImpl> cols,
334 String tableName, Collection<String> colNames) {
335 if(table != null) {
336 tableName = table.getName();
337 }
338 if(cols != null) {
339 colNames = getColumnNames(cols);
340 }
341
342 return CustomToStringStyle.valueBuilder(tableName)
343 .append(null, colNames)
344 .toString();
345 }
346
347 private String withErrorContext(String msg) {
348 return msg + "(Rel=" +
349 getTableErrorContext(_primaryTable, _primaryCols,
350 _relationship.getFromTable(),
351 _relationship.getFromColumns()) + " -> " +
352 getTableErrorContext(_secondaryTable, _secondaryCols,
353 _relationship.getToTable(),
354 _relationship.getToColumns()) + ")";
355 }
356 }