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