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 }